Search code examples
javascripthtmlhtml-table

Filter text in a table with merged cells with "rowspan" or "colspan"


I have an HTML code, and I want the filterTable() function to filter the myTable table after typing something in the searchInput.

For example, if I type "Rafael," the relevant row should be fully displayed with all its merged cells, and the rest that do not contain this word should be hidden.

My code:

function filterTable() {
    var input = document.getElementById("searchInput");
    var filter = input.value;
    var table = document.getElementById("myTable");
    var rows = table.getElementsByTagName("tr");

    for (var i = 1; i < rows.length; i++) {
        rows[i].style.display = "none";
    }

    for (var i = 1; i < rows.length; i++) {
        var row = rows[i];
        var cells = row.getElementsByTagName("td");
        var rowMatchesFilter = false;

        for (var j = 0; j < cells.length; j++) {
            var cell = cells[j];
            var cellContent = cell.textContent;

            if (cellContent.indexOf(filter) > -1) {
                rowMatchesFilter = true;

                if (cell.getAttribute("rowspan")) {
                    var rowspan = parseInt(cell.getAttribute("rowspan"));
                    for (var k = 0; k < rowspan; k++) {
                        rows[i + k].style.display = "";
                    }
                } else {
                    row.style.display = "";
                }
                break;
            }
        }
        if (rowMatchesFilter) {
            row.style.display = "";
        }
    }
}
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Table Search</title>
    <style>
        table, th, td {
            border: 1px solid black;
            border-collapse: collapse;
            padding: 8px;
            width: 500px;
        }
    </style>
</head>
<body>

<h2>Information Table</h2>

<input type="text" id="searchInput" placeholder="Enter search text" oninput="filterTable()">

<table id="myTable">
    <thead>
        <tr>
            <th>Building</th>
            <th>Floor</th>
            <th>Name</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td rowspan="5">Building 1</td>
            <td rowspan="2">Floor 1</td>
            <td>Martin</td>
        </tr>
        <tr>
            <td>Cristiano</td>
        </tr>
        <tr>
            <td rowspan="3">Floor 2</td>
            <td>Karim</td>
        </tr>
        <tr>
            <td>Rafael</td>
        </tr>
        <tr>
            <td>Anna</td>
        </tr>
        <tr>
            <td>Building 2</td>
            <td>Floor 1</td>
            <td>Carlos</td>
        </tr>
    </tbody>
</table>
</body>
</html>

However, when I type "Rafael", it filters, but shows:

Building Floor Name
Rafael

But I want to show:

Building Floor Name
Building 1 Floor 2 Rafael

Solution

  • I would use a different strategy, by using collision detection :)

    • Filter the TDs which content matches the search string
    • Create an Array of areas where x and width are the parent TD's TR element rects, and y and height are that TD elements' rects (since taller than TR when using rowspan)
    • Once you have those areas create a unique Set() of TD elements that collide with those areas
    • Filter that unique set of cells

    // DOM utility functions:
    const el = (sel, par = document) => par.querySelector(sel);
    const els = (sel, par = document) => par.querySelectorAll(sel);
    
    // Utils
    const regEsc = (v) => v.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&');
    const collides = (a, b) =>  a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
    
    // Task: Filter table
    const elSearch = el("#searchInput");
    const elTbody = el("#myTable");
         
    const filterTable = (value) => {
      const val = elSearch.value.trim();
      const elsTd = els("td", elTbody);
      const reg = new RegExp(regEsc(val), "i");
      const matchTd = [...elsTd].filter(elTd => reg.test(elTd.textContent));  
      const areas = matchTd.map((elTd) => {
        const elTr = elTd.closest("tr");
        const trBCR = elTr.getBoundingClientRect();
        const tdBCR = elTd.getBoundingClientRect();
        return {
          x: trBCR.x,
          y: tdBCR.y,
          width: trBCR.width,
          height: tdBCR.height,
        }
      });
      
      const filteredTds = areas.reduce((acc, area) => {
        elsTd.forEach((elTd) => collides(area, elTd.getBoundingClientRect()) && acc.add(elTd));
        return acc;
      }, new Set());
      
      elsTd.forEach(elTd => {
        elTd.classList.toggle("hidden", val.length && !filteredTds.has(elTd));
      });
    };
    
    elSearch.addEventListener("input", filterTable);
    table {
      inline-size: 100%;
      border-collapse: collapse;
      
      th,
      td {
        border: 1px solid black;
        
        &.hidden {
          display: none;
        }
      }
    }
    <input type="text" id="searchInput" autocomplete=off placeholder="Search...">
    
    <table id="myTable">
      <thead>
        <tr><th>Building</th><th>Floor</th><th>Name</th></tr>
      </thead>
      <tbody>
        <tr><td rowspan="5">Building 1</td><td rowspan="2">Floor 1</td><td>Martin</td></tr>
        <tr><td>Cristiano</td></tr>
        <tr><td rowspan="3">Floor 2</td><td>Karim</td></tr>
        <tr><td>Rafael</td></tr>
        <tr><td>Anna</td></tr>
        <tr><td>Building 2</td><td>Floor 1</td><td>Carlos</td></tr>
    </table>