17

我有一个包含具有行跨属性的单元格的表格,我想:

  1. 每当 atr被隐藏时,表格都会正确地重新排列
  2. 每当atr再次显示时,它将恢复到原始状态

因此,如果您有这样的表格,点击X不应该破坏布局。并单击一个come back按钮,应恢复原始布局。

(尝试从下往上删除所有行,而不是从右到左恢复它们,这是一个理想的流程)

我有一些半解决方案,但似乎都太复杂了,我相信有一个很好的方法来处理这个问题。

4

3 回答 3

23

好的,我真的在这个问题上花了很长时间,所以这里......

对于那些只想查看有效解决方案的人,请单击此处

更新:我已经更改了可视列计算方法来迭代表并创建一个二维数组,要查看使用 jQueryoffset()方法的旧方法,请单击此处。代码更短,但时间成本更高。

问题的存在是因为当我们隐藏一行时,虽然我们希望隐藏所有单元格,但我们希望伪单元格- 即由于单元格rowspan属性而出现在后续行中的单元格 - 持续存在。为了解决这个问题,每当我们遇到一个带有 a 的隐藏单元格时rowspan,我们都会尝试将其向下移动到下一个可见行(rowspan随着我们去减少它的值)。使用我们的原始单元格或它的克隆,然后我们对包含伪单元格的每一行再次向下迭代表,如果该行被隐藏,我们将rowspan再次递减。(要了解原因,请查看工作示例,并注意当蓝色行被隐藏时,红色单元格 9 的行跨度必须从 2 减少到 1,否则会将绿色 9 向右推)。

考虑到这一点,我们必须在显示/隐藏行时应用以下函数:

function calculate_rowspans() {
  // Remove all temporary cells
  $(".tmp").remove();

  // We don't care about the last row
  // If it's hidden, it's cells can't go anywhere else
  $("tr").not(":last").each(function() {
    var $tr = $(this);

    // Iterate over all non-tmp cells with a rowspan    
    $("td[rowspan]:not(.tmp)", $tr).each(function() {
      $td = $(this);
      var $rows_down = $tr;
      var new_rowspan = 1;

      // If the cell is visible then we don't need to create a copy
      if($td.is(":visible")) {
        // Traverse down the table given the rowspan
        for(var i = 0; i < $td.data("rowspan") - 1; i ++) {

          $rows_down = $rows_down.next();
          // If our cell's row is visible then it can have a rowspan
          if($rows_down.is(":visible")) {
            new_rowspan ++;
          }
        }
        // Set our rowspan value
        $td.attr("rowspan", new_rowspan);   
      }
      else {
        // We'll normally create a copy, unless all of the rows
        // that the cell would cover are hidden
        var $copy = false;
        // Iterate down over all rows the cell would normally cover
        for(var i = 0; i < $td.data("rowspan") - 1; i ++) {
          $rows_down = $rows_down.next();
          // We only consider visible rows
          if($rows_down.is(":visible")) {
            // If first visible row, create a copy

            if(!$copy) {
              $copy = $td.clone(true).addClass("tmp");
              // You could do this 1000 better ways, using classes e.g
              $copy.css({
                "background-color": $td.parent().css("background-color")
              });
              // Insert the copy where the original would normally be
              // by positioning it relative to it's columns data value 
              var $before = $("td", $rows_down).filter(function() {
                return $(this).data("column") > $copy.data("column");
              });
              if($before.length) $before.eq(0).before($copy);
              else $(".delete-cell", $rows_down).before($copy);
            }
            // For all other visible rows, increment the rowspan
            else new_rowspan ++;
          }
        }
        // If we made a copy then set the rowspan value
        if(copy) copy.attr("rowspan", new_rowspan);
      }
    });
  });
}

问题的下一个非常困难的部分是计算将单元格副本放置在行内的哪个索引处。请注意,在示例中,蓝色单元格 2 在其行中的实际索引为 0,即它是该行中的第一个实际单元格,但是我们可以看到它在视觉上位于第 2 列(0 索引)。

