feat: Implement sidebar collapse and expand functionality

- Add CSS for collapsed sidebar state and transitions
- Introduce SidebarToggle class for managing collapse/expand logic
- Integrate SidebarToggle initialization in main script
- Add toggle button to navbar and make mini sidebar clickable
- Store sidebar collapsed state in localStorage
- Filter image files and directories in view mode via FileTree
- Make navbar brand clickable to navigate to collection root or home
This commit is contained in:
Mahmoud-Emad
2025-10-26 18:48:31 +03:00
parent 7a9efd3542
commit afcd074913
7 changed files with 290 additions and 46 deletions

View File

@@ -124,6 +124,63 @@ body {
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
/* Prevent pane scrolling */ /* Prevent pane scrolling */
transition: flex 0.3s ease, min-width 0.3s ease, max-width 0.3s ease;
}
/* Collapsed sidebar state - mini sidebar */
#sidebarPane.collapsed {
flex: 0 0 50px;
min-width: 50px;
max-width: 50px;
border-right: 1px solid var(--border-color);
position: relative;
cursor: pointer;
}
/* Hide file tree content when collapsed */
#sidebarPane.collapsed #fileTree {
display: none;
}
/* Hide collection selector when collapsed */
#sidebarPane.collapsed .collection-selector {
display: none;
}
/* Visual indicator in the mini sidebar */
#sidebarPane.collapsed::before {
content: '';
display: block;
width: 100%;
height: 100%;
background: var(--bg-secondary);
transition: background 0.2s ease;
}
/* Hover effect on mini sidebar */
#sidebarPane.collapsed:hover::before {
background: var(--hover-bg);
}
/* Right arrow icon in the center of mini sidebar */
#sidebarPane.collapsed::after {
content: '\F285';
/* Bootstrap icon chevron-right */
font-family: 'bootstrap-icons';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 20px;
color: var(--text-secondary);
pointer-events: none;
opacity: 0.5;
transition: opacity 0.2s ease;
cursor: pointer;
}
#sidebarPane.collapsed:hover::after {
opacity: 1;
} }
#editorPane { #editorPane {

View File

