利用 jQuery Clone 復制行

最近客串瞭一把前端,有行復制的功能用 jQuery 來實現瞭。感覺比以前原生js用 CreateElement 要簡單多瞭,但還是遇到瞭一些陷阱比如IE7的bug,這裡記錄下來。先看看 table 的樣子:這裡3行是一組,按下"Copy"連值復制,按下"Add"隻增加行不復制值。calendar 使用的是 jQuery UI 裡的 datepicker
下圖隻是一個簡單的demo,沒有復雜的樣式表:

為瞭靈活對應不同的表格,提取瞭一個共通的 js 來處理,作為使用前提:
1. table 必須有 id;
2. 有 id 的 tr 才會被復制;(tr的id從1開始編號)
3. table 內所有id都必須以 xxx_n 編號

[javascript] function RowCopyUtility(opts) { 
  // 表格Id  
  this.tableId = opts.tableId; 
  // 分組內有多少行  
  this.rowGroupNumber = opts.rowGroupNumber; 
  // 一組內Button對應的方法Map(key=Button value, value=對應方法名)  
  // 所有方法都應以 function (idx) 方式調用  
  this.buttonHandlers = opts.buttonHandlers; 
   
  this._countForRowsGroup = -1; 
  this._keyForRow = -1; 
 
  this.getTargetRowGroup = function(groupIdx) { 
 
    var rows = []; 
    if (groupIdx > 0) { 
      for(var i=1; i<this.rowGroupNumber+1; i++) { 
        rows[i-1] = $("#row" + i + "_" + groupIdx); 
      } 
    } else { 
      for(var i=0; i<this.rowGroupNumber; i++) { 
        rows[i] = $("#" + this.tableId + " tr[id]").eq(i); 
      } 
    } 
    return rows; 
  }; 
 
  this.addRow = function (groupIdx, needCopyValue) { 
 
    if (this._countForRowsGroup == -1) { 
      this._countForRowsGroup = ($("#" + this.tableId + " tr[id]").length – 1)/this.rowGroupNumber; 
      this._keyForRow =  parseInt($("#" + this.tableId + " tr[id]:not(#row_add):last").attr("id").split("_")[1]) + 1; 
    } 
 
    if (groupIdx == 0) { 
      var firstRow = $("#" + this.tableId + " tr[id]:first"); 
      var currentIdx = firstRow.attr("id").split("_")[1]; 
      groupIdx = currentIdx; 
    } 
 
    var regForId = new RegExp("^(\\w+_)" + groupIdx + "$"); 
    var regForName = new RegExp("^(\\w+_)" + groupIdx + "$"); 
    var regForRadioId = new RegExp("^(\\w+_)" + groupIdx + "(.*)$"); 
 
    var targetRows = this.getTargetRowGroup(groupIdx); 
 
    // 重要:註意閉包參數的作用域  
    var idx = this._keyForRow;  www.aiwalls.com
 
    for(var i=0; i<targetRows.length; i++) { 
      // clone target rows  
      var cloneRow = targetRows[i].clone(false); 
      var newRowId = cloneRow.attr("id").split("_")[0] + "_" + idx; 
      cloneRow.attr("id", newRowId); 
 
      var radios = []; 
      cloneRow.find("[id]").each(function() { 
          var id = $(this).attr("id"); 
          var oldId = id; 
          var name = $(this).attr("name"); 
          
          id = id.replace(regForId, "$1" + idx); 
          $(this).attr("id", id); 
          
          var newname = name.replace(regForName, "$1" + idx); 
          $(this).attr("name", newname); 
           
          if ($(this).hasClass("hasDatepicker")) { 
              $(this).removeClass("hasDatepicker"); 
          } 
          
          if ($(this).attr("type") == "checkbox") { 
              if($(this).next().attr("for") != "") { 
                  $(this).next().attr("for", id); 
              } 
              if (!needCopyValue) { 
                  $(this).attr("checked", ""); 
              } 
          } 
          else if ($(this).attr("type") == "radio") {            
              id = id.replace(regForRadioId, "$1" + idx); 
              $(this).attr("id", id); 
          
              var radio = new Object(); 
              radio.id = id; 
              radio.oldId = oldId; 
              radio.name = name; 
              radio.newname = newname; 
              // IE7's Bug  
              radio.checked = document.getElementById(oldId).checked; 
              radios[radios.length] = radio; 
          
              if($(this).next().attr("for") != "") { 
                  $(this).next().attr("for", id); 
              } 
             
              if (!needCopyValue) { 
                  $(this).attr("checked", ""); 
              } 
          } 
          else if ($(this).attr("tagName") == "SELECT") { 
              if (needCopyValue) { 
                  $(this).val(document.getElementById(oldId).value); 
              } 
          } 
          else if ($(this).attr("tagName") == "TEXTAREA" ||  
                   $(this).attr("type") == "text" ||  
                   $(this).attr("type") == "hidden") { 
              if (!needCopyValue) { 
                  $(this).val(""); 
              } 
          } 
      }); 
 
      // insert into document  
      cloneRow.insertBefore("#" + this.tableId + " tr:last"); 
 
      // replace name for radio  
      for(var n=0; n<radios.length; n++) { 
         document.getElementById(radios[n].id).outerHTML = 
           document.getElementById(radios[n].id).outerHTML.replace(radios[n].name, radios[n].newname); 
         // IE7's Bug  
         document.getElementById(radios[n].oldId).checked = radios[n].checked; 
      } 
 
      // Event Handler  
      var maps = this.buttonHandlers; 
      cloneRow.find("input:button").each(function() { 
         var value = $(this).attr("value"); 
         
         var funcName = maps[value]; 
         if (funcName != undefined) {          
             var func = null; 
             func = function() { eval(funcName + "(" + idx + ")"); }; 
                 
             if (func != null) { 
               $(this).attr("onclick", ""); 
               $(this).unbind("click"); 
               $(this).attr("onclick", "").click(func); 
             } 
         } 
      }); 
    } 
 
    this._countForRowsGroup++; 
    this._keyForRow++; 
    
  }; 
 
 
  this.copyRow = function(groupIdx) { 
    this.addRow(groupIdx, true); 
  }; 
 
  this.deleteRow = function(groupIdx) { 
 
    if (this._countForRowsGroup == -1) { 
      this._countForRowsGroup = ($("#" + this.tableId + " tr[id]").length – 1)/this.rowGroupNumber; 
      this._keyForRow =  parseInt($("#" + this.tableId + " tr[id]:not(#row_add):last").attr("id").split("_")[1]) + 1; 
    } 
 
    var allRows = $("#" + this.tableId + " tr[id]"); 
    var miniRowsCount = this.rowGroupNumber + 1; 
    var tbl = $("#" + this.tableId); 
 
    if (allRows.length == miniRowsCount) { 
      tbl.find("input:text").each(function() { $(this).val(""); }); 
      tbl.find("textarea").each(function() { $(this).val(""); }); 
      tbl.find("input:hidden").each(function() { $(this).val(""); }); 
      tbl.find("input:radio").each(function() { $(this).attr("checked", ""); }); 
      tbl.find("input:checkbox").each(function() { $(this).attr("checked", ""); }); 
      tbl.find("select").each(function() { document.getElementById($(this).attr("id")).selectedIndex = 0; }); 
      tbl.find(".fg-common-field-errored").each(function() { 
        $(this).removeClass("fg-common-field-errored"); 
      }); 
      return; 
    } 
 
    for(var i=1; i<this.rowGroupNumber+1; i++) { 
      tbl.find("#row" + i + "_" + groupIdx).remove(); 
    } 
 
    this._countForRowsGroup–; 
  }; 
 

function RowCopyUtility(opts) {
  // 表格Id
  this.tableId = opts.tableId;
  // 分組內有多少行
  this.rowGroupNumber = opts.rowGroupNumber;
  // 一組內Button對應的方法Map(key=Button value, value=對應方法名)
  // 所有方法都應以 function (idx) 方式調用
  this.buttonHandlers = opts.buttonHandlers;
 
  this._countForRowsGroup = -1;
  this._keyForRow = -1;

  this.getTargetRowGroup = function(groupIdx) {

    var rows = [];
    if (groupIdx > 0) {
      for(var i=1; i<this.rowGroupNumber+1; i++) {
        rows[i-1] = $("#row" + i + "_" + groupIdx);
      }
    } else {
      for(var i=0; i<this.rowGroupNumber; i++) {
        rows[i] = $("#" + this.tableId + " tr[id]").eq(i);
      }
    }
    return rows;
  };

  this.addRow = function (groupIdx, needCopyValue) {

    if (this._countForRowsGroup == -1) {
      this._countForRowsGroup = ($("#" + this.tableId + " tr[id]").length – 1)/this.rowGroupNumber;
      this._keyForRow =  parseInt($("#" + this.tableId + " tr[id]:not(#row_add):last").attr("id").split("_")[1]) + 1;
    }

    if (groupIdx == 0) {
      var firstRow = $("#" + this.tableId + " tr[id]:first");
      var currentIdx = firstRow.attr("id").split("_")[1];
      groupIdx = currentIdx;
    }

    var regForId = new RegExp("^(\\w+_)" + groupIdx + "$");
    var regForName = new RegExp("^(\\w+_)" + groupIdx + "$");
    var regForRadioId = new RegExp("^(\\w+_)" + groupIdx + "(.*)$");

    var targetRows = this.getTargetRowGroup(groupIdx);

    // 重要:註意閉包參數的作用域
    var idx = this._keyForRow;

    for(var i=0; i<targetRows.length; i++) {
      // clone target rows
      var cloneRow = targetRows[i].clone(false);
      var newRowId = cloneRow.attr("id").split("_")[0] + "_" + idx;
      cloneRow.attr("id", newRowId);

      var radios = [];
      cloneRow.find("[id]").each(function() {
          var id = $(this).attr("id");
          var oldId = id;
          var name = $(this).attr("name");
        
          id = id.replace(regForId, "$1" + idx);
          $(this).attr("id", id);
        
          var newname = name.replace(regForName, "$1" + idx);
          $(this).attr("name", newname);
         
          if ($(this).hasClass("hasDatepicker")) {
             $(this).removeClass("hasDatepicker");
          }
        
          if ($(this).attr("type") == "checkbox") {
              if($(this).next().attr("for") != "") {
                  $(this).next().attr("for", id);
              }
              if (!needCopyValue) {
               $(this).attr("checked", "");
              }
          }
          else if ($(this).attr("type") == "radio") {          
              id = id.replace(regForRadioId, "$1" + idx);
              $(this).attr("id", id);
        
              var radio = new Object();
              radio.id = id;
              radio.oldId = oldId;
              radio.name = name;
              radio.newname = newname;
              // IE7's Bug
              radio.checked = document.getElementById(oldId).checked;
              radios[radios.length] = radio;
        
              if($(this).next().attr("for") != "") {
                  $(this).next().attr("for", id);
              }
           
              if (!needCopyValue) {
               $(this).attr("checked", "");
              }
          }
          else if ($(this).attr("tagName") == "SELECT") {
           if (needCopyValue) {
              $(this).val(document.getElementById(oldId).value);
           }
          }
          else if ($(this).attr("tagName") == "TEXTAREA" ||
                   $(this).attr("type") == "text" ||
                   $(this).attr("type") == "hidden") {
           if (!needCopyValue) {
               $(this).val("");
           }
          }
      });

      // insert into document
      cloneRow.insertBefore("#" + this.tableId + " tr:last");

      // replace name for radio
      for(var n=0; n<radios.length; n++) {
         document.getElementById(radios[n].id).outerHTML =
           document.getElementById(radios[n].id).outerHTML.replace(radios[n].name, radios[n].newname);
         // IE7's Bug
         document.getElementById(radios[n].oldId).checked = radios[n].checked;
      }

      // Event Handler
      var maps = this.buttonHandlers;
      cloneRow.find("input:button").each(function() {
         var value = $(this).attr("value");
       
         var funcName = maps[value];
         if (funcName != undefined) {        
          var func = null;
          func = function() { eval(funcName + "(" + idx + ")"); };
           
          if (func != null) {
            $(this).attr("onclick", "");
            $(this).unbind("click");
            $(this).attr("onclick", "").click(func);
          }
         }
      });
    }

    this._countForRowsGroup++;
    this._keyForRow++;
  
  };

  this.copyRow = function(groupIdx) {
    this.addRow(groupIdx, true);
  };

  this.deleteRow = function(groupIdx) {

    if (this._countForRowsGroup == -1) {
      this._countForRowsGroup = ($("#" + this.tableId + " tr[id]").length – 1)/this.rowGroupNumber;
      this._keyForRow =  parseInt($("#" + this.tableId + " tr[id]:not(#row_add):last").attr("id").split("_")[1]) + 1;
    }

    var allRows = $("#" + this.tableId + " tr[id]");
    var miniRowsCount = this.rowGroupNumber + 1;
    var tbl = $("#" + this.tableId);

    if (allRows.length == miniRowsCount) {
      tbl.find("input:text").each(function() { $(this).val(""); });
      tbl.find("textarea").each(function() { $(this).val(""); });
      tbl.find("input:hidden").each(function() { $(this).val(""); });
      tbl.find("input:radio").each(function() { $(this).attr("checked", ""); });
      tbl.find("input:checkbox").each(function() { $(this).attr("checked", ""); });
      tbl.find("select").each(function() { document.getElementById($(this).attr("id")).selectedIndex = 0; });
      tbl.find(".fg-common-field-errored").each(function() {
        $(this).removeClass("fg-common-field-errored");
      });
      return;
    }

    for(var i=1; i<this.rowGroupNumber+1; i++) {
      tbl.find("#row" + i + "_" + groupIdx).remove();
    }

    this._countForRowsGroup–;
  };

}實際遇到的問題與解決辦法:
1. jQuery 的 Clone() 方法,就算傳入 false,元素的事件依然會被復制過來。(IE測試)
2. attr("name", name); 在IE中,不會直接替換掉,而是生成 submitName 保存。在 IE7 裡 radio 會因為 name 相同而出現問題。
3. 在大量的匿名方法中,特別要註意閉包封送參數的作用域。
4. IE7裡的Bug:在radio被復制時,原來的元素的選擇值就沒瞭。因此在復制前保存瞭復制源的radio屬性,加入document之後再次設定:
[javascript] // replace name for radio  
for(var n=0; n<radios.length; n++) { 
   document.getElementById(radios[n].id).outerHTML = 
     document.getElementById(radios[n].id).outerHTML.replace(radios[n].name, radios[n].newname); 
   // IE7's Bug  
   document.getElementById(radios[n].oldId).checked = radios[n].checked; 

// replace name for radio
for(var n=0; n<radios.length; n++) {
   document.getElementById(radios[n].id).outerHTML =
     document.getElementById(radios[n].id).outerHTML.replace(radios[n].name, radios[n].newname);
   // IE7's Bug
   document.getElementById(radios[n].oldId).checked = radios[n].checked;
}5. jQuery裡清除事件單獨用 attr("onclick", "") 並不好用;後期用 click(function) 綁定的事件用 unbind("click") 可以移除。
[javascript] if (func != null) { 
  $(this).attr("onclick", ""); 
  $(this).unbind("click"); 
  $(this).attr("onclick", "").click(func); 

if (func != null) {
  $(this).attr("onclick", "");
  $(this).unbind("click");
  $(this).attr("onclick", "").click(func);
}

6. jQuery UI 的 DatePicker 當創建瞭 datepicker 之後,可以通過 hasClass("hasDatepick") 判斷是否存在,否則在復制之後有問題。
    (多次復制之後 datepicker settings 會莫名其妙丟失)

7. 其他,剩下就是要註意 jQuery 選擇器不要過度使用瞭,越復雜的表達式效率越低。

還要說下IE9 的 debug 工具真心不錯,提高不少開發效率哦一定要利用。

 

就這些,希望能對大傢有幫助。最後附上,測試用的 html:

 

[html] <html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja"> 
<head> 
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
<meta http-equiv="Pragma" content="no-cache" /> 
<meta http-equiv="Cache-Control" content="no-cache" /> 
<meta http-equiv="Expires" content="0" /> 
<style> 
body{font-family:'Open Sans',arial,sans-serif;} 
tr{height:30px} 
input.button{width:60px} 
table.main { 
    border-width: 2px; 
    border-spacing: 1px; 
    border-style: solid; 
    border-color: gray; 
    border-collapse: collapse; 
    background-color: white; 

table.main th { 
    border-width: 1px; 
    padding: 5px; 
    border-style: inset; 
    border-color: gray; 
    background-color: #f0f0f0; 
    -moz-border-radius: ; 

table.main td { 
    border-width: 1px; 
    padding: 5px; 
    border-style: inset; 
    border-color: gray; 
    background-color: white; 
    -moz-border-radius: ; 

</style> 
 
<script type="text/javascript" language="JavaScript" src="jquery.js"></script> 
<script type="text/javascript" language="JavaScript" src="jquery-ui.js"></script> 
<script type="text/javascript" language="JavaScript" src="rowCopyUtil.js"></script> 
<link rel="stylesheet" href="jquery-ui.css" type="text/css" media="all" /> 
<link type="text/css" href="jqueryCalendarStyle.css" rel="stylesheet" /> 
 
<script type="text/javascript" > 
 var rowUtil = new RowCopyUtility( 
    { 
      tableId: "tab1", 
      rowGroupNumber: 3, 
      buttonHandlers: {"Copy":"copyRows", "Delete":"deleteRows", "calendar":"showDatepicker", "some button":"someButtonClick"} 
    } 
 ); 
    
 function showDatepicker(idx) { 
      var textId = "#calendar_" + idx; 
      if (!$(textId).hasClass("hasDatepicker")) { 
          var text = $(textId).datepicker({ 
            showOn : "calendar", 
            dateFormat : "yy/mm/dd" 
          }); 
      } 
      $(textId).datepicker('show'); 
 } 
  
 function addRows() { 
    rowUtil.addRow(0, false); 
 } 
 function copyRows(idx) { 
    rowUtil.copyRow(idx); 
 } 
 function deleteRows(idx) { 
    rowUtil.deleteRow(idx); 
 } 
 function someButtonClick(idx) { 
    alert(idx); 
 } 
</script> 
</head> 
<body> 
  <table id="tab1" class="main"> 
    <tr> 
       <th>Header1</th> 
       <th>Header2</th> 
       <th>Header3</th> 
       <th>Header4</th> 
    </tr> 
    <tr id="row1_0"> 
       <td rowspan="3" > 
           <input class="button" type="button" value="Copy" onclick="copyRows(0);" /> 
           <input class="button" type="button" value="Delete" onclick="deleteRows(0);" /> 
       </td> 
       <td>text:<input type="text" id="text_0" /></td> 
       <td> 
           <input type="radio" name="radioAB_0" id="radioA_0" value="1" /><label for="radioA_0">Raido_A </label> 
           <input type="radio" name="radioAB_0" id="radioB_0" value="2" /><label for="radioB_0">Radio_B </label> 
       </td> 
       <td> 
           <select id="select_0"> 
              <option value="0">—select—</option> 
              <option value="1">select option1</option> 
              <option value="2">select option2</option> 
           </select> 
       </td> 
    </tr> 
    <tr id="row2_0"> 
      <td> 
          <input type="checkbox" id="checkA_0" /><label for="checkA_0">Check_A </label> 
          <input type="checkbox" id="checkB_0" /><label for="checkB_0">Check_B </label> 
      </td> 
      <td colspan="2"> 
        <input type="text" id="calendar_0" style="width:90px"/><input type="button" value="calendar" onclick="showDatepicker(0);" /> 
        <input type="button" value="some button" onclick="someButtonClick(0);" /> 
      </td> 
    </tr> 
    <tr id="row3_0"> 
      <td colspan="3"> 
        textarea:<textarea id="textarea_0" style="width:100%"></textarea> 
      </td> 
    </tr> 
    <tr id="row_add"> 
      <td colspan="4"> 
        <input class="button" type="button" value="Add" onclick="addRows();" /> 
      </td> 
    </tr> 
  </table> 
</body> 
 
</html> 

摘自 Lullaby's Blog

發佈留言