Search code examples
javascripthtmldatatablesbootstrap-5

Bootstrap 5 popover in responsive JS DataTable not working properly


I am building a hobby web-page using Bootstrap 5.3.3 and JS DataTables 2.0.5. On the page, I have a table with some players data. In the table i made it so when clicking on some cells a popover is shown with extra info. All the required modules are there and popover is working fine when table is shown at full width. However, if i adjust screen size so some of the columns become collapsed, the popover is not showing for those columns.

Repro example in JS Fiddle:

<head>
  <meta charset="utf-8">
  <title>Responsive Table Popover</title>
  <meta name="description" content="">
  <meta name="author" content="">
  <meta name="viewport" content="width=device-width,initial-scale=1">
  <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
  <link href="https://cdn.datatables.net/2.0.5/css/dataTables.bootstrap5.min.css" rel="stylesheet">
  <link href="https://cdn.datatables.net/responsive/3.0.2/css/responsive.bootstrap5.min.css" rel="stylesheet">
</head>
<body>
  <div class="container">
    <table id="players-data" class="table table-striped" style="font-size:13px; width: 100%;">
        <thead style="white-space: nowrap;">
            <tr>
                <th>Player</th>
                <th>Details 1</th>
                <th>Details 2</th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    </div>
    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"> </script>
    <script charset="utf8" src="https://cdn.datatables.net/2.0.5/js/dataTables.min.js"></script>
    <script charset="utf8" src="https://cdn.datatables.net/2.0.5/js/dataTables.bootstrap5.min.js"></script>
    <script charset="utf8" src="https://cdn.datatables.net/responsive/3.0.2/js/dataTables.responsive.min.js"></script>
    <script charset="utf8" src="https://cdn.datatables.net/responsive/3.0.2/js/responsive.bootstrap5.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
    <script type="text/javascript">
      let players = [
        {player: 'Best', details_1: 'Cool', details_2: 'Sharp', details_1_extra: 'Extra Cool', details_2_extra: 'Extra Sharp'},
        {player: 'Worst', details_1: 'Hot', details_2: 'Slow', details_1_extra: 'Extra Hot', details_2_extra: 'Extra Slow'}
      ];

      $('#players-data').DataTable({
        data: players,
        columns: [
            {data: 'player', render: DataTable.render.text(), sortable: true},
            {data: 'details_1', sortable: false, class: "text-center", render: function (data, type, row) {
                if (data)
                {
                    return `<a 
                        tabindex="0" 
                        type="button" 
                        class="btn btn-link btn-sm" 
                        data-bs-container="body" 
                        data-bs-trigger="focus" 
                        data-bs-toggle="popover" 
                        data-bs-placement="bottom" 
                        data-bs-html="true" 
                        data-bs-custom-class="custom-popover"
                        data-bs-content="${row.details_1_extra}">
                        ${data}
                    </a>`;
                } else {
                    return data;
                }
            }},
            {data: 'details_2', sortable: false, class: "text-center", render: function (data, type, row) {
                if (data)
                {
                    return `<a 
                        tabindex="0" 
                        type="button" 
                        class="btn btn-link btn-sm" 
                        data-bs-container="body" 
                        data-bs-trigger="focus" 
                        data-bs-toggle="popover" 
                        data-bs-placement="bottom" 
                        data-bs-html="true" 
                        data-bs-custom-class="custom-popover" 
                        data-bs-content="${row.details_2_extra}">
                        ${data}
                    </a>`;
                } else {
                    return data;
                }
            }}
        ],
        destroy: true,
        searching: false, 
        paging: true, 
        info: false,
        ordering: true,
        stateSave: true,
        responsive: true,
        drawCallback: function (settings) {
            const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
            const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
        }
    });
    </script>
</body>

When table is full width (all columns are shown) popovers on both columns are working fine: enter image description here

When "Details 2" column is collaplsed due to screen size, popover is no longer shown on it no matter if you click on cell text:

enter image description here

What am I missing here in my code to make popovers work no matter what?


