Hiding table columns with javascript - dynamically adjust layout - javascript

As part of my responsive design, I'm trying to dynamically hide columns of a table if the viewport size shrinks past a certain point. I've tried to set
.style.visibility = "collapse" for all <tr> elements and .style.opacity = "0" for all <td> elements. I then have to hide the background of the table so that it doesn't show that the table width is still there while also increasing the width of the table (160%) so that the remaining columns fill the screen.
While this actually works on Chrome, Firefox, IE (including ie8!) and my mobile browsers, it seems like a ridiculous kludgy hack. Any other suggestions?
var jmq = window.matchMedia("screen and (max-width: 610px)");
jmq.addListener(jmqListener);
function jmqListener(jmq){
var colTitle = getElementsByClassName('col-title');
var colName = getElementsByClassName('col-name');
var colDate = getElementsByClassName('col-date');
var colFb = getElementsByClassName('col-fb');
var table = getElementsByClassName('default');
if (jmq.matches || window.innerWidth < 611 ) {
//Mobile
... resize controls
// hide table columns
if (colName !== null){
for(var i=0,j=colName.length; i<j; i++){
colName[i].style.visibility = "collapse";
colName[i].style.opacity = "0";
}
}
// HACK - increase table width and hide the background which would show the reserved table space
if (table !== null){
for(var i=0,j=table.length; i<j; i++){
table[i].style.width = "160%";
table[i].style.background = "transparent";
}
}
}
else {
// Desktop
... restore control layout for desktop
// restore table column(s)
if (colName !== null){
for(var i=0,j=colName.length; i<j; i++){
colName[i].style.visibility = "visible";
colName[i].style.opacity = "100";
}
}
if (table !== null){
for(var i=0,j=table.length; i<j; i++){
table[i].style.width = "100%";
table[i].style.background = "#C8C8C8";
}
}
}
}
function getElementsByClassName(className) {
if (document.getElementsByClassName) {
return document.getElementsByClassName(className); }
else {
return document.querySelectorAll('.' + className);
}
}

I would change your for loop:
for(var i=0; i<colName.length; i++){
colName[i].style.display = "none";
}
Note your j variable is completely superfluous.
In the MDN docs for visibility you will find:
collapse
For table rows, columns, column groups, and row groups the row(s) or column(s) are hidden and the space they would have occupied is removed (as if display: none were applied to the column/row of the table). However, the size of other rows and columns is still calculated as though the cells in the collapsed row(s) or column(s) are present. This was designed for fast removal of a row/column from a table without having to recalculate widths and heights for every portion of the table.
emphasis mine
The solution above is essentially to use display: none

Try using CSS3 Media Queries.
E.g.
#media all and (max-width: 700px) {
table, tr, td {
display: none;
}
When the width of your devices view-port is less than or equal to 700px the above display: none; will be used to hide your table, tr, td, or what have you.

Related

How can I implement collapsing rows the way datatables.js does?

