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:
@@ -277,6 +277,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
darkMode.toggle();
|
||||
});
|
||||
|
||||
// Initialize sidebar toggle
|
||||
const sidebarToggle = new SidebarToggle('sidebarPane', 'sidebarToggleBtn');
|
||||
|
||||
// Initialize collection selector (always needed)
|
||||
collectionSelector = new CollectionSelector('collectionSelect', webdavClient);
|
||||
await collectionSelector.load();
|
||||
@@ -321,7 +324,8 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
editor.setWebDAVClient(webdavClient);
|
||||
|
||||
// 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) => {
|
||||
try {
|
||||
const currentCollection = collectionSelector.getCurrentCollection();
|
||||
@@ -579,6 +583,22 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
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)
|
||||
mermaid.initialize({ startOnLoad: true, theme: darkMode.isDark ? 'dark' : 'default' });
|
||||
// Listen for file-saved event to reload file tree
|
||||
|
||||
@@ -5,86 +5,86 @@
|
||||
|
||||
const Config = {
|
||||
// ===== TIMING CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* Long-press threshold in milliseconds
|
||||
* Used for drag-and-drop detection in file tree
|
||||
*/
|
||||
LONG_PRESS_THRESHOLD: 400,
|
||||
|
||||
|
||||
/**
|
||||
* Debounce delay in milliseconds
|
||||
* Used for editor preview updates
|
||||
*/
|
||||
DEBOUNCE_DELAY: 300,
|
||||
|
||||
|
||||
/**
|
||||
* Toast notification duration in milliseconds
|
||||
*/
|
||||
TOAST_DURATION: 3000,
|
||||
|
||||
|
||||
/**
|
||||
* Mouse move threshold in pixels
|
||||
* Used to detect if user is dragging vs clicking
|
||||
*/
|
||||
MOUSE_MOVE_THRESHOLD: 5,
|
||||
|
||||
|
||||
// ===== UI CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* Drag preview width in pixels
|
||||
* Width of the drag ghost image during drag-and-drop
|
||||
*/
|
||||
DRAG_PREVIEW_WIDTH: 200,
|
||||
|
||||
|
||||
/**
|
||||
* Tree indentation in pixels
|
||||
* Indentation per level in the file tree
|
||||
*/
|
||||
TREE_INDENT_PX: 12,
|
||||
|
||||
|
||||
/**
|
||||
* Toast container z-index
|
||||
* Ensures toasts appear above other elements
|
||||
*/
|
||||
TOAST_Z_INDEX: 9999,
|
||||
|
||||
|
||||
/**
|
||||
* Minimum sidebar width in pixels
|
||||
*/
|
||||
MIN_SIDEBAR_WIDTH: 150,
|
||||
|
||||
|
||||
/**
|
||||
* Maximum sidebar width as percentage of container
|
||||
*/
|
||||
MAX_SIDEBAR_WIDTH_PERCENT: 40,
|
||||
|
||||
|
||||
/**
|
||||
* Minimum editor width in pixels
|
||||
*/
|
||||
MIN_EDITOR_WIDTH: 250,
|
||||
|
||||
|
||||
/**
|
||||
* Maximum editor width as percentage of container
|
||||
*/
|
||||
MAX_EDITOR_WIDTH_PERCENT: 70,
|
||||
|
||||
|
||||
// ===== VALIDATION CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* Valid filename pattern
|
||||
* Only lowercase letters, numbers, underscores, and dots allowed
|
||||
*/
|
||||
FILENAME_PATTERN: /^[a-z0-9_]+(\.[a-z0-9_]+)*$/,
|
||||
|
||||
|
||||
/**
|
||||
* Characters to replace in filenames
|
||||
* All invalid characters will be replaced with underscore
|
||||
*/
|
||||
FILENAME_INVALID_CHARS: /[^a-z0-9_.]/g,
|
||||
|
||||
|
||||
// ===== STORAGE KEYS =====
|
||||
|
||||
|
||||
/**
|
||||
* LocalStorage keys used throughout the application
|
||||
*/
|
||||
@@ -93,99 +93,104 @@ const Config = {
|
||||
* Dark mode preference
|
||||
*/
|
||||
DARK_MODE: 'darkMode',
|
||||
|
||||
|
||||
/**
|
||||
* Currently selected collection
|
||||
*/
|
||||
SELECTED_COLLECTION: 'selectedCollection',
|
||||
|
||||
|
||||
/**
|
||||
* Last viewed page (per collection)
|
||||
* Actual key will be: lastViewedPage:{collection}
|
||||
*/
|
||||
LAST_VIEWED_PAGE: 'lastViewedPage',
|
||||
|
||||
|
||||
/**
|
||||
* Column dimensions (sidebar, editor, preview widths)
|
||||
*/
|
||||
COLUMN_DIMENSIONS: 'columnDimensions'
|
||||
COLUMN_DIMENSIONS: 'columnDimensions',
|
||||
|
||||
/**
|
||||
* Sidebar collapsed state
|
||||
*/
|
||||
SIDEBAR_COLLAPSED: 'sidebarCollapsed'
|
||||
},
|
||||
|
||||
|
||||
// ===== EDITOR CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* CodeMirror theme for light mode
|
||||
*/
|
||||
EDITOR_THEME_LIGHT: 'default',
|
||||
|
||||
|
||||
/**
|
||||
* CodeMirror theme for dark mode
|
||||
*/
|
||||
EDITOR_THEME_DARK: 'monokai',
|
||||
|
||||
|
||||
/**
|
||||
* Mermaid theme for light mode
|
||||
*/
|
||||
MERMAID_THEME_LIGHT: 'default',
|
||||
|
||||
|
||||
/**
|
||||
* Mermaid theme for dark mode
|
||||
*/
|
||||
MERMAID_THEME_DARK: 'dark',
|
||||
|
||||
|
||||
// ===== FILE TREE CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* Default content for new files
|
||||
*/
|
||||
DEFAULT_FILE_CONTENT: '# New File\n\n',
|
||||
|
||||
|
||||
/**
|
||||
* Default filename for new files
|
||||
*/
|
||||
DEFAULT_NEW_FILENAME: 'new_file.md',
|
||||
|
||||
|
||||
/**
|
||||
* Default folder name for new folders
|
||||
*/
|
||||
DEFAULT_NEW_FOLDERNAME: 'new_folder',
|
||||
|
||||
|
||||
// ===== WEBDAV CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* WebDAV base URL
|
||||
*/
|
||||
WEBDAV_BASE_URL: '/fs/',
|
||||
|
||||
|
||||
/**
|
||||
* PROPFIND depth for file tree loading
|
||||
*/
|
||||
PROPFIND_DEPTH: 'infinity',
|
||||
|
||||
|
||||
// ===== DRAG AND DROP CONFIGURATION =====
|
||||
|
||||
|
||||
/**
|
||||
* Drag preview opacity
|
||||
*/
|
||||
DRAG_PREVIEW_OPACITY: 0.8,
|
||||
|
||||
|
||||
/**
|
||||
* Dragging item opacity
|
||||
*/
|
||||
DRAGGING_OPACITY: 0.4,
|
||||
|
||||
|
||||
/**
|
||||
* Drag preview offset X in pixels
|
||||
*/
|
||||
DRAG_PREVIEW_OFFSET_X: 10,
|
||||
|
||||
|
||||
/**
|
||||
* Drag preview offset Y in pixels
|
||||
*/
|
||||
DRAG_PREVIEW_OFFSET_Y: 10,
|
||||
|
||||
|
||||
// ===== NOTIFICATION TYPES =====
|
||||
|
||||
|
||||
/**
|
||||
* Bootstrap notification type mappings
|
||||
*/
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
*/
|
||||
|
||||
class FileTree {
|
||||
constructor(containerId, webdavClient) {
|
||||
constructor(containerId, webdavClient, isEditMode = false) {
|
||||
this.container = document.getElementById(containerId);
|
||||
this.webdavClient = webdavClient;
|
||||
this.tree = [];
|
||||
this.selectedPath = null;
|
||||
this.onFileSelect = null;
|
||||
this.onFolderSelect = null;
|
||||
this.filterImagesInViewMode = !isEditMode; // Track if we should filter images (true in view mode)
|
||||
|
||||
// Drag and drop state
|
||||
this.draggedNode = null;
|
||||
@@ -426,6 +427,19 @@ class FileTree {
|
||||
|
||||
renderNodes(nodes, parentElement, level) {
|
||||
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');
|
||||
nodeWrapper.className = 'tree-node-wrapper';
|
||||
|
||||
|
||||
114
static/js/sidebar-toggle.js
Normal file
114
static/js/sidebar-toggle.js
Normal 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;
|
||||
|
||||
@@ -103,6 +103,31 @@ const PathUtils = {
|
||||
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
|
||||
* @param {string} path - The file path
|
||||
|
||||
Reference in New Issue
Block a user