簡述
先說一下背景,之所以封裝handsontable插件,是因為公司要實現在線編輯導入excel文件的功能,然后我就找到了這個功能強大的插件handsontable。
具體功能
除了handsontable的功能外,還包括:
1、每一行數據統計錯誤數,重復數
2、每一列標記重復項,錯誤項
3、定位功能,當數據過多出現滾動條時,點擊上一條/下一條按鈕,定位到當前標記項。
4、表頭標注每一列數據的校驗規則。
5、當數據被編輯后,立即重新校驗,並標記重復項、錯誤項
6、配置isValidate,true則本地校驗,false則不校驗
(css樣式有待改進,后面會更新)
2018/3/30日,修復所有bug,
1、根絕插件局部渲染的特性,將每行的第一列的標注完成局部渲染,以及錯誤點定位,也根據局部渲染的特性,先滾動到指定行,再進行標記。
2、另外,修復時間控件在最前/后一列/行的情況下,會被遮擋的問題,修改了源碼,根據當前單元格的位置來計算時間控件展示的位置。
3、修改源碼,將時間控件的英文改為中文。(后面會附上源碼修改部分)
2018/4/3日,修改保存錯誤定位的數據結構,並標記每一個td的row,col位置,保證做到,定位錯誤點100%准確
/* * author Happy Guo * date 2018-03-15 */ 'use strict'; define(["jquery","Handsontable"], function ($,Handsontable) { var HandsontableExtend = function(opt) { this.opt = $.extend(true, {}, opt); this.element = document.querySelector(this.opt.el); this.header = Object.keys(this.opt.dataObj.headMap); this.allErrorNum = [];//記錄每一行的錯誤數 this.table = null; this.errorRow = 0;//統計有幾行是有錯誤的 this.errorPosition = { index:0, row: 0, col: 0 };//記錄當前被標注的錯誤位置 this.firstError = { isExist :false, row:0, col:0 } this.isPosition = false; this.position = null; //如果沒有數據,默認給出一行空行 if(this.opt.dataObj.dataMap.length===0){ var obj = {}; this.header.map(function(item,index){ obj[item] = { value:'', errorType:'' } }); this.opt.dataObj.dataMap.push(obj) } } HandsontableExtend.prototype = { constructor: HandsontableExtend, // extraType: ["CODE_TYPE","Date"], //不需要校驗的類型 typeMap:{'STRING':'文本','INTEGER':'整型','DOUBLE':'小數','DATE':'日期','CODE_TYPE':'枚舉類型','BOOLEAN':'布爾','TIME':'小時分鍾'}, dateFormat:['yyyy-MM-dd','yyyy/MM/dd','yyyy.MM.dd'], timePattern:/^([0-1]{1}\d|2[0-3]):([0-5]\d)$/, /*@method 整合table的列的配置項 */ setColumn: function() { var list = []; for (var i = 0; i < this.header.length; i++) { var obj = { data: this.header[i] + ".value", width: 150, height: 60 }; var typeData = this.opt.dataObj.headMap[this.header[i]]; if (typeData.type === "CODE_TYPE") { obj.type = "dropdown"; obj.source = this.getCodeValueList(typeData.codeValueList); } if (typeData.type === "BOOLEAN") { obj.type = "dropdown"; obj.source = ['是','否']; } if(typeData.type === 'TIME'){ obj.type = "time"; obj.dateFormat = "h:mm"; } if (typeData.type === "DATE") { obj.type = "date"; obj.dateFormat = "YYYY-MM-DD"; obj.datePickerConfig={ firstDay: 1, yearRange:100, showWeekNumber: true, minDate: new Date('1900-01-01') } } list.push(obj); } return list; this.opt.set.columns = list; }, setCell:function(){ var list = []; var that = this; this.opt.dataObj.dataMap.map(function(item,index){ Object.keys(item).forEach(function(key){ if(item[key].originalValue){ list.push({ row:index, col:that.header.indexOf(key), comment:{ value:item[key].originalValue } }) } }) }); return list; }, getCodeValueList:function(list){ var newList = []; if(list instanceof Array){ list.map(function(item,index){ newList.push(item.displayName); }); return newList; }else{ return []; } }, /*@method 初始化table的配置,以及事件監聽 */ init: function() { var that = this; this.opt.set = $.extend(true, { data: that.opt.dataObj.dataMap, comments:true, columns: this.setColumn(), cells: function(row, col, prop) { //單元格渲染 this.renderer = function(instance,td,row,col,prop,value,cellProperties) { Handsontable.renderers.TextRenderer.apply(this, arguments); var obj = that.opt.dataObj.dataMap[row][that.header[col]]; $(td).attr({row:row,col:col}); if (typeof obj["errorType"] !== "undefined") { if (obj["errorType"] === "repeat") { $(td).attr({ repeat: true }); } else if (obj["errorType"] === "error") { $(td).attr({ error: true }); } } else { $(td).css({ border: "1px solid #ccc", color: "#999", "white-space": "normal", "word-break": "break-all" }); } if(obj.originalValue){ $(td).css({ 'background-color':'#F1F9FF' }) } }; }, cell:that.setCell(), stretchH: "all", width: "100%", autoWrapRow: true, autoRowSize: true, autoColumnSize: true, height: "600", maxRows: 1000, manualRowResize: false, manualColumnResize: false, // beforeKeyDown : function(e) { // // 禁止選中列后delete鍵和回退鍵清空整列數據 // if (e.keyCode === 8 || e.keyCode == 46) { // Handsontable.Dom.stopImmediatePropagation(e); // } // }, manualRowMove: true, manualColumnMove: true, contextMenu: true, filters: true, dropdownMenu: true }, this.opt.set ); this.opt.set.rowHeaders = function(index) { var repeatNum = 0; var errorNum = 0; if(that.allErrorNum[index] instanceof Array){ for(var j=0;j<that.allErrorNum[index].length;j++){ if(that.allErrorNum[index][j].type==='error'){ errorNum+=1; }else{ repeatNum+=1; } } } var html = "<span class='error-th' style='display:"+((that.allErrorNum[index]&&that.allErrorNum[index].length>0)?"block":"none")+"'></span>"; html += " <span class='error-content'>重復:" + repeatNum + ",錯誤:" + errorNum + "</span>"; html += "<span id='column_name' style='padding-right:6px;'>" + (index + 1) + "</span>"; return html; }; this.opt.set.colHeaders = function(index) { var desc = that.header[index]+"規則:"; var map = that.opt.dataObj.headMap[that.header[index]]; if(map.minLength&&map.maxLength){ desc=desc+'長度:'+map.minLength+'-'+map.maxLength; }else if(map.length){ desc=desc+'長度:'+map.length } if(map.type){ desc=desc+',類型:'+that.typeMap[map.type] } if(map.required){ desc=desc+',必填' } if(map.unique){ desc=desc+',不能重復' } var html = "<span class='remark-th'></span>"; html += " <span class='remark-content'>"+desc+"</span>"; if(map.required){ html += "<span id='column_name' style='color:#ED5565'>" + that.header[index] + "</span>"; }else{ html += "<span id='column_name'>" + that.header[index] + "</span>"; } return html; }; this.table = new Handsontable(this.element, this.opt.set); this.table.updateSettings({ contextMenu: { callback: function(key, options) { if (key === "about") { setTimeout(function() { // timeout is used to make sure the menu collapsed before alert is shown alert( "This is a context menu with default and custom options mixed" ); }, 100); } }, items: { row_above: { name: "向上插入一行", disabled: function() { return that.table.getSelected()[0] === 0; } }, remove_row: { name: "刪除選中行", disabled: function() { // if first row, disable this option return that.table.getSelected()[0] === 0; } } } } }); window.onresize = this.opt.isValidate ? function() { that.render.call(that); } : null; this.table.addHook("afterChange", function() { that.opt.isValidate && that.render.call(that); }); this.table.addHook("afterRemoveRow", function() { that.opt.isValidate && that.render.call(that); }); var topValue = 0,leftValue = 0; var interval = null; $(this.opt.el + " .wtHolder")[0].onscroll = function() { if (interval == null) { interval = setInterval(isFinishScroll, 1000); } }; function isFinishScroll() { // 判斷此刻到頂部的距離是否和1秒前的距離相等 if ($(that.opt.el + " .wtHolder")[0].scrollLeft === leftValue && $(that.opt.el + " .wtHolder")[0].scrollTop === topValue) { clearInterval(interval); interval = null; that.validate(); that.renderError(); that.remarkShow();//渲染表頭、列頭標注 if(that.position){ $("td[row='"+that.errorPosition.row+"'][col='"+that.errorPosition.col+"']").attr('current'); } } else { topValue = $(that.opt.el + " .wtHolder")[0].scrollTop; leftValue = $(that.opt.el + " .wtHolder")[0].scrollLeft; } } this.render(); return this; }, /*@method 渲染整個table數據 */ render: function() { var table = this.table; this.validate.call(this); //初始化,先驗證,並標記重復項 table.render(); //並渲染在行首部 this.renderError(); this.remarkShow();//渲染表頭、列頭標注 $(".htInvalid").removeClass('htInvalid'); $("td[current]").removeAttr('current'); }, /*@method 渲染出表頭和列頭的標注信息 */ remarkShow:function(){ $(".remark-th").hover(function(){ var top = $(this).closest('th').offset().top; var left = $(this).closest('th').offset().left; $(this).next().css({'top':top,"left":left}) }); $(".error-th").hover(function(){ var top = $(this).closest('th').offset().top; var left = $(this).closest('th').offset().left+10; $(this).next().css({'top':top,"left":left}) }) }, /*@method 渲染出重復、錯誤的單元格 */ renderError: function() { var that = this; var rowHeaderTr = $(".ht_clone_left .htCore").eq(0).find("tbody").find("tr"); var tr = $(this.opt.el + " .htCore").eq(0).find("tbody").find("tr"); //渲染錯誤項(只渲染當前可視區域的), //此處天坑,行頭是單獨的table,之前滾動后渲染位置錯誤。 for (var i = 0; i < tr.length; i++) { var rowNum = $(tr[i]).find("th #column_name").text(); rowNum = parseInt(rowNum) - 1; //統計每一行錯誤項、重復項,如果列數過多,則列會渲染不完全,所以不能用選擇器查出准確數據,只能使用統計出的數據 var repeatNum = 0; var errorNum = 0; if(that.allErrorNum[rowNum] instanceof Array){ for(var j=0;j<this.allErrorNum[rowNum].length;j++){ if(this.allErrorNum[rowNum][j].type==='error'){ errorNum+=1; }else{ repeatNum+=1; } } } var errorTH = $(rowHeaderTr[i]).find("th .error-th").eq(0); if(repeatNum+errorNum>0){ errorTH.css({'display':'block'}); var top = errorTH.offset().top; var left = errorTH.offset().left+10; errorTH.next().css({'top':top,"left":left}) errorTH.next().text('重復:'+repeatNum+',錯誤:'+errorNum); }else{ errorTH.css({'display':'none'}); } $(tr[i]).find("th .error-th,th .error-content").remove();//將另一處被渲染的行標注刪除,防止誤導 } this.remarkShow(); }, /*@method 驗證 */ validate: function() { var table = this.table; var that = this; this.allErrorNum = [];//初始化統計錯誤array this.firstError = { isExist :false, row:0, col:0 };//初始化,第一個錯誤位置改為不存在 var cellLength = table.getDataAtRow(0).length; var errorRows = []; for (var i = 0; i < cellLength; i++) { var cellData = table.getDataAtCol(i); // var newArr = []; cellData.map(function(item, index, arr) { var NewCell; var validateRule = that.opt.dataObj.headMap[that.header[i]]; var dataMap = that.opt.dataObj.dataMap[index][that.header[i]]; //if (newArr.indexOf(index) === -1) { //如果被重復項最后一個索引已有渲染標志,則不刪除渲染標志。 NewCell = table.getCell(index, i); $(NewCell).removeAttr("error"); $(NewCell).removeAttr("repeat"); dataMap["errorType"] = ""; //} if (validateRule.required && !item) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if(item){ if (validateRule.type && validateRule.type === "CODE_TYPE") { var list = that.getCodeValueList(validateRule.codeValueList); if(list.indexOf(item)===-1){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if(validateRule.type && validateRule.type === "BOOLEAN"){ if(['是','否'].indexOf(item)===-1){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if ( validateRule.type && validateRule.type === "DATE" ) { var result = false; that.dateFormat.map(function(format,index){ if(new Date(item).format(format)===item){ result = true; } }) if(!result){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if(validateRule.type && validateRule.type === "TIME" && !that.timePattern.test(item)){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if(validateRule.type && validateRule.type === "INTEGER" && parseInt(item)!=item){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if(validateRule.type && validateRule.type === "DOUBLE" && parseFloat(item)!=item){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if (parseInt(validateRule.minLength) && item.length < parseInt(validateRule.minLength)) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if (parseInt(validateRule.maxLength) && item.length > parseInt(validateRule.maxLength)) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if ((!validateRule.maxLength && !validateRule.minLength&& validateRule.length) && item.length !== validateRule.length) { dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } if (validateRule.regexp) { var pattern = new RegExp(validateRule.regexp); if(!pattern.test(item)){ dataMap["errorType"] = "error"; $(NewCell).attr("error", true); that.setAllErrorNum.call(that,index,i,"error"); errorRows.indexOf(index)===-1 && errorRows.push(index); return; } } if (validateRule.unique && that.getRepeatNum(arr,item,index)) { $(NewCell).attr("repeat", true); dataMap["errorType"] = "repeat"; that.setAllErrorNum.call(that,index,i,"repeat"); errorRows.indexOf(index)===-1 && errorRows.push(index); } } }); } $(".htInvalid").removeClass('htInvalid'); this.errorRow = errorRows.length; }, /*@method 輔助方法,獲取table中第一個錯誤點的位置 */ setAllErrorNum:function(x,y,type){ if(!this.firstError.isExist||this.firstError.row>x){ this.firstError = { isExist:true, row:x, col:y }; } if(!this.allErrorNum[x]){ this.allErrorNum[x] = []; } this.allErrorNum[x].push({ type:type, row:x, col:y }); //如果在已經定位的情況下,又修改了其他單元格到處出現新的數據,那么此處的錯誤定位要重新定位 if(x === this.errorPosition.row && y === this.errorPosition.col){ this.errorPosition.index = this.allErrorNum[x].length-1; } }, /*@method 輔助方法,判斷數組中某一個值是否有重復項 */ getRepeatNum:function(arr,val,i){ var result = false; arr.map(function(item,index,array){ if(item===val && index!==i){ result = true; } }) return result; }, /*@method 下一個錯誤調用方法 */ nextError: function() { if (this.position) { $("td[current]").removeAttr("current"); this.position = "next"; this.nextErrorPostion(); this.isPosition = true; this.scrollToError(); } }, /*@method 上一個錯誤調用方法 */ prevError: function() { if (this.position) { $("td[current]").removeAttr("current"); this.position = "prev"; this.prevErrorPosition(); this.isPosition = true; this.scrollToError(); } }, /*@method 計算出下一個錯誤單元格的位置 */ nextErrorPostion: function() { var length = this.allErrorNum[this.errorPosition.row].length; if (this.errorPosition.index < length - 1) { this.errorPosition.index += 1; this.errorPosition.col = this.allErrorNum[this.errorPosition.row][this.errorPosition.index].col; } else { var maxLength = this.allErrorNum.length; for (var i = this.errorPosition.row + 1; i < maxLength; i++) { if (this.allErrorNum[i] instanceof Array &&this.allErrorNum[i].length>0) { this.errorPosition = { index:0, row: i, col: this.allErrorNum[i][0].col }; return; } } if (i === maxLength && (this.errorPosition.index === this.allErrorNum[i-1].length-1)) { this.errorPosition = { index:0, row: this.firstError.row, col: this.firstError.col }; } } }, /*@method 計算出上一個錯誤單元格的位置 */ prevErrorPosition: function() { var length = this.allErrorNum[this.errorPosition.row].length; if (this.errorPosition.index > 0) { this.errorPosition.index -= 1; this.errorPosition.col = this.allErrorNum[this.errorPosition.row][this.errorPosition.index].col; } else { for (var i = this.errorPosition.row - 1; i >= 0; i--) { if (this.allErrorNum[i] instanceof Array &&this.allErrorNum[i].length>0) { var len = this.allErrorNum[i].length-1; this.errorPosition = { index:this.allErrorNum[i].length-1, row: i, col: this.allErrorNum[i][len].col }; return; } } } }, /*@method 滾動到errorPosition記錄的位置 */ scrollToError: function() { var that = this; var tr = $(that.opt.el + " .htCore tbody").find("th #column_name:contains('"+(that.errorPosition.row+1)+"')").closest('tr'); var lastTd = (tr.length>0)?tr.find("td[col='"+this.errorPosition.col+"']"):''; //滾動到對應行的位置,並且插件會自動渲染出新的可視區域,此時再找出定位的單元格,並標記 $(this.opt.el + " .wtHolder").animate({ scrollTop: (this.errorPosition.row-5)*28 }, 300); lastTd = tr.find("td[col='"+that.errorPosition.col+"']"); $(this.opt.el + " .wtHolder").animate({ scrollLeft: (this.errorPosition.col-1)*150 }, 300); setTimeout(function(){ //此處因為是定時器,所以要注意tr和ladtTd會在定時器回掉函數開始執行時消失,所以要在定時器中重新定義,或者使用閉包 var tr = $(that.opt.el + " .htCore tbody").find("th #column_name:contains('"+(that.errorPosition.row+1)+"')").closest('tr'); lastTd = tr.find("td[col='"+that.errorPosition.col+"']"); $(lastTd).attr("current",true); that.isPosition = false; },600) }, /*@method 標記當前錯誤點 */ currentError: function(){ if(!this.position){ this.errorPosition.row = this.firstError.row; this.errorPosition.col = this.firstError.col; } this.isPosition = true; this.position = "current"; this.scrollToError(); }, /*@method 獲取有多少條記錄是有錯誤的、正確的 *@return array [總記錄數,錯誤數,正確數] */ getErrorNum:function(){ var total = this.opt.dataObj.dataMap.length; return [total,this.errorRow,total-this.errorRow]; }, /*@method 獲取到編輯后的table數據 */ getData: function() { return this.opt.dataObj; } }; $.fn.HandsontableExtend = function (opts) { var hand = new HandsontableExtend(opts); return hand.init(window); }; })
//修改源碼部分: //第36508行,function showDatepicker(event)的部分,228,258分別為時間控件的高和寬 if(this.TD.offsetTop+228>holder.offsetHeight){ this.datePickerStyle.top = window.pageYOffset + offset.top - 228 + 'px'; }else{ this.datePickerStyle.top = window.pageYOffset + offset.top + (0, _element.outerHeight)(this.TD) + 'px'; } if(this.TD.offsetLeft+258>holder.offsetWidth){ this.datePickerStyle.left = window.pageXOffset + offset.left - (0, _element.outerWidth)(this.TD) + 'px'; }else{ this.datePickerStyle.left = window.pageXOffset + offset.left + 'px'; } //this.datePickerStyle.top = window.pageYOffset + offset.top + (0, _element.outerHeight)(this.TD) + 'px'; //this.datePickerStyle.left = window.pageXOffset + offset.left + 'px'; //i18n,json對象改為 i18n:{ previousMonth : '上一月', nextMonth : '下一月', months : ['一月','二月','三月','四月','五月','六月','七月','八月','九月','十月','十一月','十二月'], weekdays : ['星期日','星期一','星期二','星期三','星期四','星期五','星期六'], weekdaysShort : ['周日','周一','周二','周三','周四','周五','周六'] }
調用方法:
var handTableExtend = $("#hot").HandsontableExtend({
el:'#hot',
dataObj:$scope.dataObject,
isValidate:true,
set:{
height: 500
}
})
//dataObject接受的數據結構 dataObject = { headMap:{ '姓名':{ isRequired:true, type:'string', minLength:2, maxlength:20, isUnique:false }, '性別':{ isRequired:false, type:'string', isUnique:false }, '身份證號':{ isRequired:true, type:'string', minLength:9, maxlength:20, isUnique:true, regexp:/^[1-9][0-9]{5}(19|20)[0-9]{2}((01|03|05|07|08|10|12)(0[1-9]|[1-2][0-9]|31)|(04|06|09|11)(0[1-9]|[1-2][0-9]|30)|02(0[1-9]|[1-2][0-9]))[0-9]{3}([0-9]|x|X)$/ }, '聯系電話':{ isRequired:true, type:'string', //length:11, isUnique:true }, '地址':{ isRequired:false, type:'string', maxLength:50, isUnique:false }, '職位':{ isRequired:false, type:'string', isUnique:false }, '部門':{ isRequired:false, type:'code_type', enumData:['it部','hr部','后勤部','銷售部'], isUnique:false }, '入職日期':{ isRequired:false, type:'date', isUnique:false } }, dataMap:[ { '姓名':{ value:'張三', errorType:'' }, '性別':{ value:'男', errorType:'' }, '身份證號':{ value:'', errorType:'' }, '聯系電話':{ value:'18817802351', errorType:'repeat' }, '地址':{ value:'上海浦東', errorType:'' }, '職位':{ value:'it', errorType:'' }, '部門':{ value:'it部', errorType:'' }, '入職日期':{ value:'it部', errorType:'' } },{ '姓名':{value:'李四1', errorType:''}, '性別':{value:'女', errorType:''}, '身份證號':{value:'111222', errorType:''}, '聯系電話':{value:'18817802351', errorType:''}, '地址':{value:'上海浦東', errorType:''}, '職位':{value:'it', errorType:''}, '部門':{value:'it部', errorType:''}, '入職日期':{value:'it部', errorType:''} },{ '姓名':{value:'李四1', errorType:''}, '性別':{value:'女', errorType:''}, '身份證號':{value:'111222', errorType:''}, '聯系電話':{value:'18817802351', errorType:''}, '地址':{value:'上海浦東', errorType:''}, '職位':{value:'it', errorType:''}, '部門':{value:'it部', errorType:''}, '入職日期':{value:'it部', errorType:''} } ] };
頁面html代碼
<div ng-controller="SurveyHandsontable"> <button id="prev" ng-click="prevError()">></button> <button id="next" ng-click="nextError()"><</button> <div id="hot"></div> <button ng-click="getData()">點擊</button> </div>