使用phuocng/html-dom实现表格列宽可调整功能

使用phuocng/html-dom实现表格列宽可调整功能

【免费下载链接】html-dom Common tasks of managing HTML DOM with vanilla JavaScript. Give me 1 ⭐if it’s useful. 【免费下载链接】html-dom 项目地址: https://gitcode.com/gh_mirrors/ht/html-dom

还在为表格列宽固定而烦恼?数据展示不完整、用户体验差的问题一直困扰着前端开发者。本文将带你深入解析如何使用纯JavaScript实现专业级的表格列宽调整功能,无需任何外部库依赖!

读完本文你将掌握

  • ✅ 原生DOM操作实现列宽调整的核心原理
  • ✅ 完整的鼠标事件处理机制
  • ✅ 用户体验优化技巧
  • ✅ 实际项目中的最佳实践
  • ✅ 性能优化和兼容性考虑

表格列宽调整的核心需求

在企业级应用中,数据表格是最常见的UI组件之一。传统固定列宽的表格存在诸多痛点:

痛点场景用户影响解决方案
长文本截断数据展示不完整允许用户调整列宽
多列数据对比需要同时查看多个字段灵活调整各列宽度
响应式布局不同设备显示效果差用户自定义列宽适配

技术实现原理

架构设计

mermaid

核心代码实现

1. HTML结构准备
<table id="resizableTable" class="data-table">
    <thead>
        <tr>
            <th data-column="id">ID</th>
            <th data-column="name">姓名</th>
            <th data-column="email">邮箱</th>
            <th data-column="role">角色</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>001</td>
            <td>张三</td>
            <td>zhangsan@example.com</td>
            <td>管理员</td>
        </tr>
        <!-- 更多数据行 -->
    </tbody>