一旦加载文档,我就只计算一次。然后,我将此值存储为单元格的数据属性,以便我可以将它的副本放置在正确的位置(我在这个上有很多 Eureka 时刻,并做了很多页的笔记!)。为了进行这个计算,我最终构建了一个二维数组matrix来跟踪所有使用过的可视列。同时,我存储单元格的原始rowspan值,因为这将随着隐藏/显示行而改变:

function get_cell_data() {
    var matrix = [];  
    $("tr").each(function(i) {
        var $cells_in_row = $("td", this);
        // If doesn't exist, create array for row
        if(!matrix[i]) matrix[i] = [];
        $cells_in_row.each(function(j) {
            // CALCULATE VISUAL COLUMN
            // Store progress in matrix
            var column = next_column(matrix[i]);
            // Store it in data to use later
            $(this).data("column", column);
            // Consume this space
            matrix[i][column] = "x";
            // If the cell has a rowspan, consume space across
            // Other rows by iterating down
            if($(this).attr("rowspan")) {
                // Store rowspan in data, so it's not lost
                var rowspan = parseInt($(this).attr("rowspan"));
                $(this).data("rowspan", rowspan);
                for(var x = 1; x < rowspan; x++) {
                    // If this row doesn't yet exist, create it
                    if(!matrix[i+x]) matrix[i+x] = [];
                    matrix[i+x][column] = "x";
                }
            }
        });
    });

    // Calculate the next empty column in our array
    // Note that our array will be sparse at times, and
    // so we need to fill the first empty index or push to end
    function next_column(ar) {
        for(var next = 0; next < ar.length; next ++) {
            if(!ar[next]) return next;
        }
        return next;
    }
}

然后只需将其应用于页面加载:

$(document).ready(function() {
    get_cell_data();
});

(注意:虽然这里的代码比我的 jQuery.offset()替代代码长,但计算起来可能更快。如果我错了,请纠正我)。

于 2013-07-19T13:42:57.350 回答
1

工作解决方案 - http://codepen.io/jmarroyave/pen/eLkst

这与我之前提出的解决方案基本相同,我只是更改了如何获取列索引以消除 jquery.position 的限制,并对代码进行了一些重构。

function layoutInitialize(tableId){
  var layout = String();
  var maxCols, maxRows, pos, i, rowspan, idx, xy;

  maxCols = $(tableId + ' tr').first().children().length;
  maxRows = $(tableId + ' tr').length;

  // Initialize the layout matrix
  for(i = 0; i < (maxCols * maxRows); i++){
    layout += '?';
  }

  // Initialize cell data
  $(tableId + ' td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    rowspan = 1;
    if($(this).attr('rowspan')){
      rowspan = $(this).attr("rowspan");  
      $(this).data("rowspan", rowspan);  
    }

    // Look for the next position available
    idx = layout.indexOf('?');
    pos = {x:idx % maxCols, y:Math.floor(idx / maxCols)}; 
    // store the column index in the cell for future reposition
    $(this).data('column', pos.x);
    for(i = 0; i < rowspan; i++){
      // Mark this position as not available
      xy = (maxCols * pos.y) + pos.x
      layout = layout.substr(0, xy + (i * maxCols)) + 'X' + layout.substr(xy + (i * maxCols)  + 1);
    }
  });   

}

解决方案:使用 jquery.position() - http://codepen.io/jmarroyave/pen/rftdy

这是另一种解决方案,它假定第一行包含有关表列数和每个列的位置的所有信息。

这种方法的限制是必须在表格可见时调用初始化代码,因为它取决于列的可见位置。

如果这不是问题,希望它对您有用

初始化

  // Initialize cell data
  $('td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    $(this).data('posx', $(this).position().left);
    if($(this).attr('rowspan')){
      $(this).data("rowspan", $(this).attr("rowspan"));  
    }
  });

更新 根据这篇文章确保可以管理表的可见性

  $('table').show();
  // Initialize cell data
  $('td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    $(this).data('posx', $(this).position().left);
    if($(this).attr('rowspan')){
      $(this).data("rowspan", $(this).attr("rowspan"));  
    }
  });
  $('table').hide();