I'd like to implement the data-tables responsive behavior of hiding columns when the table's width decreases past a certain point (basically the point when the tr's element overflow would occur).
I know how I can collapse the rows: simply on window resize, check for when the width of the table is greater than the width of it's container #table-wrapper. When this happens, I hide the outer most table column and place it in a stack, while adding those values to each rows extended portion (that will be toggled to be visible, the way datatables does).
If the website is accessed while the window is in a small size, upon loading the table can check for the overflow condition (table.width > table-wrapper.width), and iterate through the outer most columns, hiding them and pushing them on to the stack until the overflow condition is false.
However, How can I bring the elements back? That is, when the table is growing, I'm not sure under what condition I can pop the columns off the stack and unhide them.
I'm thinking of getting the minimum-size of the table somehow, and upon window resize, check if the wrappers width is bigger than the minimum size of the table plus the minimum size of first item on the stack? However, I don't know how to get these minimum widths.
Is there a reliable way of getting these min.widths for any font size, or is there a better way you can recommend?
<table class="test">
<tbody>
<tr>
<th> heading1 </th>
<th> heading2 </th>
<th> heading3 </th>
<th> heading4 </th>
</tr>
<tr>
<td>data1</td>
<td>data2</td>
<td>data3</td>
<td>data4</td>
</tr>
<tr>
<td>data1</td>
<td>data2</td>
<td>data3</td>
<td>data4</td>
</tr>
</tbody>
</table>
*Update:
I thought a very unelegant solution where I could place an invisible copy of the table directly behind it, and upon the window resize event firing, I could continually add an invisible column to the invisible table and check for the overflow condition (table_border > wrapper_border). This seems really inefficient though...
I also learned that min-width for < td > elements are undefined. However, Its possible to place a or inside each element that does have a min-width. For each column, the minimum with is equal to the that has the maximum length. So, I guess I could check all the data for each column and get the minimum length that way. The min. width of the table is what I need to base whether or not adding another column would cause an overflow.
What do you think; do you have any other ideas?
One approach is to have data using white-space: nowrap elements inside the table cells and calculate the min width of each row which it will be the max width of the row data elements. These "break-widths" will be calculated only once when the table is filled with data (on load or re-new the data). Having these "break-widths", we can calculate the table minimum width and compare it with the body client width and hide columns accordingly on window resize.
Calculating the "break-widths"
This is performed on loading or each time table updates with new data (i.e. pagination page change). We go through table cell elements TH and TD and we're storing the biggest cell text width of the span elements.
function calcBreakWidths() {
// Each column one base index
let thIndex = 1;
for (let th of document.querySelectorAll('thead th')) {
// Get the width of every text span of every TH column,
// this way, we'll have also header break-widths
let breakWidth = document.querySelector(`thead th:nth-child(${thIndex}) > span`).offsetWidth;
// Go through all column TD elements and keep the biggest text span width
for (let span of document.querySelectorAll(`tbody td:nth-child(${thIndex}) > span`)) {
if (span.offsetWidth > breakWidth) {
breakWidth = span.offsetWidth;
}
}
// Save the biggest text span break-width to the TH dataset
th.dataset.breakwidth = breakWidth;
// Next column index
thIndex++;
}
}
Hide / Show columns on window resize
On window.resize we calculate the table minimum widths by summing up all break-widths. Then we go through each column, starting from the most right one, and we check if the table min width including the current column is exceeded the body client width; if it did, then we hide the current column else we show the current column. This is done by using classList.add and classList.remove using the class named hidden with display: none CSS style. At the end of each column iteration, we subtract the current column break-width from the table minimum width so to have the next correct table minimum width without the current column break-width.
window.addEventListener('resize', function() {
const bodyWidth = document.body.clientWidth;
// Get all columns break-widths from the TH element dataset
const breakWidths = [...document.querySelectorAll('th')]
.map(th => parseInt(th.dataset.breakwidth));
// Sum-up all column break-widths (+2 pixels) to calculate table minimum width
let tableMinWidth = breakWidths
.reduce((total, breakWidth) => total + breakWidth + 2, 0);
for (let column = breakWidths.length; column > 0; column--) {
const tableIsLarger = tableMinWidth > bodyWidth;
// const th = document.querySelector(`th:nth-child(${column})`);
const cells = document.querySelectorAll(`th:nth-child(${column}), td:nth-child(${column})`);
// If table min width is larger than body client width,
// then hide the current column
if (tableMinWidth > bodyWidth) {
// We're hidding the column by iterating on each table cell
// and add the hidden class only if the column does not already contain
// the hidden class. We're doing this for performance reasons
if (!cells[0].classList.contains('hidden')) {
cells.forEach(cell => cell.classList.add('hidden'));
}
// Else if the table min width is not larger than body client width,
// we remove the hidden class from the column to show each column cell
} else if (cells[0].classList.contains('hidden')) {
cells.forEach(cell => cell.classList.remove('hidden'));
}
// Subtract current column break-width from the total table min width
// so to have the correct min table width for the next column
tableMinWidth -= breakWidths[column - 1] + 2;
}
});
The snippet shows this in action
Please read inline comments
// Each TH class is a field name
const fields = [...document.querySelectorAll('thead th')].map(el => el.className);
// Generate 20 rows with fake data
for (let i = 0; i <= 20; i++) {
const tr = document.createElement('tr');
fields.forEach(field => {
const td = document.createElement('td');
const text = document.createElement('span');
td.className = field;
text.textContent = fake(field);
td.appendChild(text);
tr.appendChild(td);
});
document.querySelector('table tbody').appendChild(tr);
}
// Calculate each column break width, the max data span element width
function calcBreakWidths() {
let thIndex = 1;
for (let th of document.querySelectorAll('thead th')) {
let breakWidth = document.querySelector(`thead th:nth-child(${thIndex}) > span`).offsetWidth;
for (let span of document.querySelectorAll(`tbody td:nth-child(${thIndex}) > span`)) {
if (span.offsetWidth > breakWidth) {
breakWidth = span.offsetWidth;
}
}
th.dataset.breakwidth = breakWidth;
thIndex++;
}
}
calcBreakWidths();
// Handle window resize and hide each column exceeds BODY client width
window.addEventListener('resize', function() {
const bodyWidth = document.body.clientWidth;
// Get the break widths saved to the TH datasets
const breakWidths = [...document.querySelectorAll('th')]
.map(th => parseInt(th.dataset.breakwidth));
// Calculate table min width (+2 pixels for border + padding for each cell)
let tableMinWidth = breakWidths
.reduce((total, breakWidth) => total + breakWidth + 2, 0);
// Loop from last to the first column and compare the widths
for (let column = breakWidths.length; column > 0; column--) {
const cells = document.querySelectorAll(`th:nth-child(${column}), td:nth-child(${column})`);
if (tableMinWidth > bodyWidth) {
if (!cells[0].classList.contains('hidden')) {
cells.forEach(cell => cell.classList.add('hidden'));
}
} else if (cells[0].classList.contains('hidden')) {
cells.forEach(cell => cell.classList.remove('hidden'));
}
tableMinWidth -= breakWidths[column - 1] + 2;
}
});
// Function to create fake data
function fake(what) {
switch (what) {
case 'name': return faker.name.findName();
case 'email': return faker.internet.exampleEmail();
case 'address': return faker.address.streetAddress();
case 'country': return faker.address.country();
case 'account': return faker.finance.accountName();
}
}
table {
border-collapse: collapse;
width: 100%;
}
table, th, td {
border: 1px solid black;
}
tbody td > span,
thead th > span {
white-space: nowrap;
background-color: gold;
}
thead th > span {
background-color: aquamarine;
}
.hidden {
display: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/faker.min.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Faker/3.1.0/locales/en/faker.en.min.js" crossorigin="anonymous"></script>
<table>
<thead>
<tr>
<th class="name"><span>Name<span></th>
<th class="email"><span>Email</span></th>
<th class="account"><span>Personal or Business Account</span></th>
<th class="address"><span>Personal or Business Address</span></th>
<th class="country"><span>Country</span></th>
</tr>
</thead>
<tbody>
</tbody>
</table>
You can run the above code snippet and try to resize the window, but you'll have a better view of the table responsive behavior if you try to resize the browser after opening the snippet in Full page or by navigating to the full example page here: https://zikro.gr/dbg/so/62491859/responsive-table.html
It's not perfect but it runs smoothly on my browser and it hides columns from right to left based on columns data text width.
Could you give an example of the desired behavior? If you're talking about this datatables.js behavior: https://www.datatables.net/examples/styling/semanticui.html, then the simple answer would be this CSS Rule https://www.w3schools.com/cssref/css3_pr_mediaquery.asp
#media only screen and (max-width: 600px) {
table td:nth-child(5) {
display: none;
visibility:hidden;
}
}
#media only screen and (max-width: 700px) {
table td:nth-child(6) {
display: none;
visibility:hidden;
}
}
(you get the idea)

