Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/jquery/72.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Javascript 隐藏表行时使用rowspan处理单元格_Javascript_Jquery_Html_Algorithm - Fatal编程技术网

Javascript 隐藏表行时使用rowspan处理单元格

Javascript 隐藏表行时使用rowspan处理单元格,javascript,jquery,html,algorithm,Javascript,Jquery,Html,Algorithm,我有一个问题,我想: 每当tr被隐藏时,表格将正确地重新排列 每当再次显示tr时,它将恢复到原始状态 因此,如果您点击X不应破坏布局。 并点击一个返回按钮,应恢复原来的布局 (尝试从下到上删除所有行,然后从右到左恢复它们,这是所需的流) 我有一些半解,但似乎都太复杂了,我相信有一个很好的方法来处理这个问题。好的,我真的花了很长时间在这个问题上,所以这里是 对于那些只想看到有效解决方案的人, 更新:我更改了可视列计算方法,以迭代表格并创建二维数组,以查看使用jQueryoffset()方法的旧方法

我有一个问题,我想:

  • 每当
    tr
    被隐藏时,表格将正确地重新排列
  • 每当再次显示
    tr
    时,它将恢复到原始状态
  • 因此,如果您点击
    X
    不应破坏布局。 并点击一个
    返回
    按钮,应恢复原来的布局

    (尝试从下到上删除所有行,然后从右到左恢复它们,这是所需的流)


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

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

    对于那些只想看到有效解决方案的人,

    更新:我更改了可视列计算方法,以迭代表格并创建二维数组,以查看使用jQuery
    offset()方法的旧方法。代码较短,但时间成本较高

    问题的存在是因为当我们隐藏一行时,虽然我们希望所有单元格都被隐藏,但我们希望伪单元格(即,由于cells
    rowspan
    属性而出现在以下行中的单元格)保持不变。为了避免这种情况,每当我们遇到一个隐藏的单元格有
    rowspan
    ,我们都会尝试将其向下移动到下一个可见的行(在移动过程中逐渐减小它的
    rowspan
    值)。使用原始单元格或其克隆,然后对包含伪单元格的每一行再次向下迭代该表,如果该行被隐藏,则再次减小
    行span
    。(要了解原因,请查看工作示例,并注意当蓝色行被隐藏时,红细胞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);
          }
        });
      });
    }
    
    然后只需在页面加载时应用此选项:

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

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

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

    这是我得到的

    我添加了两个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>
    
    
    A.
    B
    C
    D
    E
    F
    G
    H
    我
    J
    K
    L
    M
    N
    o
    P
    Q
    R
    s
    T
    U
    v
    W
    x
    Y
    
    第一条规则就是把桌子的上边缘保持在同一个地方。第二条规则是通过与背景混合使单元格显示为空白,因此相应地进行更改

    最后是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();
    }
    
    $(函数(){
    //第一表()
    var myTb2=新的dynamicTable();
    myTb2.createFromElement($(“#动态#表2”);
    myTb2.drawTable()
    $(窗口).on(“tr_hide”,函数(e,数据){
    var tbl=data.ctx,
    rowIndex=data.idx;
    tbl.hideRow.call(tbl,行索引);
    })
    $(窗口)。打开(“tr_show”,函数(e,数据){
    var tbl=data.ctx,
    rowIndex=data.idx;
    tbl.showRow.call(tbl,行索引);
    })
    })
    函数dynamicTableItem(){
    this.height=null;
    this.content=null;
    }
    函数dynamicTableRow(){
    this.color=null;
    this.items=[]
    this.show=true
    this.setNumColumns=函数(numCols){
    对于(var i=0;i=this.items.length){console.error(“索引超出范围”,index);}
    此项。项[索引]=项;
    }
    }
    函数dynamicTable(){
    this.element=null;
    this.numCols=null;
    this.rows=[]
    this.addRow=函数(颜色){
    var row=新的dynamicTableRow();
    row.color=颜色;
    row.setNumColumns(this.numCols)
    var length=this.rows.push(行)
    返回此值。行[length-1]
    }
    this.drawTable=函数(){
    this.element.empty()
    var cols=[],
    行元素=[];
    对于(var i=0;i工作解决方案-
    这与我之前介绍的解决方案基本相同,我只是改变了获取列索引的方式,以消除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);
        }
      });   
    
    }
    
    更新 根据这一点,确保可以使用

      $('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所说,这个问题要解决的主要问题是在合并隐藏行和可见行时计算单元格的位置

    我试图弄清楚浏览器是如何实现这种功能的,以及如何使用这种功能的

     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;
        }
      }
    }
    
    函数getColumnVisiblePostion($firstRow,$cell){
    var tdsFirstRow=$firstRow.children();
    对于(变量i=0;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);
    }
    
    $(文档).ready(函数(){
    添加\删除\按钮();
    $(窗口).on(“tr_走了”,函数(e,tr){
    添加返回按钮(tr);
    });
    //初始化单元格数据
    $('td')。每个(函数(){
    $(this.addClass($(this.parent().attr('color_class'));
    $(this.data('posx',$(this.position().left));
    if($(this.attr('rowspan')){
    $(this.data(“rowspan”),$(this.attr(“rowspan”);
    }
    });
    });
    函数calculate_max_rowspands(){
    //移除所有临时单元格
    $(“.tmp”).remove();
    //获取所有行
    var trs=$('tr'),tds,tdsTarget,
    $tr、$trTarget、$td、$trFirst、,
    cellPos,cellTargetPos,i;
    //获取第一行,这是布局参考
    $trFirst=$('tr').first();
    //遍历所有行
    对于(var rowIdx=0;rowIdx 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;
        }
      }
    }
    
    $(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);
    }