正如 Ian 所说,这个问题要解决的主要问题是在将隐藏行与可见行合并时计算单元格的位置。

我试图弄清楚浏览器如何实现该功能以及如何使用它。然后查看 DOM 我搜索了 columnVisiblePosition 之类的东西,我找到了位置属性并采取了这种方式

 function getColumnVisiblePostion($firstRow, $cell){
  var tdsFirstRow = $firstRow.children();
  for(var i = 0; i < tdsFirstRow.length; i++){
    if($(tdsFirstRow[i]).data('posx') == $cell.data('posx')){
      return i;
    }
  }
}

js代码

$(document).ready(function () {
  add_delete_buttons();

  $(window).on("tr_gone", function (e, tr) {
    add_come_back_button(tr);
  });

  // Initialize cell data
  $('td').each(function() {
    $(this).addClass($(this).parent().attr('color_class'));
    $(this).data('posx', $(this).position().left);
    if($(this).attr('rowspan')){
      $(this).data("rowspan", $(this).attr("rowspan"));  
    }
  });
});

function calculate_max_rowspans() {
  // Remove all temporary cells
  $(".tmp").remove();

  // Get all rows
  var trs = $('tr'), tds, tdsTarget,
      $tr, $trTarget, $td, $trFirst,
      cellPos, cellTargetPos, i;

  // Get the first row, this is the layout reference
  $trFirst = $('tr').first();

  // Iterate through all rows
  for(var rowIdx = 0; rowIdx < trs.length; rowIdx++){
    $tr = $(trs[rowIdx]);
    $trTarget = $(trs[rowIdx+1]);
    tds = $tr.children();

    // For each cell in row
    for(cellIdx = 0; cellIdx < tds.length; cellIdx++){
      $td = $(tds[cellIdx]);
      // Find which one has a rowspan
      if($td.data('rowspan')){
        var rowspan = Number($td.data('rowspan'));

        // Evaluate how the rowspan should be display in the current state
        // verify if the cell with rowspan has some hidden rows
        for(i = rowIdx; i < (rowIdx + Number($td.data('rowspan'))); i++){
          if(!$(trs[i]).is(':visible')){
            rowspan--;
          }
        }

        $td.attr('rowspan', rowspan);

        // if the cell doesn't have rows hidden within, evaluate the next cell
        if(rowspan == $td.data('rowspan')) continue;

        // If this row is hidden copy the values to the next row
        if(!$tr.is(':visible') && rowspan > 0) {
          $clone = $td.clone();
          // right now, the script doesn't care about copying data, 
          // but here is the place to implement it
          $clone.data('rowspan', $td.data('rowspan') - 1);
          $clone.data('posx', $td.data('posx'));
          $clone.attr('rowspan',  rowspan);
          $clone.addClass('tmp');

          // Insert the temp node in the correct position
          // Get the current cell position
          cellPos = getColumnVisiblePostion($trFirst, $td);

          // if  is the last just append it
          if(cellPos == $trFirst.children().length - 1){
            $trTarget.append($clone);
          }
          // Otherwise, insert it before its closer sibling
          else {
            tdsTarget = $trTarget.children();
            for(i = 0; i < tdsTarget.length; i++){
              cellTargetPos = getColumnVisiblePostion($trFirst, $(tdsTarget[i]));
              if(cellPos < cellTargetPos){
                $(tdsTarget[i]).before($clone);  
                break;
              }
            }                
          }          
        }
      } 
    }

    // remove tmp nodes from the previous row 
    if(rowIdx > 0){
      $tr = $(trs[rowIdx-1]);
      if(!$tr.is(':visible')){
        $tr.children(".tmp").remove();  
      }

    } 
  }
}

// this function calculates the position of a column 
// based on the visible position
function getColumnVisiblePostion($firstRow, $cell){
  var tdsFirstRow = $firstRow.children();
  for(var i = 0; i < tdsFirstRow.length; i++){
    if($(tdsFirstRow[i]).data('posx') == $cell.data('posx')){
      return i;
    }
  }
}

