diff options
author | elioat <elioat@tilde.institute> | 2024-12-22 17:04:26 -0500 |
---|---|---|
committer | elioat <elioat@tilde.institute> | 2024-12-22 17:04:26 -0500 |
commit | 9ff68eb3dd3bf57a67e11efeee7bd7d178344b7e (patch) | |
tree | aa02a725852c9e3a9ee1d72027481e04a6048009 | |
parent | be71ba06acd077052c2306748cb1f27800286a9e (diff) | |
download | tour-9ff68eb3dd3bf57a67e11efeee7bd7d178344b7e.tar.gz |
*
-rw-r--r-- | html/file-system/index.html | 614 |
1 files changed, 599 insertions, 15 deletions
diff --git a/html/file-system/index.html b/html/file-system/index.html index 89e05b8..6743b8a 100644 --- a/html/file-system/index.html +++ b/html/file-system/index.html @@ -39,24 +39,53 @@ cursor: pointer; } .folder { - display: inline-block; - padding: 0.3125em 0; - cursor: pointer; - font-size: 1.125em; - color: black; + display: flex; + align-items: center; + padding: 0.5em; + margin: 0.25em 0; + border-radius: 4px; + transition: background-color 0.2s; + } + .folder:hover { + background-color: rgba(0, 0, 0, 0.05); + } + .folder.dragging { + opacity: 0.5; + } + .folder.drag-over { + background-color: rgba(0, 120, 250, 0.1); + border: 2px dashed #0078fa; + } + .folder::before { + content: "📁"; + margin-right: 0.5em; + } + ul { + padding-left: 1.5em; + border-left: 1px solid #e0e0e0; + } + li { + margin: 0; + padding: 0; } #editor { margin-top: 1.25em; width: 100%; max-width: 37.5em; + cursor: move; + min-width: 200px; + min-height: 150px; + resize: both; + overflow: auto; } #editor textarea { width: 100%; - height: 9.375em; + height: calc(100% - 3em); font-size: 1em; padding: 0.625em; box-sizing: border-box; resize: none; + margin-bottom: 0; } h1 { text-align: center; @@ -79,6 +108,191 @@ font-size: 0.875em; } } + #navigation { + display: none; /* Hide the button navigation by default */ + } + + .context-menu { + position: fixed; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + box-shadow: 2px 2px 5px rgba(0,0,0,0.2); + padding: 0.5em 0; + min-width: 150px; + z-index: 1000; + display: none; + } + + .context-menu.active { + display: block; + } + + .context-menu-item { + padding: 0.5em 1em; + cursor: pointer; + display: flex; + align-items: center; + } + + .context-menu-item:hover { + background-color: #f0f0f0; + } + + .context-menu-item::before { + margin-right: 0.5em; + } + + .context-menu-item.new-folder::before { + content: "📁"; + } + + .context-menu-item.copy::before { + content: "📋"; + } + + .context-menu-item.paste::before { + content: "📥"; + } + + .context-menu-item.delete::before { + content: "🗑️"; + } + + #editor { + display: none; /* Hide editor by default */ + position: fixed; + right: 20px; + top: 20px; + background: white; + border: 1px solid #ccc; + border-radius: 4px; + padding: 1em; + box-shadow: 2px 2px 5px rgba(0,0,0,0.2); + } + + .folder.selected { + background-color: rgba(0, 120, 250, 0.1); + } + + #fileSystemContainer { + width: 100%; + max-width: 37.5em; + min-height: 300px; + background: white; + border: 1px solid #e0e0e0; + border-radius: 4px; + padding: 1em; + margin-top: 1em; + box-sizing: border-box; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 200px; + color: #666; + text-align: center; + } + + .empty-state::before { + content: "📁"; + font-size: 3em; + margin-bottom: 0.5em; + opacity: 0.5; + } + + .empty-state-text { + font-size: 1.1em; + margin-bottom: 1em; + } + + .root-folder { + padding: 0.5em; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; + } + + .root-folder:hover { + background-color: rgba(0, 0, 0, 0.05); + } + + .root-folder.selected { + background-color: rgba(0, 120, 250, 0.1); + } + + #editorHeader { + padding: 0.5em; + background-color: #f5f5f5; + border-bottom: 1px solid #ccc; + margin: -1em -1em 1em -1em; + border-radius: 4px 4px 0 0; + display: flex; + justify-content: space-between; + align-items: center; + gap: 0.5em; + } + + #editorHeader h2 { + margin: 0; + font-size: 1.1em; + } + + .editor-controls { + display: flex; + gap: 0.3em; + } + + .editor-button { + cursor: pointer; + padding: 0.2em 0.5em; + border-radius: 3px; + font-size: 0.9em; + color: #666; + } + + .editor-button:hover { + background-color: #e0e0e0; + } + + .resize-handle { + position: absolute; + width: 10px; + height: 10px; + background-color: #f5f5f5; + border: 1px solid #ccc; + } + + .resize-handle.se { + right: 0; + bottom: 0; + cursor: se-resize; + } + + .resize-handle.sw { + left: 0; + bottom: 0; + cursor: sw-resize; + } + + .resize-handle.ne { + right: 0; + top: 0; + cursor: ne-resize; + } + + .resize-handle.nw { + left: 0; + top: 0; + cursor: nw-resize; + } + + .resize-handle:hover { + background-color: #e0e0e0; + } </style> </head> <body> @@ -92,13 +306,38 @@ <button onclick="deleteItem()">Delete</button> </div> -<h1 id="currentPath">Current Folder: /</h1> - -<ul id="fileSystemTree"></ul> +<div id="fileSystemContainer" oncontextmenu="showContextMenu(event, 'root')"> + <div class="root-folder" onclick="selectFolder('root')">Root</div> + <ul id="fileSystemTree"></ul> + <div class="empty-state" style="display: none;"> + <div class="empty-state-text">No folders yet</div> + <div class="empty-state-hint">Right-click or use the menu to create a folder</div> + </div> +</div> <div id="editor"> - <h2>Folder Content Editor</h2> + <div id="editorHeader"> + <h2><span id="editorPath">/</span></h2> + <div class="editor-controls"> + <span class="editor-button" onclick="setEditorSize('normal')" title="Normal Size">⊡</span> + <span class="editor-button" onclick="setEditorSize('vertical')" title="Vertical Split">⊢</span> + <span class="editor-button" onclick="setEditorSize('horizontal')" title="Horizontal Split">⊤</span> + <span class="editor-button" onclick="setEditorSize('full')" title="Full Screen">⛶</span> + <span class="editor-button" id="closeEditor" onclick="hideEditor()" title="Close">✕</span> + </div> + </div> <textarea id="folderContent" placeholder="Select a folder to edit its contents"></textarea> + <div class="resize-handle se"></div> + <div class="resize-handle sw"></div> + <div class="resize-handle ne"></div> + <div class="resize-handle nw"></div> +</div> + +<div class="context-menu" id="contextMenu"> + <div class="context-menu-item new-folder" onclick="createFolderPrompt()">New Folder</div> + <div class="context-menu-item copy" onclick="copyItem()">Copy</div> + <div class="context-menu-item paste" onclick="pasteItem()">Paste</div> + <div class="context-menu-item delete" onclick="deleteItem()">Delete</div> </div> <script> @@ -111,8 +350,9 @@ const loadFileSystem = () => JSON.parse(localStorage.getItem('fileSystem')) || i const state = { fileSystem: loadFileSystem(), currentFolderPath: 'root', - copiedFolderData: null, // Pastebin for the copied folder - copiedItemPath: null + copiedFolderData: null, + copiedItemPath: null, + draggedPath: null }; const clone = (obj) => JSON.parse(JSON.stringify(obj)); @@ -253,21 +493,95 @@ document.getElementById('folderContent').addEventListener('input', () => { saveFolderContent(); }); +const handleDragStart = (e, path) => { + e.target.classList.add('dragging'); + e.dataTransfer.setData('text/plain', path); + state.draggedPath = path; +}; + +const handleDragEnd = (e) => { + e.target.classList.remove('dragging'); + document.querySelectorAll('.folder').forEach(f => { + f.classList.remove('drag-over'); + }); +}; + +const handleDragOver = (e) => { + e.preventDefault(); + e.target.classList.add('drag-over'); +}; + +const handleDragLeave = (e) => { + e.target.classList.remove('drag-over'); +}; + +const handleDrop = (e, targetPath) => { + e.preventDefault(); + e.target.classList.remove('drag-over'); + + if (!state.draggedPath || state.draggedPath === targetPath) return; + + const sourcePath = state.draggedPath; + const sourceFolder = getFolder(sourcePath); + + if (!sourceFolder) return; + + // Create a copy of the dragged folder + const [, folderName] = sourcePath.split(/\/([^\/]+)$/); + const updatedFileSystem = addFolder(targetPath, folderName, clone(sourceFolder), state.fileSystem); + + if (updatedFileSystem) { + // Remove the original folder + updateFileSystem(deleteFolder(sourcePath, updatedFileSystem)); + state.draggedPath = null; + render(); + } +}; + const renderFolderTree = (folder, path = 'root') => { const entries = Object.entries(folder.children); return entries.length ? entries.map(([name, item]) => ` <li> - <span class="folder" onclick="selectFolder('${path}/${name}')">${name}</span> + <div class="folder" + onclick="selectFolder('${path}/${name}')" + oncontextmenu="showContextMenu(event, '${path}/${name}')" + draggable="true" + ondragstart="handleDragStart(event, '${path}/${name}')" + ondragend="handleDragEnd(event)" + ondragover="handleDragOver(event)" + ondragleave="handleDragLeave(event)" + ondrop="handleDrop(event, '${path}/${name}')" + >${name}</div> <ul>${renderFolderTree(item, `${path}/${name}`)}</ul> </li> `).join('') : ''; }; const selectFolder = (path) => { - const folder = getFolder(path); + const folder = path === 'root' ? state.fileSystem.root : getFolder(path); if (folder) { + // Remove previous selection + document.querySelectorAll('.folder.selected, .root-folder.selected').forEach(f => { + f.classList.remove('selected'); + }); + + // Add selection to current folder + if (path === 'root') { + document.querySelector('.root-folder').classList.add('selected'); + } else { + const folderElement = document.querySelector(`.folder[onclick*="${path}"]`); + if (folderElement) { + folderElement.classList.add('selected'); + } + } + state.currentFolderPath = path; document.getElementById('folderContent').value = folder.content || ''; + + // Show editor in normal size + editor.style.display = 'block'; + setEditorSize('normal'); + render(); } else { console.error('Folder not found', path); @@ -281,8 +595,27 @@ const render = () => { alert('File system is not initialized correctly'); return; } - document.getElementById('currentPath').textContent = state.currentFolderPath.replace('root', '') || '/'; + + const entries = Object.entries(state.fileSystem.root.children); + const isEmpty = entries.length === 0; + + // Update path displays + const currentPath = state.currentFolderPath.replace('root', '') || '/'; + document.title = `Folder: ${currentPath}`; + document.getElementById('editorPath').textContent = currentPath; + + // Update root folder selection state + const rootFolder = document.querySelector('.root-folder'); + if (state.currentFolderPath === 'root') { + rootFolder.classList.add('selected'); + } else { + rootFolder.classList.remove('selected'); + } + + // Update folder tree and empty state document.getElementById('fileSystemTree').innerHTML = renderFolderTree(state.fileSystem.root); + const emptyState = document.querySelector('.empty-state'); + emptyState.style.display = isEmpty ? 'flex' : 'none'; }; const createFolderPrompt = () => { @@ -290,6 +623,257 @@ const createFolderPrompt = () => { if (name) createFolder(state.currentFolderPath, name); }; +const contextMenu = document.getElementById('contextMenu'); + +// Prevent default context menu +document.addEventListener('contextmenu', (e) => { + e.preventDefault(); +}); + +// Hide context menu when clicking outside +document.addEventListener('click', (e) => { + if (!contextMenu.contains(e.target)) { + contextMenu.classList.remove('active'); + } +}); + +const showContextMenu = (e, path) => { + e.preventDefault(); + + // Update current folder path + if (path) { + state.currentFolderPath = path; + } + + // Position the menu + contextMenu.style.left = `${e.pageX}px`; + contextMenu.style.top = `${e.pageY}px`; + + // Show the menu + contextMenu.classList.add('active'); + + // Enable/disable paste option + const pasteOption = contextMenu.querySelector('.paste'); + pasteOption.style.opacity = state.copiedFolderData ? '1' : '0.5'; + + // Prevent menu from going off-screen + const rect = contextMenu.getBoundingClientRect(); + if (rect.right > window.innerWidth) { + contextMenu.style.left = `${e.pageX - rect.width}px`; + } + if (rect.bottom > window.innerHeight) { + contextMenu.style.top = `${e.pageY - rect.height}px`; + } +}; + +// Add double-click handler to show/hide editor +document.addEventListener('dblclick', (e) => { + if (!e.target.classList.contains('folder')) { + document.getElementById('editor').style.display = 'none'; + } +}); + +const editor = document.getElementById('editor'); +const editorHeader = document.getElementById('editorHeader'); + +let isDragging = false; +let currentX; +let currentY; +let initialX; +let initialY; +let xOffset = 0; +let yOffset = 0; +let isResizing = false; +let currentHandle = null; +let startWidth, startHeight, startX, startY; + +const dragStart = (e) => { + if (e.target.closest('#closeEditor') || e.target.classList.contains('resize-handle')) return; + + if (e.type === "touchstart") { + initialX = e.touches[0].clientX - xOffset; + initialY = e.touches[0].clientY - yOffset; + } else { + initialX = e.clientX - xOffset; + initialY = e.clientY - yOffset; + } + + if (e.target === editorHeader || e.target.closest('#editorHeader')) { + isDragging = true; + } +}; + +const dragEnd = () => { + initialX = currentX; + initialY = currentY; + isDragging = false; +}; + +const drag = (e) => { + if (isDragging) { + e.preventDefault(); + + if (e.type === "touchmove") { + currentX = e.touches[0].clientX - initialX; + currentY = e.touches[0].clientY - initialY; + } else { + currentX = e.clientX - initialX; + currentY = e.clientY - initialY; + } + + xOffset = currentX; + yOffset = currentY; + + setTranslate(currentX, currentY, editor); + } +}; + +const setTranslate = (xPos, yPos, el) => { + el.style.transform = `translate3d(${xPos}px, ${yPos}px, 0)`; +}; + +const hideEditor = () => { + editor.style.display = 'none'; +}; + +// Add event listeners for drag functionality +editorHeader.addEventListener("touchstart", dragStart, false); +editorHeader.addEventListener("touchend", dragEnd, false); +editorHeader.addEventListener("touchmove", drag, false); + +editorHeader.addEventListener("mousedown", dragStart, false); +document.addEventListener("mousemove", drag, false); +document.addEventListener("mouseup", dragEnd, false); + +// Add resize functionality +const initResize = (e) => { + if (e.target.classList.contains('resize-handle')) { + isResizing = true; + currentHandle = e.target; + + startWidth = editor.offsetWidth; + startHeight = editor.offsetHeight; + startX = e.clientX; + startY = e.clientY; + + e.preventDefault(); + e.stopPropagation(); + } +}; + +const doResize = (e) => { + if (!isResizing) return; + + const deltaX = e.clientX - startX; + const deltaY = e.clientY - startY; + + let newWidth = startWidth; + let newHeight = startHeight; + + if (currentHandle.classList.contains('se') || currentHandle.classList.contains('ne')) { + newWidth = startWidth + deltaX; + } else if (currentHandle.classList.contains('sw') || currentHandle.classList.contains('nw')) { + newWidth = startWidth - deltaX; + editor.style.left = `${editor.offsetLeft + deltaX}px`; + } + + if (currentHandle.classList.contains('se') || currentHandle.classList.contains('sw')) { + newHeight = startHeight + deltaY; + } else if (currentHandle.classList.contains('ne') || currentHandle.classList.contains('nw')) { + newHeight = startHeight - deltaY; + editor.style.top = `${editor.offsetTop + deltaY}px`; + } + + // Apply minimum dimensions + newWidth = Math.max(200, newWidth); + newHeight = Math.max(150, newHeight); + + editor.style.width = `${newWidth}px`; + editor.style.height = `${newHeight}px`; +}; + +const stopResize = () => { + isResizing = false; + currentHandle = null; +}; + +// Add event listeners for resize +document.addEventListener('mousedown', initResize); +document.addEventListener('mousemove', doResize); +document.addEventListener('mouseup', stopResize); + +const setEditorSize = (mode, editorElement = editor) => { + // Reset any existing transforms and positioning + editorElement.style.transform = 'none'; + editorElement.style.transition = 'all 0.3s ease'; + + // Get viewport dimensions and convert padding to pixels + const vw = window.innerWidth; + const vh = window.innerHeight; + const padding = '2em'; + const paddingPx = parseFloat(getComputedStyle(document.documentElement).fontSize) * 2; + const fullModePadding = '4em'; + const fullModePaddingPx = parseFloat(getComputedStyle(document.documentElement).fontSize) * 4; + + // Reset all positioning first + editorElement.style.position = 'fixed'; + editorElement.style.margin = '0'; + editorElement.style.maxWidth = 'none'; + editorElement.style.bottom = padding; // Always set bottom padding + + switch (mode) { + case 'normal': + editorElement.style.width = '37.5em'; + editorElement.style.height = '300px'; + editorElement.style.right = '20px'; + editorElement.style.top = '20px'; + editorElement.style.left = 'auto'; + editorElement.style.bottom = 'auto'; // Override for normal mode + break; + + case 'vertical': + // Take up the right half of the viewport + editorElement.style.width = `${(vw - paddingPx * 3) / 2}px`; + editorElement.style.height = `${vh - paddingPx * 2}px`; + editorElement.style.right = padding; + editorElement.style.top = padding; + editorElement.style.left = 'auto'; + break; + + case 'horizontal': + // Take up the top half of the viewport + editorElement.style.width = `${vw - paddingPx * 2}px`; + editorElement.style.height = `${(vh - paddingPx * 3) / 2}px`; + editorElement.style.right = padding; + editorElement.style.top = padding; + editorElement.style.left = padding; + break; + + case 'full': + // Take up the full viewport with larger padding + editorElement.style.width = `${vw - fullModePaddingPx * 2}px`; + editorElement.style.height = `${vh - fullModePaddingPx * 2}px`; + editorElement.style.top = fullModePadding; + editorElement.style.right = fullModePadding; + editorElement.style.bottom = fullModePadding; + editorElement.style.left = fullModePadding; + break; + } + + // Reset offsets used by drag functionality + xOffset = 0; + yOffset = 0; + + // Remove resize handles when not in normal mode + const resizeHandles = editorElement.querySelectorAll('.resize-handle'); + resizeHandles.forEach(handle => { + handle.style.display = mode === 'normal' ? 'block' : 'none'; + }); + + // Ensure the editor is visible + editorElement.style.display = 'block'; +}; + render(); </script> |