</table>
2. CSS样式定义
.data-table {
    width: 100%;
    border-collapse: collapse;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.data-table th,
.data-table td {
    padding: 12px;
    border: 1px solid #e1e5e9;
    text-align: left;
}

.data-table th {
    position: relative;
    background-color: #f8f9fa;
    font-weight: 600;
    color: #495057;
}

/* 列宽调整器样式 */
.column-resizer {
    position: absolute;
    top: 0;
    right: 0;
    width: 8px;
    height: 100%;
    cursor: col-resize;
    background-color: transparent;
    transition: background-color 0.2s ease;
}

.column-resizer:hover {
    background-color: #007bff;
}

.column-resizer.resizing {
    background-color: #0056b3;
}

/* 调整时的视觉反馈 */
.resizing-column {
    background-color: rgba(0, 123, 255, 0.1) !important;
}
3. JavaScript核心逻辑
class TableColumnResizer {
    constructor(tableId) {
        this.table = document.getElementById(tableId);
        this.isResizing = false;
        this.currentColumn = null;
        this.initialX = 0;
        this.initialWidth = 0;
        
        this.init();
    }

    init() {
        if (!this.table) {
            console.error('Table not found');
            return;
        }

        const headers = this.table.querySelectorAll('th');
        headers.forEach(header => {
            this.createResizer(header);
        });
    }

    createResizer(column) {
        const resizer = document.createElement('div');
        resizer.className = 'column-resizer';
        resizer.setAttribute('aria-label', '调整列宽');
        
        // 事件绑定
        resizer.addEventListener('mousedown', this.handleMouseDown.bind(this, column));
        resizer.addEventListener('dblclick', this.handleDoubleClick.bind(this, column));
        
        column.appendChild(resizer);
        column.style.position = 'relative';
    }

    handleMouseDown(column, event) {
        event.preventDefault();
        event.stopPropagation();

        this.isResizing = true;
        this.currentColumn = column;
        this.initialX = event.clientX;
        this.initialWidth = column.offsetWidth;

        // 添加视觉反馈
        column.classList.add('resizing-column');
        event.target.classList.add('resizing');

        // 绑定文档级事件
        document.addEventListener('mousemove', this.handleMouseMove);
        document.addEventListener('mouseup', this.handleMouseUp);
        
        // 防止文本选择
        document.body.style.userSelect = 'none';
        document.body.style.cursor = 'col-resize';
    }

    handleMouseMove = (event) => {
        if (!this.isResizing) return;

        const deltaX = event.clientX - this.initialX;
        const newWidth = Math.max(50, this.initialWidth + deltaX); // 最小宽度50px

        this.currentColumn.style.width = `${newWidth}px`;
        
        // 同步更新同一列的所有单元格
        const columnIndex = Array.from(this.currentColumn.parentNode.children).indexOf(this.currentColumn);
        const rows = this.table.querySelectorAll('tr');
        
        rows.forEach(row => {
            const cell = row.children[columnIndex];
            if (cell) {
                cell.style.width = `${newWidth}px`;
            }
        });
    }

    handleMouseUp = () => {
        if (!this.isResizing) return;

        this.isResizing = false;
        
        // 清理样式
        if (this.currentColumn) {
            this.currentColumn.classList.remove('resizing-column');
            const resizer = this.currentColumn.querySelector('.column-resizer');
            if (resizer) {
                resizer.classList.remove('resizing');
            }
        }

        // 移除事件监听
        document.removeEventListener('mousemove', this.handleMouseMove);
        document.removeEventListener('mouseup', this.handleMouseUp);
        
        // 恢复默认样式
        document.body.style.userSelect = '';
        document.body.style.cursor = '';

        // 触发自定义事件
        this.dispatchResizeEvent();
    }

    handleDoubleClick(column, event) {
        event.preventDefault();
        event.stopPropagation();
        
        // 双击自动调整到合适宽度
        const maxContentWidth = this.getMaxContentWidth(column);
        const newWidth = Math.max(50, maxContentWidth + 20); // 增加20px边距
        
        column.style.width = `${newWidth}px`;
        
        const columnIndex = Array.from(column.parentNode.children).indexOf(column);
        const rows = this.table.querySelectorAll('tr');
        
        rows.forEach(row => {
            const cell = row.children[columnIndex];
            if (cell) {
                cell.style.width = `${newWidth}px`;
            }
        });
        
        this.dispatchResizeEvent();
    }

    getMaxContentWidth(header) {
        const columnIndex = Array.from(header.parentNode.children).indexOf(header);
        const rows = this.table.querySelectorAll('tr');
        let maxWidth = 0;

        // 创建临时元素测量文本宽度
        const measurer = document.createElement('span');
        measurer.style.position = 'absolute';
        measurer.style.visibility = 'hidden';
        measurer.style.whiteSpace = 'nowrap';
        measurer.style.font = window.getComputedStyle(header).font;
        
        document.body.appendChild(measurer);

        rows.forEach(row => {
            const cell = row.children[columnIndex];
            if (cell) {
                measurer.textContent = cell.textContent.trim();
                maxWidth = Math.max(maxWidth, measurer.offsetWidth);
            }
        });

        document.body.removeChild(measurer);
        return maxWidth;
    }

    dispatchResizeEvent() {
        const event = new CustomEvent('columnResized', {
            detail: {
                table: this.table,
                timestamp: Date.now()
            }
        });
        this.table.dispatchEvent(event);
    }

    destroy() {
        const resizers = this.table.querySelectorAll('.column-resizer');
        resizers.forEach(resizer => {
            resizer.removeEventListener('mousedown', this.handleMouseDown);
            resizer.removeEventListener('dblclick', this.handleDoubleClick);
            resizer.remove();
        });
        
        document.removeEventListener('mousemove', this.handleMouseMove);
        document.removeEventListener('mouseup', this.handleMouseUp);
    }
}

完整使用示例

基础用法

// 初始化表格列宽调整功能
const tableResizer = new TableColumnResizer('resizableTable');

// 监听列宽调整事件
document.getElementById('resizableTable').addEventListener('columnResized', (event) => {
    console.log('列宽已调整', event.detail);
});

// 销毁实例(如果需要)
// tableResizer.destroy();

高级配置选项

class AdvancedTableResizer extends TableColumnResizer {
    constructor(tableId, options = {}) {
        super(tableId);
        this.options = {
            minWidth: options.minWidth || 50,
            maxWidth: options.maxWidth || 500,
            resizeHandleWidth: options.resizeHandleWidth || 8,
            enableDoubleClick: options.enableDoubleClick !== false,
            persistWidths: options.persistWidths || false,
            ...options
        };
        
        this.applyOptions();
    }
    
    applyOptions() {
        // 应用配置选项
        const resizers = this.table.querySelectorAll('.column-resizer');
        resizers.forEach(resizer => {
            resizer.style.width = `${this.options.resizeHandleWidth}px`;
        });
    }
    
    handleMouseMove = (event) => {
        if (!this.isResizing) return;

        const deltaX = event.clientX - this.initialX;
        let newWidth = this.initialWidth + deltaX;
        
        // 应用宽度限制
        newWidth = Math.max(this.options.minWidth, newWidth);
        newWidth = Math.min(this.options.maxWidth, newWidth);
        
        this.currentColumn.style.width = `${newWidth}px`;
        
        // 同步更新同一列的所有单元格
        const columnIndex = Array.from(this.currentColumn.parentNode.children).indexOf(this.currentColumn);
        const rows = this.table.querySelectorAll('tr');
        
        rows.forEach(row => {
            const cell = row.children[columnIndex];
            if (cell) {
                cell.style.width = `${newWidth}px`;
            }
        });
        
        // 持久化存储
        if (this.options.persistWidths) {
            this.saveColumnWidths();
        }
    }
    
    saveColumnWidths() {
        const widths = {};
        const headers = this.table.querySelectorAll('th');
        
        headers.forEach((header, index) => {
            const columnName = header.dataset.column || `column-${index}`;
            widths[columnName] = header.offsetWidth;
        });
        
        localStorage.setItem(`tableWidths-${this.table.id}`, JSON.stringify(widths));
    }
    
    loadColumnWidths() {
        if (!this.options.persistWidths) return;
        
        const savedWidths = localStorage.getItem(`tableWidths-${this.table.id}`);
        if (savedWidths) {
            const widths = JSON.parse(savedWidths);
            const headers = this.table.querySelectorAll('th');
            
            headers.forEach((header, index) => {
                const columnName = header.dataset.column || `column-${index}`;
                if (widths[columnName]) {
                    header.style.width = `${widths[columnName]}px`;
                    
                    // 同步到数据单元格
                    const rows = this.table.querySelectorAll('tr');
                    rows.forEach(row => {
                        const cell = row.children[index];
                        if (cell) {
                            cell.style.width = `${widths[columnName]}px`;
                        }
                    });
                }
            });
        }
    }
}

性能优化建议

1. 事件委托优化

// 使用事件委托减少事件监听器数量
this.table.addEventListener('mousedown', (event) => {
    if (event.target.classList.contains('column-resizer')) {
        const column = event.target.parentElement;
        this.handleMouseDown(column, event);
    }
});

2. 防抖处理

// 添加防抖机制避免频繁重绘
this.debouncedResize = debounce(() => {
    this.dispatchResizeEvent();
}, 100);

function debounce(func, wait) {
    let timeout;
    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };
        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

3. 内存管理

// 使用WeakMap避免内存泄漏
const resizerInstances = new WeakMap();

function getResizerInstance(table) {
    if (!resizerInstances.has(table)) {
        resizerInstances.set(table, new TableColumnResizer(table.id));
    }
    return resizerInstances.get(table);
}

浏览器兼容性

浏览器支持情况备注
Chrome✅ 完全支持推荐版本60+
Firefox✅ 完全支持推荐版本55+
Safari✅ 完全支持推荐版本11+
Edge✅ 完全支持推荐版本79+
IE 11⚠️ 部分支持需要polyfill

常见问题解答

Q: 如何处理表格中的大量数据?

A: 建议使用虚拟滚动技术,只渲染可视区域内的单元格,结合列宽调整时只更新可见单元格。

Q: 列宽调整会影响表格响应式布局吗?

A: 不会。我们的实现保持CSS的流动性,只是在固定布局表格上提供额外的调整能力。

Q: 如何保存用户调整后的列宽?

A: 可以使用localStorage或服务器端存储,在表格初始化时恢复用户偏好设置。

Q: 支持触摸设备吗?

A: 需要额外添加touch事件支持,原理与鼠标事件类似。

总结

通过phuocng/html-dom提供的纯JavaScript方案,我们实现了专业级的表格列宽调整功能。这种方案具有以下优势:

  1. 零依赖:不依赖任何外部库,减少项目体积
  2. 高性能:优化的DOM操作和事件处理
  3. 可定制:灵活的配置选项和扩展接口
  4. 良好体验:视觉反馈和双击自动调整
  5. 易于集成:简单的API设计和事件机制

现在你已经掌握了实现表格列宽调整的完整技术栈,可以在实际项目中应用这些技术来提升数据表格的用户体验!

【免费下载链接】html-dom Common tasks of managing HTML DOM with vanilla JavaScript. Give me 1 ⭐if it’s useful. 【免费下载链接】html-dom 项目地址: https://gitcode.com/gh_mirrors/ht/html-dom

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值