function add_delete_buttons() {
  var $all_rows = $("tr");
  $all_rows.each(function () {
    // TR to remove
    var $tr = $(this);
    var delete_btn = $("<button>").text("x");
    delete_btn.on("click", function () {
      $tr.hide();
      calculate_max_rowspans();
      $(window).trigger("tr_gone", $tr);
    });
    var delete_cell = $("<td>");
    delete_cell.append(delete_btn);
    $(this).append(delete_cell);
  });
}

function add_come_back_button(tr) {
  var $tr = $(tr);
  var come_back_btn = $("<button>").text("come back " + $tr.attr("color_class"));
  come_back_btn.css({"background": $(tr).css("background")});
  come_back_btn.on("click", function () {
    $tr.show();
    come_back_btn.remove();
    calculate_max_rowspans();
  });
  $("table").before(come_back_btn);
}

如果您有任何问题或意见,请告诉我。

于 2013-08-02T00:07:02.263 回答
0

我假设您希望在隐藏行时行向上移动,但您不希望单元格向左移动。

这是我得到的http://codepen.io/anon/pen/prDcK

我添加了两个 CSS 规则:

#come_back_container{height: 30px;}
td[rowspan='0']{background-color: white;}

这是我使用的html:

<div id="come_back_container"></div>
<table id="dynamic_table" cellpadding=7></table>
<table id="dynamic_table2" cellpadding=7>
  <tr style="background-color: red">
    <td rowspan="5">a</td>
    <td rowspan="1">b</td>
    <td rowspan="5">c</td>
    <td rowspan="1">d</td>
    <td rowspan="2">e</td>
  </tr>
  <tr style="background-color: grey">
    <td rowspan="0">f</td>
    <td rowspan="1">g</td>
    <td rowspan="0">h</td>
    <td rowspan="1">i</td>
    <td rowspan="0">j</td>
  </tr>
  <tr style="background-color: blue">
    <td rowspan="0">k</td>
    <td rowspan="1">l</td>
    <td rowspan="0">m</td>
    <td rowspan="1">n</td>
    <td rowspan="1">o</td>
  </tr>
  <tr style="background-color: yellow">
    <td rowspan="0">p</td>
    <td rowspan="1">q</td>
    <td rowspan="0">r</td>
    <td rowspan="1">s</td>
    <td rowspan="2">t</td>
  </tr>
  <tr style="background-color: green">
    <td rowspan="0">u</td>
    <td rowspan="1">v</td>
    <td rowspan="0">w</td>
    <td rowspan="1">x</td>
    <td rowspan="0">y</td>
  </tr>
</table>

第一条规则只是将桌子的上边缘保持在同一位置。第二条规则是通过与背景混合使单元格显示为空白,因此请相应地进行更改。

最后是js:

$(function () {
  //firstTable()

  var myTb2 = new dynamicTable();
  myTb2.createFromElement( $("#dynamic_table2") );
  myTb2.drawTable()

  $(window).on("tr_hide", function (e,data){
    var tbl = data.ctx,
        rowIndex = data.idx;
    tbl.hideRow.call(tbl, rowIndex);
  })
  $(window).on("tr_show", function (e,data){
    var tbl = data.ctx,
        rowIndex = data.idx;
    tbl.showRow.call(tbl, rowIndex);
  })
})

function dynamicTableItem(){
  this.height = null;
  this.content = null;
}

function dynamicTableRow(){
  this.color = null;
  this.items = []
  this.show = true

  this.setNumColumns = function(numCols){
    for(var i=0;i<numCols;i++){
      var item = new dynamicTableItem(); 
      item.height = 0;
      this.items.push(item)
    }
  }

  this.addItem = function(index, height, content){
    var item = new dynamicTableItem();
    item.height = height;
    item.content = content;
    if(index>=this.items.length){ console.error("index out of range",index); }
    this.items[index] = item;
  }
}

