Files
markdown_editor/static/js/utils.js
Mahmoud-Emad afcd074913 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
2025-10-26 18:48:31 +03:00

430 lines
13 KiB
JavaScript

/**
* Utilities Module
* Common utility functions used throughout the application
*/
/**
* Path Utilities
* Helper functions for path manipulation
*/
const PathUtils = {
/**
* Get the filename from a path
* @param {string} path - The file path
* @returns {string} The filename
* @example PathUtils.getFileName('folder/subfolder/file.md') // 'file.md'
*/
getFileName(path) {
if (!path) return '';
return path.split('/').pop();
},
/**
* Get the parent directory path
* @param {string} path - The file path
* @returns {string} The parent directory path
* @example PathUtils.getParentPath('folder/subfolder/file.md') // 'folder/subfolder'
*/
getParentPath(path) {
if (!path) return '';
const lastSlash = path.lastIndexOf('/');
return lastSlash === -1 ? '' : path.substring(0, lastSlash);
},
/**
* Normalize a path by removing duplicate slashes
* @param {string} path - The path to normalize
* @returns {string} The normalized path
* @example PathUtils.normalizePath('folder//subfolder///file.md') // 'folder/subfolder/file.md'
*/
normalizePath(path) {
if (!path) return '';
return path.replace(/\/+/g, '/');
},
/**
* Join multiple path segments
* @param {...string} paths - Path segments to join
* @returns {string} The joined path
* @example PathUtils.joinPaths('folder', 'subfolder', 'file.md') // 'folder/subfolder/file.md'
*/
joinPaths(...paths) {
return PathUtils.normalizePath(paths.filter(p => p).join('/'));
},
/**
* Get the file extension
* @param {string} path - The file path
* @returns {string} The file extension (without dot)
* @example PathUtils.getExtension('file.md') // 'md'
*/
getExtension(path) {
if (!path) return '';
const fileName = PathUtils.getFileName(path);
const lastDot = fileName.lastIndexOf('.');
return lastDot === -1 ? '' : fileName.substring(lastDot + 1);
},
/**
* Check if a path is a descendant of another path
* @param {string} path - The path to check
* @param {string} ancestorPath - The potential ancestor path
* @returns {boolean} True if path is a descendant of ancestorPath
* @example PathUtils.isDescendant('folder/subfolder/file.md', 'folder') // true
*/
isDescendant(path, ancestorPath) {
if (!path || !ancestorPath) return false;
return path.startsWith(ancestorPath + '/');
},
/**
* Check if a file is a binary/non-editable file based on extension
* @param {string} path - The file path
* @returns {boolean} True if the file is binary/non-editable
* @example PathUtils.isBinaryFile('image.png') // true
* @example PathUtils.isBinaryFile('document.md') // false
*/
isBinaryFile(path) {
const extension = PathUtils.getExtension(path).toLowerCase();
const binaryExtensions = [
// Images
'png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'svg', 'webp', 'tiff', 'tif',
// Documents
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx',
// Archives
'zip', 'rar', '7z', 'tar', 'gz', 'bz2',
// Executables
'exe', 'dll', 'so', 'dylib', 'app',
// Media
'mp3', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'wav', 'ogg',
// Other binary formats
'bin', 'dat', 'db', 'sqlite'
];
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
* @returns {string} The file type description
* @example PathUtils.getFileType('image.png') // 'Image'
*/
getFileType(path) {
const extension = PathUtils.getExtension(path).toLowerCase();
const imageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'ico', 'svg', 'webp', 'tiff', 'tif'];
const documentExtensions = ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'];
const archiveExtensions = ['zip', 'rar', '7z', 'tar', 'gz', 'bz2'];
const mediaExtensions = ['mp3', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'wav', 'ogg'];
if (imageExtensions.includes(extension)) return 'Image';
if (documentExtensions.includes(extension)) return 'Document';
if (archiveExtensions.includes(extension)) return 'Archive';
if (mediaExtensions.includes(extension)) return 'Media';
if (extension === 'pdf') return 'PDF';
return 'File';
}
};
/**
* DOM Utilities
* Helper functions for DOM manipulation
*/
const DOMUtils = {
/**
* Create an element with optional class and attributes
* @param {string} tag - The HTML tag name
* @param {string} [className] - Optional class name(s)
* @param {Object} [attributes] - Optional attributes object
* @returns {HTMLElement} The created element
*/
createElement(tag, className = '', attributes = {}) {
const element = document.createElement(tag);
if (className) {
element.className = className;
}
Object.entries(attributes).forEach(([key, value]) => {
element.setAttribute(key, value);
});
return element;
},
/**
* Remove all children from an element
* @param {HTMLElement} element - The element to clear
*/
removeAllChildren(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
},
/**
* Toggle a class on an element
* @param {HTMLElement} element - The element
* @param {string} className - The class name
* @param {boolean} [force] - Optional force add/remove
*/
toggleClass(element, className, force) {
if (force !== undefined) {
element.classList.toggle(className, force);
} else {
element.classList.toggle(className);
}
},
/**
* Query selector with error handling
* @param {string} selector - The CSS selector
* @param {HTMLElement} [parent] - Optional parent element
* @returns {HTMLElement|null} The found element or null
*/
querySelector(selector, parent = document) {
try {
return parent.querySelector(selector);
} catch (error) {
Logger.error(`Invalid selector: ${selector}`, error);
return null;
}
},
/**
* Query selector all with error handling
* @param {string} selector - The CSS selector
* @param {HTMLElement} [parent] - Optional parent element
* @returns {NodeList|Array} The found elements or empty array
*/
querySelectorAll(selector, parent = document) {
try {
return parent.querySelectorAll(selector);
} catch (error) {
Logger.error(`Invalid selector: ${selector}`, error);
return [];
}
}
};
/**
* Timing Utilities
* Helper functions for timing and throttling
*/
const TimingUtils = {
/**
* Debounce a function
* @param {Function} func - The function to debounce
* @param {number} wait - The wait time in milliseconds
* @returns {Function} The debounced function
*/
debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
},
/**
* Throttle a function
* @param {Function} func - The function to throttle
* @param {number} wait - The wait time in milliseconds
* @returns {Function} The throttled function
*/
throttle(func, wait) {
let inThrottle;
return function executedFunction(...args) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, wait);
}
};
},
/**
* Delay execution
* @param {number} ms - Milliseconds to delay
* @returns {Promise} Promise that resolves after delay
*/
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
};
/**
* Download Utilities
* Helper functions for file downloads
*/
const DownloadUtils = {
/**
* Trigger a download in the browser
* @param {string|Blob} content - The content to download
* @param {string} filename - The filename for the download
*/
triggerDownload(content, filename) {
const blob = content instanceof Blob ? content : new Blob([content]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
},
/**
* Download content as a blob
* @param {Blob} blob - The blob to download
* @param {string} filename - The filename for the download
*/
downloadAsBlob(blob, filename) {
DownloadUtils.triggerDownload(blob, filename);
}
};
/**
* Validation Utilities
* Helper functions for input validation
*/
const ValidationUtils = {
/**
* Validate and sanitize a filename
* @param {string} name - The filename to validate
* @param {boolean} [isFolder=false] - Whether this is a folder name
* @returns {Object} Validation result with {valid, sanitized, message}
*/
validateFileName(name, isFolder = false) {
const type = isFolder ? 'folder' : 'file';
if (!name || name.trim().length === 0) {
return { valid: false, sanitized: '', message: `${type} name cannot be empty` };
}
// Check for invalid characters using pattern from Config
const validPattern = Config.FILENAME_PATTERN;
if (!validPattern.test(name)) {
const sanitized = ValidationUtils.sanitizeFileName(name);
return {
valid: false,
sanitized,
message: `Invalid characters in ${type} name. Only lowercase letters, numbers, and underscores allowed.\n\nSuggestion: "${sanitized}"`
};
}
return { valid: true, sanitized: name, message: '' };
},
/**
* Sanitize a filename by removing/replacing invalid characters
* @param {string} name - The filename to sanitize
* @returns {string} The sanitized filename
*/
sanitizeFileName(name) {
return name
.toLowerCase()
.replace(Config.FILENAME_INVALID_CHARS, '_')
.replace(/_+/g, '_')
.replace(/^_+|_+$/g, '');
},
/**
* Check if a string is empty or whitespace
* @param {string} str - The string to check
* @returns {boolean} True if empty or whitespace
*/
isEmpty(str) {
return !str || str.trim().length === 0;
},
/**
* Check if a value is a valid email
* @param {string} email - The email to validate
* @returns {boolean} True if valid email
*/
isValidEmail(email) {
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailPattern.test(email);
}
};
/**
* String Utilities
* Helper functions for string manipulation
*/
const StringUtils = {
/**
* Truncate a string to a maximum length
* @param {string} str - The string to truncate
* @param {number} maxLength - Maximum length
* @param {string} [suffix='...'] - Suffix to add if truncated
* @returns {string} The truncated string
*/
truncate(str, maxLength, suffix = '...') {
if (!str || str.length <= maxLength) return str;
return str.substring(0, maxLength - suffix.length) + suffix;
},
/**
* Capitalize the first letter of a string
* @param {string} str - The string to capitalize
* @returns {string} The capitalized string
*/
capitalize(str) {
if (!str) return '';
return str.charAt(0).toUpperCase() + str.slice(1);
},
/**
* Convert a string to kebab-case
* @param {string} str - The string to convert
* @returns {string} The kebab-case string
*/
toKebabCase(str) {
return str
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/[\s_]+/g, '-')
.toLowerCase();
}
};
// Make utilities globally available
window.PathUtils = PathUtils;
window.DOMUtils = DOMUtils;
window.TimingUtils = TimingUtils;
window.DownloadUtils = DownloadUtils;
window.ValidationUtils = ValidationUtils;
window.StringUtils = StringUtils;