mahmud-js-tools
Version:
一个简单而实用的JavaScript工具库,包含多种常用的工具函数。
477 lines (419 loc) • 14.4 kB
JavaScript
(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 });
}));