function dynamicTable(){
  this.element = null;
  this.numCols = null;
  this.rows = []

  this.addRow = function(color){
    var row = new dynamicTableRow();
    row.color = color;
    row.setNumColumns(this.numCols)
    var length = this.rows.push( row )
    return this.rows[length-1]
  }
  this.drawTable = function(){
    this.element.empty()

    var cols = [],
        rowElements = [];
    for(var i=0;i<this.numCols;i++){
      cols.push( [] )
    }

    for(var r=0; r<this.rows.length; r++){
      var row = this.rows[r]
      if(row.show){
        var $tr = $("<tr>"),
            delete_cell = $("<td>"),
            delete_btn = $("<button>").text("x")
        var data = {ctx: this, idx: r};
        delete_btn.on("click", data, function(e){
          $(window).trigger("tr_hide", e.data);
        })
        delete_cell.addClass("deleteCell");
        $tr.css( {"background": row.color} );

        delete_cell.append(delete_btn);
        $tr.append(delete_cell);
        this.element.append($tr);
        rowElements.push( $tr );

        for(var i=0; i<row.items.length; i++){
          cols[i].push( row.items[i] );
        }
      }
    }

    for(var c=0; c<cols.length; c++){
      var cellsFilled = 0;
      for(var r=0; r<cols[c].length; r++){
        var item = cols[c][r]
        var size = item.height;
        if(r>=cellsFilled){
          cellsFilled += (size>0 ? size : 1);
          var el = $("<td>").attr("rowspan",size);
          el.append(item.content);
          rowElements[r].children().last().before(el);
        }
      }
    }
  }

  this.hideRow = function(rowIndex){
    var row = this.rows[rowIndex]
    row.show = false; 

    var come_back_btn = $("<button>").text("come back");
    come_back_btn.css( {"background": row.color} );
    var data = {ctx:this, idx:rowIndex};
    come_back_btn.on("click", data, function(e){
      $(window).trigger("tr_show", e.data);
      $(this).remove();
    });
    $("#come_back_container").append(come_back_btn);

    this.drawTable();
  }

  this.showRow = function(rowIndex){
    this.rows[rowIndex].show = true;
    this.drawTable();
  }

  this.createFromElement = function(tbl){
    this.element = tbl;
    var tblBody = tbl.children().filter("tbody")
    var rows = tblBody.children().filter("tr")
    this.numCols = rows.length

    for(var r=0;r<rows.length;r++){
      var row = this.addRow( $(rows[r]).css("background-color") );
      var items = $(rows[r]).children().filter("td");

      for(var i=0;i<items.length;i++){
        var item = $(items[i]);
        var height = parseInt(item.attr("rowspan"));
        var contents = item.contents();
        row.addItem(i,height,contents);
      }
    }
    //console.log(this); 
  }

}

function firstTable(){
  var myTable = new dynamicTable();
  myTable.element = $("#dynamic_table");
  myTable.numCols = 5

  var red = myTable.addRow("red"); 
  red.addItem(0,5);
  red.addItem(1,1);
  red.addItem(2,5);
  red.addItem(3,1);
  red.addItem(4,2);

  var white = myTable.addRow("grey");
  //white.addItem(0,0);
  white.addItem(1,1);
  //white.addItem(2,0);
  white.addItem(3,1);
  //white.addItem(4,0);

  var blue = myTable.addRow("blue");
  //blue.addItem(0,3);  //try uncommenting this and removing red
  blue.addItem(1,1);
  //blue.addItem(2,0);
  blue.addItem(3,1);
  blue.addItem(4,1);

  var yellow = myTable.addRow("yellow");
  //yellow.addItem(0,0);
  yellow.addItem(1,1);
  //yellow.addItem(2,0);
  yellow.addItem(3,1);
  yellow.addItem(4,2);

  var green = myTable.addRow("green");
  //green.addItem(0,0);
  green.addItem(1,1);
  //green.addItem(2,0);
  green.addItem(3,1);
  //green.addItem(4,0);

  myTable.drawTable();
}

我尝试使用清晰的变量和方法名称,但如果您有任何任务,请询问。

PS-我知道现在没有简单的方法向单元格添加内容,但您只要求消失的行。

于 2013-08-01T22:24:22.463 回答