How to set the width of each header cell in a table (with fixed header) to be the same to the biggest in its column, and make the browser respect it?

Here is the infamous table:
My boss wants me, at this time of the year, to make the header of the table fixed, so the user can scroll down the table and continue reading the header. I want to preserve the original precomputed dimensions of the table, I mean, the width that every column has at the moment of its creation (widths aren't established by CSS) and then, adapt the header so its columns match the columns of the body. Following some of the answers I found in Stackoverflow, I started making the header and the body of the table display: block. And after that, I wrote this:
function setTableHeadDimensions() {
var $taskTable = $('.tablaTareas_PEMVISUALIZA'),
$firstRow = $taskTable.find('tbody > tr:first-child'),
$firstRowTds = $firstRow.find('td'),
$firstRowHead = $taskTable.find('thead > tr:first-child'),
$secondRowHead = $taskTable.find('thead > tr:eq(1)'),
$firstRowHeadThs = $firstRowHead.find('th'),
$secondRowHeadThs = $secondRowHead.find('th'),
i = 0,
cells = [];
//We prepare CSS, so we can specify width.
$taskTable
.css('table-layout', 'fixed')
.find('td, th').each(function () {
var $tdh = $(this);
$tdh.css('box-sizing', 'border-box');
$tdh.css('overflow', 'hidden');
});
//Cells of the first row of the table head.
for (i = 0; i < 3; i++) {
cells.push($($firstRowHeadThs[i]));
}
//Cells of the second row of the table head.
for (i = 0; i < $secondRowHeadThs.length; i++) {
cells.push($($secondRowHeadThs[i]));
}
//Rest of the cells for the first row.
for (i = 5; i < $firstRowHeadThs.length; i++) {
cells.push($($firstRowHeadThs[i]));
}
//Try to set the width of the current column's header cell
//to the biggest cell width in the column.
for (i = 0; i < cells.length; i++) {
var maxWidth = 0;
$taskTable.find('td:nth-child(' + (i + 1) + ')').each(function () {
var $el = $(this);
maxWidth = Math.max(maxWidth, $el.width());
});
cells[i].width(maxWidth);
}
}
But, as you can see in the picture, the browser doesn't want to cooperate. What's more, it establishes the width of the cell, but to a number that doesn't match the width of it's corresponding column:
What's more, it doesn't match the width of the row it should match:
So I have two questions:
Why does the browser behave in the way it does?
How can I solve this problem in a way compatible with IE8? (no fancy CSS3 solution, please)
Here's a codepen with the example cut down to the minimum necessary: Codepen example
I solved it. In reality, there were two problems:
The first one is that jQuery.width() returns only the width of the content of the cell, without padding and margin (even if you specify border-sizing: border-box). I found more natural to use jQuery.css('width') and then take borders and padding into account in my calculations, without specifying border-sizing: border-box because retrieving the width with border-sizing: border-box and then setting it in another element with the idea of matching both widths can be error prone (I had problems with it).
The second one is if you use rowspan in the header of the table. In that case, you have to establish the width of the rows envolved doing the proper calculations, not only one of them hopping that the rest of the rows will adapt.
Here's the codepen with the solution: http://codepen.io/PolarKuma/pen/BQXMbO

Setting TH min and max width extremely slow

Given a fixed-header HTML table, I'm trying to line up the header columns with the body row columns. I'm doing this because the CSS I'm using to make the headers fixed results in the headers being out of alignment with the rest of the body.
The javascript I'm using works, but is extremely slow. Any ideas on how I can speed this up?
Here's the fiddle showing the problem. Right now it's taking about 5+ seconds for a relatively small table.
http://jsfiddle.net/w93NU/
Here is the code I'm using:
function fixedHeader($table) {
//This function compares the header row with the first body row and lines up all of the widths
var firstRowTds = $table.children("tbody:first").children("tr:first").children("td");
var headerRowThs = $table.find("th");
for (var i = 0; i < firstRowTds.length; i++) {
var head = headerRowThs[i];
var cell = firstRowTds[i];
var width = (Math.max($(cell).outerWidth(), $(head).outerWidth())) + "px";
//Here are my problem pieces. Setting these values are what kills the perfomanrce
$(cell).css({
"min-width": width,
"max-width": width
});
$(head).css({
"min-width": width,
"max-width": width
});
}
}
Ok, after much trial and error, if you comment out the last item in the style sheet, then it is fast. I don't know why.
Updated fiddle here
/* fixed width for THs */
/* the tbody needs to be 16px less than the thead, for the scrollbar */
/*#readiness-grid tbody td {
width: 242px;
}*/
// changing:
var firstRowTds = $table.children("tbody:first").children("tr:first").children("td");
var headerRowThs = $table.find("th");
// to:
var firstRowTds = $table.children("tbody:first > tr:first > td");
var headerRowThs = $table.find("thead > th");
scopes the node lookup and cuts the time from ~800ms to ~2ms between the start/end times on your sample table.

multiple tables with the same row height using closure

I have two tables, side by side. What I am trying to do is have each row of each table that is in the same container have the same row height per row. I have gotten that far with this.
The way it works is you have an array thay grabs the row heights of each table and uses the largest height for each row. Thats fine except as its a single array that means if there are other containers on the page they will look at the same array. I tried writting a closure function but failed. any ideas?
$(document).ready(function() {
var heights = [];
computeTableHeights(true);
assignTableHeights();
var windowWidth = $(window).width();
$(window).resize(function() {
computeTableHeights(($(window).width() < windowWidth) && ($(window).width() != windowWidth));
windowWidth = $(window).width();
assignTableHeights();
})
function computeTableHeights(recordBiggestHeights) {
$("table").each(function() {
var rowIndex = 0;
var rows = $(this).find("tr");
$(rows).each(function() {
var rowHeight = $(this).css("height");
if (heights[rowIndex] === undefined) {
heights[rowIndex] = rowHeight;
} else {
var existingHeight = parseInt(heights[rowIndex]);
var currentHeight = parseInt(rowHeight);
if (shouldChangeHeight(recordBiggestHeights, existingHeight, currentHeight)) {
heights[rowIndex] = rowHeight;
}
}
rowIndex++;
});
});
}
function shouldChangeHeight(recordBiggestHeights, existingHeight, currentHeight) {
if (existingHeight == currentHeight) {
return false;
} else if (recordBiggestHeights) {
return existingHeight < currentHeight;
} else {
return existingHeight > currentHeight;
}
}
function assignTableHeights() {
$(".container table").each(function() {
var rowIndex = 0;
var rows = $(this).find("tr");
$(rows).each(function() {
var rowHeight = $(this).css("height");
if (heights[rowIndex]) {
var existingHeight = parseInt(rowHeight);
var targetHeight = parseInt(heights[rowIndex]);
if (existingHeight != targetHeight) {
$(this).css("height", heights[rowIndex]);
}
}
rowIndex++;
});
});
}
});
I think I understand what you're trying to do. If not, please elaborate a little on the requirements you're looking for, so I can revise this answer.
You want to treat the row heights of each container and its child tables separately. Correct me if I'm wrong
The code below loops through each container separately, before equalizing the heights of the table rows.
You are indeed right that storing all row heights for all tables in one array will not get you the results you need. You would need to create an array instance per container.
In your code you read out the css for the height of the row. Once you set the height in the css, this property will stay the same. I believe in your use-case you need the height of the row as the browser has calculated it (jquery offers methods for this purpose).
Therefore, on resizing, the css property should be cleared, before setting it again to the greatest calculated height.
function resizeHandler() {
// Treat each container separately
$(".container").each(function(i, container) {
// Stores the highest rowheight for all tables in this container, per row
var aRowHeights = [];
// Loop through the tables
$(container).find("table").each(function(indx, table) {
// Loop through the rows of current table (clear their css height value)
$(table).find("tr").css("height", "").each(function(i, tr) {
// If there is already a row height defined
if (aRowHeights[i])
// Replace value with height of current row if current row is higher.
aRowHeights[i] = Math.max(aRowHeights[i], $(tr).height());
else
// Else set it to the height of the current row
aRowHeights[i] = $(tr).height();
});
});
// Loop through the tables in this container separately again
$(container).find("table").each(function(i, table) {
// Set the height of each row to the stored greatest height.
$(table).find("tr").each(function(i, tr) {
$(tr).css("height", aRowHeights[i]);
});
});
});
}
$(document).ready(resizeHandler);
$(window).resize(resizeHandler);
I have this fiddle: http://jsfiddle.net/k5g87/
You can resize the result window in there.

Script that makes all floating divs the same height

Hey there, I have 20 divs floated left with different height. I use this script to resize them. It worked perfect when my website was designed using pixels.
When i have changed my website to % design (percentage design), the script stopped working that reliable, sometimes it does not resize.
can you take a look, see if there are any adjustments needed for liquid layouts?
maybe it's the way i call the script?
Ty very much
Here it is:
var currentTallest = 0;
var currentRowStart = 0;
var rowDivs = new Array();
function setConformingHeight(el, newHeight) {
// set the height to something new, but remember the original height in case things change
el.data("originalHeight", (el.data("originalHeight") == undefined) ? (el.height()) : (el.data("originalHeight")));
el.height(newHeight);
}
function getOriginalHeight(el) {
// if the height has changed, send the originalHeight
return (el.data("originalHeight") == undefined) ? (el.height()) : (el.data("originalHeight"));
}
function columnConform() {
// find the tallest DIV in the row, and set the heights of all of the DIVs to match it.
$('div.column').each(function(index) {
if(currentRowStart != $(this).position().top) {
// we just came to a new row. Set all the heights on the completed row
for(currentDiv = 0 ; currentDiv < rowDivs.length ; currentDiv++) setConformingHeight(rowDivs[currentDiv], currentTallest);
// set the variables for the new row
rowDivs.length = 0; // empty the array
currentRowStart = $(this).position().top;
currentTallest = getOriginalHeight($(this));
rowDivs.push($(this));
} else {
// another div on the current row. Add it to the list and check if it's taller
rowDivs.push($(this));
currentTallest = (currentTallest < getOriginalHeight($(this))) ? (getOriginalHeight($(this))) : (currentTallest);
}
// do the last row
for(currentDiv = 0 ; currentDiv < rowDivs.length ; currentDiv++) setConformingHeight(rowDivs[currentDiv], currentTallest);
});
}
$(window).resize(function() {
columnConform();
});
$(document).ready(function() {
columnConform();
});
Well if you change it to fluid layout (% design) then you are going to have to add a window resize listener, basically when the resize event is done or while it's running you need to recall the script so it can recalculate with new dimensions, you did not need to doo that with pixels because it was a fixed size and once assigned will not change no matter how many times you resize the actual screen.
If you use styles like this:
<style>
.parent{
background:#F00;height:300px
}
.parent div{
float:left;height:100%;width:33.33%;
}
</style>
<div class="parent">
<div style="background:#FED"></div>
<div style="background:#EDF"></div>
<div style="background:#DFE"></div>
</div>
You just have to set the height of the parent div, and the width of the children div

Categories

Resources