UNPKG

mahmud-js-tools

Version:

一个简单而实用的JavaScript工具库,包含多种常用的工具函数。

477 lines (419 loc) 14.4 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JsBooster = {})); })(this, (function (exports) { 'use strict'; /** * js-booster - High-performance frontend library * VirtualScroll - Virtual scrolling implementation * @version 1.1.1 * @license MIT */ class VirtualScroll { /** * Create a virtual scroll instance * @param {Object} options Configuration options * @param {HTMLElement} options.container Scroll container element * @param {Array} options.items Data items to display * @param {number} [options.itemHeight=20] Height of each list item (pixels) * @param {number} [options.bufferSize=10] Number of buffer items outside the visible area * @param {Function} [options.renderItem] Custom item rendering function * @param {Function} [options.renderHeader] Custom header rendering function * @param {number} [options.maxHeight=26840000] Maximum height in pixels for the content wrapper */ constructor(options) { this.container = options.container; this.items = options.items || []; this.itemHeight = options.itemHeight || 20; this.bufferSize = options.bufferSize || 10; this.customRenderItem = options.renderItem; this.customRenderHeader = options.renderHeader; this.maxHeight = options.maxHeight || 26840000; // Add maximum height limit to prevent DOM height overflow this.visibleStartIndex = 0; this.visibleEndIndex = 0; this.scrollContainer = null; this.contentWrapper = null; this.contentContainer = null; this.totalHeight = this.items.length * this.itemHeight; this.heightScale = 1; // Height scaling factor // If total height exceeds maximum height, calculate scaling factor if (this.totalHeight > this.maxHeight) { this.heightScale = this.maxHeight / this.totalHeight; } this.initialize(); } /** * Initialize virtual scroll component * @private */ initialize() { // Clear container this.container.innerHTML = ''; // Create scroll container this.scrollContainer = document.createElement('div'); // Add inline styles Object.assign(this.scrollContainer.style, { flex: '1', overflow: 'auto', position: 'relative', minHeight: '0', height: '100%', boxSizing: 'border-box' }); // If there's a custom header render function, render the header if (this.customRenderHeader) { const header = this.customRenderHeader(); if (header) { this.scrollContainer.appendChild(header); } } // Create content wrapper this.contentWrapper = document.createElement('div'); // Add inline styles Object.assign(this.contentWrapper.style, { position: 'relative', width: '100%' }); // Use scaled height to ensure it doesn't exceed browser limits const scaledHeight = this.totalHeight * this.heightScale; this.contentWrapper.style.height = `${scaledHeight}px`; // Create content container this.contentContainer = document.createElement('div'); // Add inline styles Object.assign(this.contentContainer.style, { position: 'absolute', width: '100%', left: '0' }); // Add scroll event listener this.scrollContainer.addEventListener('scroll', this.handleScroll.bind(this)); // Assemble DOM this.contentWrapper.appendChild(this.contentContainer); this.scrollContainer.appendChild(this.contentWrapper); this.container.appendChild(this.scrollContainer); // Render initial visible items this.renderVisibleItems(0, Math.min(100, this.items.length)); } /** * Handle scroll event * @private */ handleScroll() { const scrollTop = this.scrollContainer.scrollTop; const containerHeight = this.scrollContainer.clientHeight; // Consider scaling factor in calculations const realScrollTop = scrollTop / this.heightScale; // Calculate visible range const startIndex = Math.max(0, Math.floor(realScrollTop / this.itemHeight) - this.bufferSize); const endIndex = Math.min( this.items.length, Math.ceil((realScrollTop + containerHeight / this.heightScale) / this.itemHeight) + this.bufferSize ); // Only update when visible range changes if (startIndex !== this.visibleStartIndex || endIndex !== this.visibleEndIndex) { this.renderVisibleItems(startIndex, endIndex); this.visibleStartIndex = startIndex; this.visibleEndIndex = endIndex; } } /** * Render visible items * @param {number} startIndex Start index * @param {number} endIndex End index * @private */ renderVisibleItems(startIndex, endIndex) { // Clear content container this.contentContainer.innerHTML = ''; // Set position considering scaling factor this.contentContainer.style.transform = `translateY(${startIndex * this.itemHeight * this.heightScale}px)`; // Render visible items for (let i = startIndex; i < endIndex; i++) { const item = this.items[i]; if (this.customRenderItem) { // Use custom render function const itemElement = this.customRenderItem(item, i); if (itemElement) { // Only set necessary height styles, other styles are determined by the caller itemElement.style.height = `${this.itemHeight * this.heightScale}px`; itemElement.style.boxSizing = 'border-box'; itemElement.style.width = '100%'; this.contentContainer.appendChild(itemElement); } } else { // Use default rendering - very simple default implementation const row = document.createElement('div'); Object.assign(row.style, { height: `${this.itemHeight * this.heightScale}px`, width: '100%', boxSizing: 'border-box', padding: '8px', borderBottom: '1px solid #eee' }); row.textContent = JSON.stringify(item); this.contentContainer.appendChild(row); } } } /** * Update data items and re-render * @param {Array} items New data items array * @public */ updateItems(items) { this.items = items || []; this.totalHeight = this.items.length * this.itemHeight; // Recalculate scaling factor this.heightScale = 1; if (this.totalHeight > this.maxHeight) { this.heightScale = this.maxHeight / this.totalHeight; } // Ensure height is set correctly if (this.contentWrapper) { this.contentWrapper.style.height = `${this.totalHeight * this.heightScale}px`; } this.visibleStartIndex = 0; this.visibleEndIndex = 0; // Force recalculation of visible items this.handleScroll(); } /** * Scroll to specified index * @param {number} index Index of the item to scroll to * @public */ scrollToIndex(index) { if (index >= 0 && index < this.items.length) { // Apply scaling factor when scrolling this.scrollContainer.scrollTop = index * this.itemHeight * this.heightScale; } } /** * Destroy component, remove event listeners, etc. * @public */ destroy() { if (this.scrollContainer) { this.scrollContainer.removeEventListener('scroll', this.handleScroll); } if (this.container) { this.container.innerHTML = ''; } this.items = null; this.container = null; this.scrollContainer = null; this.contentWrapper = null; this.contentContainer = null; } /** * Refresh virtual scroll, re-render current visible items * @public */ refresh() { this.handleScroll(); } /** * Get scroll container element * @returns {HTMLElement} Scroll container element * @public */ getScrollContainer() { return this.scrollContainer; } } /** * js-booster - Utility functions * @version 1.1.1 * @license MIT */ /** * Deep clone an object * @param {Object} obj Object to clone * @returns {Object} Deep cloned object */ function deepClone(obj) { if (obj === null || typeof obj !== 'object') return obj; if (obj instanceof Date) return new Date(obj); if (obj instanceof RegExp) return new RegExp(obj); const clone = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { clone[key] = deepClone(obj[key]); } } return clone; } /** * Clone an array * @param {Array} arr Array to clone * @returns {Array} Cloned array */ function cloneArray(arr) { return arr.slice(); } /** * Debounce function * @param {Function} func Function to debounce * @param {number} delay Delay in milliseconds * @returns {Function} Debounced function */ function debounce(func, delay) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; } /** * Throttle function * @param {Function} func Function to throttle * @param {number} limit Time limit in milliseconds * @returns {Function} Throttled function */ function throttle(func, limit) { let lastFunc; let lastRan; return function(...args) { if (!lastRan) { func.apply(this, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(() => { if (Date.now() - lastRan >= limit) { func.apply(this, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; } /** * Deep equality check * @param {*} a First value * @param {*} b Second value * @returns {boolean} True if deeply equal */ function isEqual(a, b) { if (a === b) return true; if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) return false; const keysA = Object.keys(a); const keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; for (const key of keysA) { if (!keysB.includes(key) || !isEqual(a[key], b[key])) return false; } return true; } /** * Generate random number in range * @param {number} min Minimum value * @param {number} max Maximum value * @returns {number} Random number */ function random(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } /** * Format timestamp to readable string * @param {Date|number|string} time Input time * @param {string} format Format string (default: 'YYYY-MM-DD HH:mm:ss') * @returns {string} Formatted time string */ function formatTime(time, format = 'YYYY-MM-DD HH:mm:ss') { const date = new Date(time); const map = { 'YYYY': date.getFullYear(), 'MM': String(date.getMonth() + 1).padStart(2, '0'), 'DD': String(date.getDate()).padStart(2, '0'), 'HH': String(date.getHours()).padStart(2, '0'), 'mm': String(date.getMinutes()).padStart(2, '0'), 'ss': String(date.getSeconds()).padStart(2, '0'), }; return format.replace(/YYYY|MM|DD|HH|mm|ss/g, matched => map[matched]); } /** * Check if string is a valid URL * @param {string} str String to check * @returns {boolean} True if valid URL */ function isUrl(str) { try { new URL(str); return true; } catch { return false; } } /** * Remove duplicates from array * @param {Array} arr Input array * @returns {Array} Array with unique values */ function unique(arr) { return [...new Set(arr)]; } /** * Capitalize first letter of string * @param {string} str Input string * @returns {string} Capitalized string */ function capitalize(str) { return str.charAt(0).toUpperCase() + str.slice(1); } /** * Convert string to camelCase * @param {string} str Input string * @returns {string} camelCase string */ function camelCase(str) { return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : ''); } /** * Shuffle array (Fisher-Yates algorithm) * @param {Array} arr Array to shuffle * @returns {Array} Shuffled array */ function shuffle(arr) { const array = [...arr]; for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } /** * Sleep for specified time * @param {number} ms Time in milliseconds * @returns {Promise} Promise that resolves after delay */ function sleep(ms) { return new Promise(resolve => setTimeout(resolve, ms)); } var utils = /*#__PURE__*/Object.freeze({ __proto__: null, deepClone: deepClone, cloneArray: cloneArray, debounce: debounce, isEqual: isEqual, throttle: throttle, random: random, formatTime: formatTime, isUrl: isUrl, unique: unique, capitalize: capitalize, camelCase: camelCase, shuffle: shuffle, sleep: sleep }); // If in browser environment, add to global object if (typeof window !== 'undefined') { window.JsBooster = { VirtualScroll, ...utils }; } exports.VirtualScroll = VirtualScroll; exports.utils = utils; Object.defineProperty(exports, '__esModule', { value: true }); }));