Solution

  • When datatable is resized, and when uncollapsing collapsed cells, datatable actually adds another row, which contains collapsed cells, but in ul/li/span element, i.e. it creates new DOM elements, which don't have popover or any other listener attached.

    example of newly added elements:

    <tr data-dt-row="1" class="child">
      <td class="child" colspan="1">
        <ul data-dtr-index="1" class="dtr-details">
          <li class=" text-center" data-dtr-index="1" data-dt-row="1" data-dt-column="1">
            <span class="dtr-title">Details 1</span> <span class="dtr-data"><a tabindex="0" type="button" class="btn btn-link btn-sm" data-bs-container="body" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-html="true" data-bs-custom-class="custom-popover" data-bs-content="Extra Hot">
                            Hot
                        </a></span></li>
          <li class=" text-center" data-dtr-index="2" data-dt-row="1" data-dt-column="2"><span class="dtr-title">Details 2</span> <span class="dtr-data"><a tabindex="0" type="button" class="btn btn-link btn-sm" data-bs-container="body" data-bs-trigger="focus" data-bs-toggle="popover" data-bs-placement="bottom" data-bs-html="true" data-bs-custom-class="custom-popover" data-bs-content="Extra Slow">
                            Slow
                        </a></span></li>
        </ul>
      </td>
    </tr>

    So, what you need to do is when uncollapsing hidden cells, i.e. when datatable adds those elements, find and initiate popover on those newly added elements.

    This could be done by adding separate listener on the whole table, or more precisely on collapsing element which has .dtr-control class, and then, find newly added row (which is control's parent's next sibling with class .child), and in it find all newly added popovers, and initiate them:

    $('#players-data').on('click', 'td.dtr-control',  function (evt) {
    
    const popovers = $(this).parent().next('.child').find('[data-bs-toggle="popover"]')
    .each((i,el)=>new bootstrap.Popover(el));
    
    });
    

    demo:

    <head>
      <meta charset="utf-8">
      <title>Responsive Table Popover</title>
      <meta name="description" content="">
      <meta name="author" content="">
      <meta name="viewport" content="width=device-width,initial-scale=1">
      <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
      <link href="https://cdn.datatables.net/2.0.5/css/dataTables.bootstrap5.min.css" rel="stylesheet">
      <link href="https://cdn.datatables.net/responsive/3.0.2/css/responsive.bootstrap5.min.css" rel="stylesheet">
    </head>
    
    <body>
      <div class="container">
        <table id="players-data" class="table table-striped" style="font-size:13px; width: 100%;">
          <thead style="white-space: nowrap;">
            <tr>
              <th>Player</th>
              <th>Details 1</th>
              <th>Details 2</th>
            </tr>
          </thead>
          <tbody>
          </tbody>
        </table>
      </div>
      <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous">
      </script>
      <script charset="utf8" src="https://cdn.datatables.net/2.0.5/js/dataTables.min.js"></script>
      <script charset="utf8" src="https://cdn.datatables.net/2.0.5/js/dataTables.bootstrap5.min.js"></script>
      <script charset="utf8" src="https://cdn.datatables.net/responsive/3.0.2/js/dataTables.responsive.min.js"></script>
      <script charset="utf8" src="https://cdn.datatables.net/responsive/3.0.2/js/responsive.bootstrap5.min.js"></script>
      <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
      <script type="text/javascript">
        let players = [{
            player: 'Best',
            details_1: 'Cool',
            details_2: 'Sharp',
            details_1_extra: 'Extra Cool',
            details_2_extra: 'Extra Sharp'
          },
          {
            player: 'Worst',
            details_1: 'Hot',
            details_2: 'Slow',
            details_1_extra: 'Extra Hot',
            details_2_extra: 'Extra Slow'
          }
        ];
    
        $('#players-data').DataTable({
          data: players,
          columns: [{
              data: 'player',
              render: DataTable.render.text(),
              sortable: true
            },
            {
              data: 'details_1',
              sortable: false,
              class: "text-center",
              render: function(data, type, row) {
                if (data) {
                  return `<a 
                            tabindex="0" 
                            type="button" 
                            class="btn btn-link btn-sm" 
                            data-bs-container="body" 
                            data-bs-trigger="focus" 
                            data-bs-toggle="popover" 
                            data-bs-placement="bottom" 
                            data-bs-html="true" 
                            data-bs-custom-class="custom-popover"
                            data-bs-content="${row.details_1_extra}">
                            ${data}
                        </a>`;
                } else {
                  return data;
                }
              }
            },
            {
              data: 'details_2',
              sortable: false,
              class: "text-center",
              render: function(data, type, row) {
                if (data) {
                  return `<a 
                            tabindex="0" 
                            type="button" 
                            class="btn btn-link btn-sm" 
                            data-bs-container="body" 
                            data-bs-trigger="focus" 
                            data-bs-toggle="popover" 
                            data-bs-placement="bottom" 
                            data-bs-html="true" 
                            data-bs-custom-class="custom-popover" 
                            data-bs-content="${row.details_2_extra}">
                            ${data}
                        </a>`;
                } else {
                  return data;
                }
              }
            }
          ],
          destroy: true,
          searching: false,
          paging: true,
          info: false,
          ordering: true,
          stateSave: true,
          responsive: true,
          drawCallback: function(settings) {
            const popoverTriggerList = document.querySelectorAll('[data-bs-toggle="popover"]');
            const popoverList = [...popoverTriggerList].map(popoverTriggerEl => new bootstrap.Popover(popoverTriggerEl));
          }
        });
    
    
        $('#players-data').on('click', 'td.dtr-control', function(evt) {
    
          const popovers = $(this).parent().next('.child').find('[data-bs-toggle="popover"]')
            .each((i, el) => new bootstrap.Popover(el));
    
        });
      </script>
    </body>