网页版仿Excel效果组件--handsontable拓展运用
引言(祝看官们新年万事大吉)
前段时间项目需要实现网页版的excel表格功能,瞬间就想到了handsontable,为什么呢?理由如下:该UI组件功能齐全多样,展示效果也更贴近bootstrap风格,兼容所有现代浏览器和IE9+,然后开源,api相当给力。
唯一美中不足的是没有中文版的api,也有些分享中文api的文章,也不完整(有就不错了,不满意自己去看官网api啊,啦啦啦~),闲言少叙,进入正文:
功能需求
我们先看一下功能操作栏(红框部分为部分我们需要实现的功能):
上图中的 input 会同步响应我们所选单元格的数据,其他的方法如图所示。
首先我们既然是表格,那么我们的操作对象基本都是单元格,但同时我们肯定也希望能够满足范围性操作,比如说批量修改样式。当然我们不仅需要考虑到展示效果,还需要考虑到储存和数据渲染,
我们需要的做到以下几点:所选范围、所选范围里的单元格、如何储存数据、如何通过loadData一次性加载数据(包括样式及自定义属性),带着这些问题我们去看api,我们需要找到我们需要方法以及了解如何使用它们。
这里附上官方api链接:https://docs.handsontable.com/pro/1.16.0/tutorial-introduction.html
阅读过程小生这里就不做赘述了(唔...一把辛酸泪),首先我们需要清楚的是,我们需要获取到被选择的单元格对象,而要获取单元格对象我们要知道所选范围(单个单元格也是一个范围,这里不需要另作判断),而要知道所选范围我们要用到 getSelected() 方法,而此方法应该在我们选择范围后触发,所以我们需要用到钩子函数:afterOnCellMouseDown,具体用法可以到api里自行查阅
根据我们的需求,结合api得到如下图:
万事俱备,接下来该做什么呢?当然是做我们最爱做的事:撸代码!~
实现功能
我们这里用本地数据做展示:
var data = [ [, , , , , , , , , , , , , , , , , , , , ], ["2001", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2002", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2003", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2004", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2005", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2006", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2007", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2008", 10, 11, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,87,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2009", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,8,6,78,5,6,6,35,6,3,6,3,8,38,3], ["2010", 30, 15, 12, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,52,6,3,5,3,8,6,8,5,6,56,355,6,3,66,23,8,38,3], ["2011", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,26,3,5,26,3,5,3,8,6,8,5,56,6,35,6,3,6,23,8,38,3], ["2012", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,666,5,8,6,3,9,6,3,5,56,3,5,3,78,6,58,55,6,6,35,6,23,6,3,8,38,3], ["2013", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,68,6,3,9,6,3,5,26,3,5,3,8,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2014", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,26,3,5,3,8,6,58,5,6,6,35,6,3,6,3,8,38,3], ["2015", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,29,26,3,5,6,3,5,3,78,76,8,5,6,6,35,6,3,6,3,8,38,3], ["2016", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,55,6,3,5,3,8,6,28,5,6,6,35,6,3,6,3,8,38,3], ["2017", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,43,9,6,3,5,6,3,5,3,8,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2018", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,62,3,29,6,3,5,6,3,5,3,8,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2019", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,48,6,3,9,6,3,5,6,3,5,3,8,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2020", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,68,6,3,9,6,3,5,6,3,5,3,8,6,8,5,6,6,345,6,3,64,3,8,38,3], ["2021", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,6,3,5,6,3,5,3,8,6,8,5,6,6,35,6,3,6,3,8,38,3], ["2022", 20, 11, 14, 13,14,15,16,17,18,19,20,21,22,22,3,3,55,66,5,8,6,3,9,66,3,5,6,3,5,3,8,6,8,5,6,6,35,6,3,6,3,48,38,23] ];
前端界面如图:
汉化右键菜单:如果不需要右键菜单可以设置为false,contextMenu : false
contextMenu: { items: { 'mergeCells':{ name: '合并单元格' , }, 'row_above': { name: '上方添加一行', }, 'row_below': { name: '下方添加一行', }, 'col_left': { name: '左侧添加一列', }, 'col_right': { name: '右侧添加一列', }, 'remove_row': { name: '移除此行', }, 'remove_col': { name: '移除此列', }, 'copy': { name: '复制', }, 'cut': { name: '剪切', }, 'make_read_only': { name: '禁止编辑选中项', }, 'alignment': { }, 'undo': { name: '还原上次操作', }, 'redo': { name: '重复上次动作', }, 'setAlias':{ name:'设置别名', callback:function(){ if( $(Ccell) != undefined ){ addAliasDialog(); }else{ alert("请先选择单元格..."); } } } } }
看效果图:
自定义菜单及回调函数:自定义菜单也是在 contextMenu里面,格式如下(如上图中的 setAlias 设置别名,设置属性方法我们在后面再说明):
contextMenu: { items: { '参数名':{ name:'菜单名', callback:function(){ // doSomething } } } }
列出全局变量:
// 列出全局变量 var Crow,Ccol,Ccell,valT,selectRange,selectRangeArr = [];
Crow:所选单元格的行,Ccol:所选单元格的列,valT:所选单元格的值,selectRange:所选范围,selectRangeArr:所选单元格数组
获取所选区域单元格数组,当前单元格高亮:这里需要补充的一点是,handsontable本身在表格失去焦点时会移除所有当前高亮类,而我们在点击按钮修改样式时又需要所选高亮来满足我们的“心中有数”,所以这里我们需要自定义一个高亮的类,看代码:
// 获取所选区域单元格数组 当前高亮 hot.addHook('afterOnCellMouseDown', function (event, cellCoords) { Crow = cellCoords.row, Ccol = cellCoords.col; selectRangeArr = []; // 所选区域所有单元格数组 Ccell = hot.getCell(Crow, Ccol) selectRange = hot.getSelected(); // 获取所选区域范围 console.log(selectRange); var txt = hot.getDataAtCell(selectRange[0],selectRange[1]); // 获取所选区域第一个单元格值 // 单击任意单元格取消编辑状态 $(".handsontableInputHolder").css({ "display":"none" }); $("#templateCellInput").val(txt); var rangeRowArr = []; // 所选区域行数组 var rangeColArr = []; // 所选区域列数组 for( var i=selectRange[0];i<selectRange[2]+1;i++ ){ rangeRowArr.push(i); } for( var i=selectRange[1];i<selectRange[3]+1;i++ ){ rangeColArr.push(i); } for( var i=0;i<rangeRowArr.length;i++ ){ for( var n=0;n<rangeColArr.length;n++ ){ var selectRangeCell = { row:rangeRowArr[i],col:rangeColArr[n] }; selectRangeArr.push(selectRangeCell); } } // 添加表格失去焦点时的当前单元格类 $("td").removeClass("currentTd"); for( var i=0;i<selectRangeArr.length;i++ ){ var rangeCell = hot.getCell(selectRangeArr[i].row, selectRangeArr[i].col); $(rangeCell).addClass("currentTd"); } });
我们可以在控制台查看一下令我们心动的 selectRange 和 selectRangeArr :
所选单元格的值和input同步:templateCellInput 为 input的id
$("#templateCellInput").keyup(function(){ var val = $(this).val(); if(selectRangeArr.length>0){ for( var i=0;i<selectRangeArr.length;i++ ){ hot.setDataAtCell(selectRangeArr[i].row, selectRangeArr[i].col,val) } } });
修改单元格样式(字体样式和对齐方式):switch条件语句在这种事件触发对象判断中用起来是相当地令人愉快..
// 修改单元格样式 $(".btn-group label.btn").click(function(e){ console.log(e.target); var _index = $(this).index(); var styleType = $(this).parent(); var StyleClassName = ''; // 修改单元格文本样式 var toggleSwitch = true; if( styleType.hasClass("fontStyle") ){ var fontClass = ""; switch(_index){ case 0 : fontClass = "htBold"; // 加粗 break; case 1 : fontClass = "htItalic"; // 斜体 break; case 2 : fontClass = "htUnderline"; // 下划线 break; } StyleClassName = fontClass; } // 修改单元格对齐方式 if( styleType.hasClass("alignStyle") ){ var alignClass = ""; switch(_index){ case 0 : alignClass = "htLeft"; // 左对齐 break; case 1 : alignClass = "htCenter"; // 居中对齐 break; case 2 : alignClass = "htRight"; // 右对齐 break; case 3 : alignClass = "htJustify"; // 两端对齐 break; } StyleClassName = alignClass; } // 修改所选区域所有单元格样式并赋予属性 for( var i=0;i<selectRangeArr.length;i++ ){ var rangeCell = hot.getCell(selectRangeArr[i].row, selectRangeArr[i].col); var checkMergeCell = $(rangeCell).attr("rowspan"); $(rangeCell).removeClass("htLeft htCenter htRight htJustify"); // 定义修改类名 创建对应属性方法 var setRangeCellClass = function(){ $(rangeCell).toggleClass(StyleClassName); var cellClass = $(rangeCell)[0].className; hot.setCellMeta(selectRangeArr[i].row, selectRangeArr[i].col,"cellClass",cellClass); } if( checkMergeCell != undefined ){ if( toggleSwitch ){ setRangeCellClass(); toggleSwitch = false; }else{ continue; } }else{ setRangeCellClass(); } } });
这里需要注意的两点是:
1、如果对象是合并单元格,那么在赋值和修改样式上需要区别对待,看代码(具体区别请调试代码自己体会o(∩_∩)o );
2、在方法里我们可以看到运用了 setCellMeta() 方法,单纯的前端效果我们不需要用到此方法,这里是为了便于储存和渲染数据,如此在初始化表格渲染数据的时候我们能将每一个单元格所对应的样式类名也添加进去,简而言之:每一次初始化表格我们只需要渲染一次,开心~
自定义背景色、字体颜色、边框色:这里我们用到了插件 bootstrap-colorpicker.js :
$(".ColorStyle input").each(function(){ $(this).colorpicker(); }) $(".ColorStyle input").blur(function(){ var val = $(this).val(); var _index = $(this).parent().index(); $(this).css("cssText","background:"+val+"!important;color:"+val+"!important;"); // 定义改变样式方法 var changeCellStyle = function(){ if( _index == 0 ){ $(rangeCell).css({"background":val}); hot.setCellMeta(selectRangeArr[i].row, selectRangeArr[i].col,"bkColor",val); } if( _index == 1 ){ $(rangeCell).css({"color":val}); hot.setCellMeta(selectRangeArr[i].row, selectRangeArr[i].col,"ftColor",val); } if( _index == 2 ){ $(rangeCell).css({"border":"solid 1px "+val}); hot.setCellMeta(selectRangeArr[i].row, selectRangeArr[i].col,"bdColor",val); } }; for( var i=0;i<selectRangeArr.length;i++ ){ var rangeCell = hot.getCell(selectRangeArr[i].row, selectRangeArr[i].col); var checkMergeCell = $(rangeCell).attr("rowspan"); if( checkMergeCell != undefined ){ if( toggleSwitch ){ changeCellStyle(); toggleSwitch = false; }else{ continue; } }else{ changeCellStyle(); } } });
看效果图:
自定义属性:假设我们现在需要给某些单元格添加任意属性,这里我们以设置别名为例,并且希望能在右键菜单中可以直接操作:
'setAlias':{ name:'设置别名', callback:function(){ if( $(Ccell) != undefined ){ addAliasDialog(); }else{ alert("请先选择单元格..."); } } }
对的,此处应该有弹窗,这里小生强烈推荐用 layer.js,快准狠还高大上...这里附上 layer.js的官网地址,具体用法有空亲们可以自行琢磨:http://www.layui.com/doc/modules/layer.html,下面看设置别名回调函数:
function addAliasDialog(){ var html = '<div class="alias" style="text-align:center;margin-top:20px;"><label>请输入别名:<input type="text" id="aliasVal" /></label></div>'; layer.open({ type: 1, btn: ['确认', '取消'], shadeClose:true, title:"设置别名", area: ['420px', '240px'], //宽高 content: html, yes:function(index,layero){ var val = $("#aliasVal").val(); var cellMeta = hot.getCellMeta(Crow, Ccol); if( val != "" ){ hot.setCellMeta(Crow, Ccol, "alias", val); layer.msg('设置成功', { icon: 1, time: 1000 //1秒关闭(如果不配置,默认是3秒) }, function(){ console.log(cellMeta); layer.close(index); }); }else{ alert("别名不能为空!"); } }, cancel:function(index,layero){ layer.close(index); },btn2: function(index, layero){ layer.confirm('确认取消设置别名吗?', {icon: 3, title:'提示'}, function(index){ layer.close(index); },function(index){ addAliasDialog(); }); } }); }
这里需要注意的一点是,setCellMeta添加的属性是单元格的属性,而不是单元格的DOM属性,所以我们用attr或者prop方法是获取不到的(小生不可能告诉亲们当初经历了什么~),我们可以在设置完成以后将所选单元格的属性打印出来:
看打印结果
:
结语
这次分享到这里就结束了,希望对大家有所帮助,有疑问或者建议都可以留言交流,新年快乐,摸摸踹~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?