@@ -277,6 +277,9 @@ document.addEventListener('DOMContentLoaded', async () => {
darkMode.toggle(); darkMode.toggle();
}); });
// Initialize sidebar toggle
const sidebarToggle = new SidebarToggle('sidebarPane', 'sidebarToggleBtn');
// Initialize collection selector (always needed) // Initialize collection selector (always needed)
collectionSelector = new CollectionSelector('collectionSelect', webdavClient); collectionSelector = new CollectionSelector('collectionSelect', webdavClient);
await collectionSelector.load(); await collectionSelector.load();
@@ -321,7 +324,8 @@ document.addEventListener('DOMContentLoaded', async () => {
editor.setWebDAVClient(webdavClient); editor.setWebDAVClient(webdavClient);
// Initialize file tree (needed in both modes) // Initialize file tree (needed in both modes)
fileTree = new FileTree('fileTree', webdavClient); // Pass isEditMode to control image filtering (hide images only in view mode)
fileTree = new FileTree('fileTree', webdavClient, isEditMode);
fileTree.onFileSelect = async (item) => { fileTree.onFileSelect = async (item) => {
try { try {
const currentCollection = collectionSelector.getCurrentCollection(); const currentCollection = collectionSelector.getCurrentCollection();
@@ -579,6 +583,22 @@ document.addEventListener('DOMContentLoaded', async () => {
await autoLoadPageInViewMode(); await autoLoadPageInViewMode();
} }
// Setup clickable navbar brand (logo/title)
const navbarBrand = document.getElementById('navbarBrand');
if (navbarBrand) {
navbarBrand.addEventListener('click', (e) => {
e.preventDefault();
const currentCollection = collectionSelector ? collectionSelector.getCurrentCollection() : null;
if (currentCollection) {
// Navigate to collection root
window.location.href = `/${currentCollection}/`;
} else {
// Navigate to home page
window.location.href = '/';
}
});
}
// Initialize mermaid (always needed) // Initialize mermaid (always needed)
mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' }); mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' });
// Listen for file-saved event to reload file tree // Listen for file-saved event to reload file tree

View File

@@ -5,86 +5,86 @@
const Config = { const Config = {
// ===== TIMING CONFIGURATION ===== // ===== TIMING CONFIGURATION =====
/** /**
* Long-press threshold in milliseconds * Long-press threshold in milliseconds
* Used for drag-and-drop detection in file tree * Used for drag-and-drop detection in file tree
*/ */
LONG_PRESS_THRESHOLD: 400, LONG_PRESS_THRESHOLD: 400,
/** /**
* Debounce delay in milliseconds * Debounce delay in milliseconds
* Used for editor preview updates * Used for editor preview updates
*/ */
DEBOUNCE_DELAY: 300, DEBOUNCE_DELAY: 300,
/** /**
* Toast notification duration in milliseconds * Toast notification duration in milliseconds
*/ */
TOAST_DURATION: 3000, TOAST_DURATION: 3000,
/** /**
* Mouse move threshold in pixels * Mouse move threshold in pixels
* Used to detect if user is dragging vs clicking * Used to detect if user is dragging vs clicking
*/ */
MOUSE_MOVE_THRESHOLD: 5, MOUSE_MOVE_THRESHOLD: 5,
// ===== UI CONFIGURATION ===== // ===== UI CONFIGURATION =====
/** /**
* Drag preview width in pixels * Drag preview width in pixels
* Width of the drag ghost image during drag-and-drop * Width of the drag ghost image during drag-and-drop
*/ */
DRAG_PREVIEW_WIDTH: 200, DRAG_PREVIEW_WIDTH: 200,
/** /**
* Tree indentation in pixels * Tree indentation in pixels
* Indentation per level in the file tree * Indentation per level in the file tree
*/ */
TREE_INDENT_PX: 12, TREE_INDENT_PX: 12,
/** /**
* Toast container z-index * Toast container z-index
* Ensures toasts appear above other elements * Ensures toasts appear above other elements
*/ */
TOAST_Z_INDEX: 9999, TOAST_Z_INDEX: 9999,
/** /**
* Minimum sidebar width in pixels * Minimum sidebar width in pixels
*/ */
MIN_SIDEBAR_WIDTH: 150, MIN_SIDEBAR_WIDTH: 150,
/** /**
* Maximum sidebar width as percentage of container * Maximum sidebar width as percentage of container
*/ */
MAX_SIDEBAR_WIDTH_PERCENT: 40, MAX_SIDEBAR_WIDTH_PERCENT: 40,
/** /**
* Minimum editor width in pixels * Minimum editor width in pixels
*/ */
MIN_EDITOR_WIDTH: 250, MIN_EDITOR_WIDTH: 250,
/** /**
* Maximum editor width as percentage of container * Maximum editor width as percentage of container
*/ */
MAX_EDITOR_WIDTH_PERCENT: 70, MAX_EDITOR_WIDTH_PERCENT: 70,
// ===== VALIDATION CONFIGURATION ===== // ===== VALIDATION CONFIGURATION =====
/** /**
* Valid filename pattern * Valid filename pattern
* Only lowercase letters, numbers, underscores, and dots allowed * Only lowercase letters, numbers, underscores, and dots allowed
*/ */
FILENAME_PATTERN: /^[a-z0-9_]+(\.[a-z0-9_]+)*$/, FILENAME_PATTERN: /^[a-z0-9_]+(\.[a-z0-9_]+)*$/,
/** /**
* Characters to replace in filenames * Characters to replace in filenames
* All invalid characters will be replaced with underscore * All invalid characters will be replaced with underscore
*/ */
FILENAME_INVALID_CHARS: /[^a-z0-9_.]/g, FILENAME_INVALID_CHARS: /[^a-z0-9_.]/g,
// ===== STORAGE KEYS ===== // ===== STORAGE KEYS =====
/** /**
* LocalStorage keys used throughout the application * LocalStorage keys used throughout the application
*/ */
@@ -93,99 +93,104 @@ const Config = {
* Dark mode preference * Dark mode preference
*/ */
DARK_MODE: 'darkMode', DARK_MODE: 'darkMode',
/** /**
* Currently selected collection * Currently selected collection
*/ */
SELECTED_COLLECTION: 'selectedCollection', SELECTED_COLLECTION: 'selectedCollection',
/** /**
* Last viewed page (per collection) * Last viewed page (per collection)
* Actual key will be: lastViewedPage:{collection} * Actual key will be: lastViewedPage:{collection}
*/ */
LAST_VIEWED_PAGE: 'lastViewedPage', LAST_VIEWED_PAGE: 'lastViewedPage',
/** /**
* Column dimensions (sidebar, editor, preview widths) * Column dimensions (sidebar, editor, preview widths)
*/ */
COLUMN_DIMENSIONS: 'columnDimensions' COLUMN_DIMENSIONS: 'columnDimensions',
/**
* Sidebar collapsed state
*/
SIDEBAR_COLLAPSED: 'sidebarCollapsed'
}, },
// ===== EDITOR CONFIGURATION ===== // ===== EDITOR CONFIGURATION =====
/** /**
* CodeMirror theme for light mode * CodeMirror theme for light mode
*/ */
EDITOR_THEME_LIGHT: 'default', EDITOR_THEME_LIGHT: 'default',
/** /**
* CodeMirror theme for dark mode * CodeMirror theme for dark mode
*/ */
EDITOR_THEME_DARK: 'monokai', EDITOR_THEME_DARK: 'monokai',
/** /**
* Mermaid theme for light mode * Mermaid theme for light mode
*/ */
MERMAID_THEME_LIGHT: 'default', MERMAID_THEME_LIGHT: 'default',
/** /**
* Mermaid theme for dark mode * Mermaid theme for dark mode
*/ */
MERMAID_THEME_DARK: 'dark', MERMAID_THEME_DARK: 'dark',
// ===== FILE TREE CONFIGURATION ===== // ===== FILE TREE CONFIGURATION =====
/** /**
* Default content for new files * Default content for new files
*/ */
DEFAULT_FILE_CONTENT: '# New File\n\n', DEFAULT_FILE_CONTENT: '# New File\n\n',
/** /**
* Default filename for new files * Default filename for new files
*/ */
DEFAULT_NEW_FILENAME: 'new_file.md', DEFAULT_NEW_FILENAME: 'new_file.md',
/** /**
* Default folder name for new folders * Default folder name for new folders
*/ */
DEFAULT_NEW_FOLDERNAME: 'new_folder', DEFAULT_NEW_FOLDERNAME: 'new_folder',
// ===== WEBDAV CONFIGURATION ===== // ===== WEBDAV CONFIGURATION =====
/** /**
* WebDAV base URL * WebDAV base URL
*/ */
WEBDAV_BASE_URL: '/fs/', WEBDAV_BASE_URL: '/fs/',
/** /**
* PROPFIND depth for file tree loading * PROPFIND depth for file tree loading
*/ */
PROPFIND_DEPTH: 'infinity', PROPFIND_DEPTH: 'infinity',
// ===== DRAG AND DROP CONFIGURATION ===== // ===== DRAG AND DROP CONFIGURATION =====
/** /**
* Drag preview opacity * Drag preview opacity
*/ */
DRAG_PREVIEW_OPACITY: 0.8, DRAG_PREVIEW_OPACITY: 0.8,
/** /**
* Dragging item opacity * Dragging item opacity
*/ */
DRAGGING_OPACITY: 0.4, DRAGGING_OPACITY: 0.4,
/** /**
* Drag preview offset X in pixels * Drag preview offset X in pixels
*/ */
DRAG_PREVIEW_OFFSET_X: 10, DRAG_PREVIEW_OFFSET_X: 10,
/** /**
* Drag preview offset Y in pixels * Drag preview offset Y in pixels
*/ */
DRAG_PREVIEW_OFFSET_Y: 10, DRAG_PREVIEW_OFFSET_Y: 10,
// ===== NOTIFICATION TYPES ===== // ===== NOTIFICATION TYPES =====
/** /**
* Bootstrap notification type mappings * Bootstrap notification type mappings
*/ */

View File

@@ -4,13 +4,14 @@
*/ */
class FileTree { class FileTree {
constructor(containerId, webdavClient) { constructor(containerId, webdavClient, isEditMode = false) {
this.container = document.getElementById(containerId); this.container = document.getElementById(containerId);
this.webdavClient = webdavClient; this.webdavClient = webdavClient;
this.tree = []; this.tree = [];
this.selectedPath = null; this.selectedPath = null;
this.onFileSelect = null; this.onFileSelect = null;
this.onFolderSelect = null; this.onFolderSelect = null;
this.filterImagesInViewMode = !isEditMode; // Track if we should filter images (true in view mode)
// Drag and drop state // Drag and drop state
this.draggedNode = null; this.draggedNode = null;
@@ -426,6 +427,19 @@ class FileTree {
renderNodes(nodes, parentElement, level) { renderNodes(nodes, parentElement, level) {
nodes.forEach(node => { nodes.forEach(node => {
// Filter out images and image directories in view mode
if (this.filterImagesInViewMode) {
// Skip image files
if (!node.isDirectory && PathUtils.isBinaryFile(node.path)) {
return;
}
// Skip image directories
if (node.isDirectory && PathUtils.isImageDirectory(node.path)) {
return;
}
}
const nodeWrapper = document.createElement('div'); const nodeWrapper = document.createElement('div');
nodeWrapper.className = 'tree-node-wrapper'; nodeWrapper.className = 'tree-node-wrapper';

114
static/js/sidebar-toggle.js Normal file
View File

@@ -0,0 +1,114 @@
/**
* Sidebar Toggle Module
* Manages sidebar collapse/expand functionality with localStorage persistence
*/
class SidebarToggle {
constructor(sidebarId, toggleButtonId) {
this.sidebar = document.getElementById(sidebarId);
this.toggleButton = document.getElementById(toggleButtonId);
this.storageKey = Config.STORAGE_KEYS.SIDEBAR_COLLAPSED || 'sidebarCollapsed';
this.isCollapsed = localStorage.getItem(this.storageKey) === 'true';
this.init();
}
/**
* Initialize the sidebar toggle
*/
init() {
// Apply initial state
this.apply();
// Setup toggle button click handler
if (this.toggleButton) {
this.toggleButton.addEventListener('click', () => {
this.toggle();
});
}
// Make mini sidebar clickable to expand
if (this.sidebar) {
this.sidebar.addEventListener('click', (e) => {
// Only expand if sidebar is collapsed and click is on the mini sidebar itself
// (not on the file tree content when expanded)
if (this.isCollapsed) {
this.expand();
}
});
// Add cursor pointer when collapsed
this.sidebar.style.cursor = 'default';
}
Logger.debug(`Sidebar initialized: ${this.isCollapsed ? 'collapsed' : 'expanded'}`);
}
/**
* Toggle sidebar state
*/
toggle() {
this.isCollapsed = !this.isCollapsed;
localStorage.setItem(this.storageKey, this.isCollapsed);
this.apply();
Logger.debug(`Sidebar ${this.isCollapsed ? 'collapsed' : 'expanded'}`);
}
/**
* Apply the current sidebar state
*/
apply() {
if (this.sidebar) {
if (this.isCollapsed) {
this.sidebar.classList.add('collapsed');
this.sidebar.style.cursor = 'pointer'; // Make mini sidebar clickable
} else {
this.sidebar.classList.remove('collapsed');
this.sidebar.style.cursor = 'default'; // Normal cursor when expanded
}
}
// Update toggle button icon
if (this.toggleButton) {
const icon = this.toggleButton.querySelector('i');
if (icon) {
if (this.isCollapsed) {
icon.className = 'bi bi-layout-sidebar-inset-reverse';
} else {
icon.className = 'bi bi-layout-sidebar';
}
}
}
}
/**
* Collapse the sidebar
*/
collapse() {
if (!this.isCollapsed) {
this.toggle();
}
}
/**
* Expand the sidebar
*/
expand() {
if (this.isCollapsed) {
this.toggle();
}
}
/**
* Check if sidebar is currently collapsed
* @returns {boolean} True if sidebar is collapsed
*/
isCollapsedState() {
return this.isCollapsed;
}
}
// Make SidebarToggle globally available
window.SidebarToggle = SidebarToggle;

View File

@@ -103,6 +103,31 @@ const PathUtils = {
return binaryExtensions.includes(extension); return binaryExtensions.includes(extension);
}, },
/**
* Check if a directory is an image directory based on its name
* @param {string} path - The directory path
* @returns {boolean} True if the directory is for images
* @example PathUtils.isImageDirectory('images') // true
* @example PathUtils.isImageDirectory('assets/images') // true
* @example PathUtils.isImageDirectory('docs') // false
*/
isImageDirectory(path) {
const dirName = PathUtils.getFileName(path).toLowerCase();
const imageDirectoryNames = [
'images',
'image',
'img',
'imgs',
'pictures',
'pics',
'photos',
'assets',
'media',
'static'
];
return imageDirectoryNames.includes(dirName);
},
/** /**
* Get a human-readable file type description * Get a human-readable file type description
* @param {string} path - The file path * @param {string} path - The file path

View File

@@ -30,10 +30,18 @@
<!-- Navbar --> <!-- Navbar -->
<nav class="navbar navbar-expand-lg"> <nav class="navbar navbar-expand-lg">
<div class="container-fluid"> <div class="container-fluid">
<!-- Left: Logo and Title --> <!-- Left: Sidebar Toggle + Logo and Title -->
<span class="navbar-brand mb-0"> <div class="d-flex align-items-center gap-2">
<i class="bi bi-markdown"></i> Markdown Editor <!-- Sidebar Toggle Button -->
</span> <button id="sidebarToggleBtn" class="btn-flat btn-flat-secondary" title="Toggle Sidebar">
<i class="bi bi-layout-sidebar"></i>
</button>
<!-- Logo and Title (Clickable) -->
<a href="/" class="navbar-brand mb-0" id="navbarBrand" style="cursor: pointer; text-decoration: none;">
<i class="bi bi-markdown"></i> Markdown Editor
</a>
</div>
<!-- Right: All Buttons --> <!-- Right: All Buttons -->
<div class="ms-auto d-flex gap-2 align-items-center"> <div class="ms-auto d-flex gap-2 align-items-center">
@@ -227,6 +235,7 @@
<script src="/static/js/context-menu.js" defer></script> <script src="/static/js/context-menu.js" defer></script>
<script src="/static/js/file-upload.js" defer></script> <script src="/static/js/file-upload.js" defer></script>
<script src="/static/js/dark-mode.js" defer></script> <script src="/static/js/dark-mode.js" defer></script>
<script src="/static/js/sidebar-toggle.js" defer></script>
<script src="/static/js/collection-selector.js" defer></script> <script src="/static/js/collection-selector.js" defer></script>
<script src="/static/js/editor-drop-handler.js" defer></script> <script src="/static/js/editor-drop-handler.js" defer></script>