about summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorelioat <elioat@tilde.institute>2024-12-22 17:04:26 -0500
committerelioat <elioat@tilde.institute>2024-12-22 17:04:26 -0500
commit9ff68eb3dd3bf57a67e11efeee7bd7d178344b7e (patch)
treeaa02a725852c9e3a9ee1d72027481e04a6048009
parentbe71ba06acd077052c2306748cb1f27800286a9e (diff)
downloadtour-9ff68eb3dd3bf57a67e11efeee7bd7d178344b7e.tar.gz
*
-rw-r--r--html/file-system/index.html614
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>