I am creating a table with sticky columns using css position: sticky. I want to style the columns differently when they are "stuck". My first project involves styling the first column or .wdtscroll td:nth-child(1) when the second column partially slides over it.
Here is the javascript
const stickyElm = document.querySelector('.wdtscroll td')
const observer = new IntersectionObserver(
([e]) => e.target.classList.toggle('isSticky', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(stickyElm)
Here is the jsfiddle: https://jsfiddle.net/g421kjcx/
While it is certainly not perfect, I have accomplished this by setting its left position at -1px so as soon as you start scrolling the table horizontally, it is styled. As you can see this is working but only for the top cell.
When I use this code on my website, it does not work and I get a warning stating:
"TypeError: Argument 1 of IntersectionObserver.observe is not an object."
When I looked it up, it seems Object.observe is obsolete.
How would I go about using this javascript without using Object.observe, and how can I target all td's in the first column.
Bonus question: How would I style the second column or .wdtscroll td:nth-child(2) when it is stuck, even though it will never scroll past the viewport.
Thank you!
Here is the jsfiddle: https://jsfiddle.net/g421kjcx/
Here is my response to your fiddle: https://jsfiddle.net/5nfb2qdy/4/
I answered a range of questions you put in this post:
As you can see this is working but only for the top cell.
...how can I "target" all td's in the first column.
1) the target of the observer is a single element (in this case). This means you can't rely on it for styling multiple elements.
Instead, do it this way:
([e]) => {
let all_td = document.querySelectorAll('.wdtscroll td:first-child')
all_td.forEach(entry =>
entry.classList.toggle('isSticky', e.intersectionRatio < 1)
)
}
When I use this code on my website, it does not work and I get a warning stating: "TypeError: Argument 1 of IntersectionObserver.observe is not an object."
2) This is most likely because the JavaScript is running BEFORE the elements referred to even exist on the page. If this block of code is in the <head></head> part of the page, only a slight revision is needed to make it work:
window.onload = function(){
observer.observe(stickyElm)
}
By wrapping the observer activation in the onload, it won't run before the page finishes rendering. The other option is to move all of this JavaScript to the end of the page just before your </body></html>
Bonus question: How would I style the second column or .wdtscroll td:nth-child(2) when it is stuck, even though it will never scroll past the viewport.
3) Maybe like this?
([e]) => {
let all_td = document.querySelectorAll('.wdtscroll td:nth-child(1)')
let all_2nd_td = document.querySelectorAll('.wdtscroll td:nth-child(2)')
all_td.forEach(entry =>
entry.classList.toggle('isSticky', e.intersectionRatio < 1)
)
all_2nd_td.forEach(entry =>
entry.classList.toggle('isSticky', e.intersectionRatio < 1)
)
}
Also add this to the end of the CSS:
.wdtscroll tr td:nth-child(2).isSticky {
background-color: pink;
}
4) Not a response to any of your questions but I noticed some problems with your CSS in general. Here are the things I changed:
CSS
/* added td to the selectors of the next 2 rules */
.wdtscroll tr:nth-child(even) td { background-color: #f2f2f2; }
.wdtscroll tr:hover td { background-color: #ddd; }
/* added tr to the selector list to override the background color set above */
.wdtscroll tr td.isSticky{
background-color: salmon;
}
5) Lastly, a critique of the approach used to do the class assignments on the elements. You may have good reason to assign class attributes on each td of each tr. This can also be achieved more simply by assigning class attributes to the table itself with rules that apply styles to td:nth-child(1) and td:nth-child(2) with only 2 CSS rules. This would eliminate the need to loop through every row of the table in the JavaScript and leverage the feature of CSS to style bulk elements.
CSS:
.wdtscroll.isSticky tr td:nth-child(1) {
background-color: salmon;
}
.wdtscroll.isSticky tr td:nth-child(2) {
background-color: pink;
}
JavaScript:
// get the sticky element
const stickyElm = document.querySelector('.wdtscroll td')
const tableElm = document.querySelector('.wdtscroll')
const observer = new IntersectionObserver(
([e]) => {
tableElm.classList.toggle('isSticky', e.intersectionRatio < 1)
},
{threshold: [1]}
);
window.onload = function(){
observer.observe(stickyElm)
}
How's that for a nice, neat, and tidy solution? :)
Final fiddle: https://jsfiddle.net/5nfb2qdy/5/
Related
I have multiple divs that when clicked adds a border and scales them up a little. I am looping through all elements using foreach and on click i remove every element's border and scale property except the clicked element, to which i add a border and scale.
My code is completely logical and is supposed to work but for some reason i cant seem to grasp, it only applies the styles to clicked elements but not removing from the rest of the elements (like my code says it should).
JS
document.querySelectorAll('.projcolorpick div').forEach(el => {
el.onclick = (e) => {
el.style.border = "none"
el.style.transform = "scale(1)"
e.target.style.border = "2px solid #fff"
e.target.style.transform = "scale(1.2)"
projcolor = e.target.style.background
}
})
}
give something like this a try... each element needs an id attribute for this to work (the filter part - if there is a unique attribute...)
const list = Array.from(document.querySelectorAll('.projcolorpick div'));
list.forEach(el => {
el.addEventListener('click', (e) => {
//code that affects the element you click on
el.style.border = "2px solid #fff"
el.style.transform = "scale(1.2)"
projcolor = e.target.style.background;
list.filter(x=>x.id!=el.id).forEach(otherEl=>{
//code that affects the other elements you didn't click on
otherEl.style.border = "none"
otherEl.style.transform = "scale(1)"
});
});
});
```
edit:
fixed some typos.
forEach only applies to Arrays unless you configure it otherwise.
querySelectorAll does not return arrays, but array-like objects (NodeLists)
To allow looping over NodeLists, add the following code:
if (window.NodeList && !NodeList.prototype.forEach) {
NodeList.prototype.forEach = Array.prototype.forEach;
}
var nL = document.querySelectorAll('*');
console.log(nL instanceof NodeList); // true
You don't really need an id attribute on each div and I would advocate using class-assignments instead of changing their individual attributes. You can compare the actual DOM elements with each other like c==ev.target, as you can see in my code below:
// === populate the page first ... ============================= START =
const cont=document.getElementById('container');
cont.innerHTML=
[...Array(3)].map(cp=>'<div class="projcolorpick">'+
[...Array(8)].map(d=>{
let hsl= "hsl("+Math.floor(Math.random()*360)+",100%,80%)";
return ' <div style="background-color:'+hsl+'">'+hsl+'</div>'}).join('\n')
+'</div>').join('\n');
// === populate the page first ... =============================== END =
// now, do the action:
cont.onclick=ev=>{
if ( ev.target.parentNode.classList.contains('projcolorpick')
&& ev.target.tagName=='DIV'){
[...ev.target.parentNode.children].forEach(c=>c.classList.toggle('selected',c==ev.target));
ev.target.parentNode.style.backgroundColor=ev.target.textContent;
}
}
.projcolorpick {border: 2px solid #888}
.selected {border: 2px solid #fff; transform:scale(1.2);}
div {margin:6px; padding:4px}
.projcolorpick div {width:200px; height:20px}
<div id="container"></div>
The action happens here:
cont.onclick=ev=>{
if ( ev.target.parentNode.classList.contains('projcolorpick')
&& ev.target.tagName=='DIV'){
[...ev.target.parentNode.children].forEach(c=>c.classList.toggle('selected',c==ev.target));
ev.target.parentNode.style.backgroundColor=ev.target.textContent;
}
}
I use a delegated event-attachment to the parent .container div. The first if statements makes sure that only clicks on .projcolorpick>div elements are processed.
If you want to include more than one generation between them you need to use something like ev.target.closest('.projcolorpick') instead ...
Now, inside the if block two things happen:
Using toggle() on all DOM elements in ev.target.parentNode.children the class "selected" is either
assigned or
removed.
The text found in the clicked div is applied as background-color to the parent .projcolorpick container.
I have created a table to display JSON data via a for loop in a function. Because I have created the table this way, it has no ID/Class.
I have hidden the last three columns of the table in CSS via the following method, where (n) is the column number:
#divIdName table tr td:nth-child(n) {
display: none; }
#divIdName table th:nth-child(n) {
display: none; }
I am trying to display them via three javascript functions, using queryselector but directly coping the CSS i.e.
function showColumnN () {
if (ArrayName.indexOf("StringName")>-1) {
var colNd = document.querySelector("td:nth-child(n)");
var colNh = document.querySelector("th:nth-child(n)");
colNd.style.display = "block";
colNh.style.display = "block"; }
However only one of the hidden columns is displayed, and it contains just the three headings (one on top of another) and first row data (one on top of another) from each of the three.
Does anyone know where I'm going wrong and how I can get the full columns to display?
EDIT: I omitted that there was a conditional in the showColumnN function, to check whether a particular string is in a particular array and proceed with the column unveiling if this were so.
However only one of the hidden columns is displayed
That's because you've only selected the first td and th matching those selectors, but there are (presumably) multiple tds that match (one per row).
To keep going the way you're going (but don't, keep reading), you'd need to loop through those:
function showColumnN(n) {
showAll(document.querySelectorAll("td:nth-child(" + n + ")"));
showAll(document.querySelectorAll("th:nth-child(" + n + ")"));
}
function showAll(list) {
Array.prototype.forEach.call(list, function(element) {
element.style.display = "block";
});
}
However, I'd probably use a CSS solution instead where you could add classes to the table that would show those columns:
table.show1 tr > th:nth-child(1), table.show1 tr > td:nth-child(1) {
display: block;
}
table.show2 tr > th:nth-child(2), table.show2 tr > td:nth-child(2) {
display: block;
}
...and so on. Then showing column 2 (for instance) is:
document.querySelector("selector-for-the-table").classList.add("show2");
(Or better yet, use hideX classes that hide them, and only add those as appropriate. Then you don't have to do the block thing.)
Side note: The default display for td and th isn't block, it's table-cell.
You could do:
var table=document.getElementById("divIdName");
var rows=table.getElementsByTagName("tr");
rows.forEach((row)=>{
elems=row.getElementsByTagName("td");
for(i=0;i<4;i++){
elems[elems.length-4+i].style.display="block";
}
});
However T. J. Crowders answer is much shorter...
I'm using Bootstrap and have a striped table that can be filtered by selecting some options on a form. Javascript interprets the form inputs, and hides rows from the table that don't match the selected criteria.
However, this breaks the table striping on the table depending on which rows are hidden (gray rows next to gray rows, white rows next white rows).
I'd like to reapply the striping based on what rows are visible after filtering the results. How can I do this?
Using .remove() on the table rows is not an option, because I may need to show them again if the filter criteria changes and I'm trying to avoid using AJAX to update the table dynamically based on the filter inputs (I'd like to stick to hiding DOM elements).
Any help is appreciated! I can clarify the question if needed :)
Seems like Bootstrap 4 have a different implementation. Following #Anthony's answer, this is how it would work:
$("tr:visible").each(function (index) {
$(this).css("background-color", !!(index & 1)? "rgba(0,0,0,.05)" : "rgba(0,0,0,0)");
});
Tables are now striped by pure CSS and not by adding the "stripe" class name.
Yes, this is definitely one of the annoying parts of table striping. The better part of valor here is probably just to reapply the striping with jQuery after each update:
$("tr:not(.hidden)").each(function (index) {
$(this).toggleClass("stripe", !!(index & 1));
});
Anthony's answer did not work for me. First, it does not hide the Bootstrap table class table-striped, and second, there is not (or at least does not appear to be) a built-in class stripe for table rows.
Here's my approach, where I've filtered rows in a table with an id of "reports".
Here's a version to use if you define the class "stripe" for <tr> elements:
// un-stripe table, since bootstrap striping doesn't work for filtered rows
$("table#reports").removeClass("table-striped");
// now add stripes to alternating rows
$rows.each(function (index) {
// but first remove class that may have been added by previous changes
$(this).removeClass("stripe");
if ( index % 2 == 0) {
$(this).addClass("stripe");
}
});
If you're too lazy to define the CSS class "stripe" then here's a quick & dirty version:
// un-stripe table, since bootstrap striping doesn't work for filtered rows
$("table#reports").removeClass("table-striped");
// now add stripes to alternating rows
$rows.each(function (index) {
// but first remove color that may have been added by previous changes:
$(this).css("background-color", "inherit");
if ( index % 2 == 0) {
$(this).css("background-color", "#f9f9f9");
}
});
This is the same answer as #Jacobski's answer but will keep the hover effect of a bootstrap table-hover.
$("tr:visible").each(function (index) {
$(this).css("background-color", !!(index & 1) ? "rgba(0,0,0,.05)": "rgba(0,0,0,0)");
if (!(index & 1)) {
$(this).hover(
function () { //On hover over
$(this).css("background-color", "rgba(0,0,0,.07)");
},
function () { //On hover out
$(this).css("background-color", "rgba(0,0,0,0)");
}
)
}
});
My answer build upon what #Jacob and #yehuda suggested.
This works with bootstrap4, for a table that needs both the behavior of ".table-striped" and ".table-hover".
The hover part is handled by CSS, which makes it more efficient (I noticed a small delay due to javascript handler, when testing #yehuda's snippet).
// CSS
<style>
.table-striped tbody tr.visible-odd {
background-color: rgba(0, 0, 0, 0.05);
}
.table-striped tbody tr.visible-even {
background-color: rgba(0, 0, 0, 0.00);
}
.table-hover tbody tr.visible-even:hover {
background-color: rgba(0, 0, 0, 0.075);
}
</style>
// JS
$("tr:visible").each( function(index, obj) {
if (index % 2) {
$(this).addClass('visible-odd').removeClass('visible-even');
} else {
$(this).addClass('visible-even').removeClass('visible-odd');
}
});
For me this works fine with hidden rows and reapplies the striping as expected:
$("table#ProductTable").removeClass("table-striped");
$("table#ProductTable").addClass("table-striped");
#Jacobski's answer was great, but I had some pages with multiple tables and the header row's background would get changed on separate tables. Also my table rows that were always visible had the class "accordion-toggle" not sure if that's a bootstrap 5 thing, but that is how I targeted it! (also I don't know JavaScript so there's probably cleaner syntax to do what I did)
$("tr:visible").each(function (index) {
if ($(this).hasClass("tb-header")) {
rowIndex = 0; // need to reset the rowIndex since we are now on a new table!
} else {
if ($(this).hasClass("accordion-toggle")) {
$(this).css("background-color", !!(rowIndex & 1)? "rgba(0,0,0,0)" : "rgba(0,0,0,.05)");
rowIndex++;
}
}
});
I have more than 3 buttons, each of them has a different style (different border color, different background color on hover).
(I created them with the <li> because they have an action to change the background-position of a picture).
I want them to maintain the same hover state appearance after they've been clicked, but to go back to the normal state when another button is clicked.
How can I do this?
Thank you in advance :)
ps: I'm working in HTML with css, js when needed (like in this case).
Given the complete lack of information about the HTML, and current JavaScript, you're using, the best I can offer is a simple demonstration of how this might be achieved:
function colorify (e) {
// get a reference to the element we're changing/working on:
var demo = document.getElementById('demo'),
/* getting the siblings, the other controls,
of the clicked-element (e.target):
*/
controls = e.target.parentNode.children;
// iterating over those controls
for (var i = 0, len = controls.length; i < len; i++) {
/* if the current control[i] is the clicked-element, we 'add' the 'active'
class, otherwise we 'remove' it (using a ternary operator):
*/
controls[i].classList[controls[i] == e.target ? 'add' : 'remove']('active');
}
/* changing the background-color of the 'demo' element, setting it to the
textContent of the clicked-element:
*/
demo.style.backgroundColor = e.target.textContent;
}
var controls = document.getElementById('controls');
controls.addEventListener('click', colorify);
JS Fiddle demo.
The above is based on the following HTML:
<div id="demo"></div>
<ul id="controls">
<li>Red</li>
<li>Green</li>
<li>Blue</li>
</ul>
And CSS:
#demo {
width: 10em;
height: 10em;
border: 2px solid #000;
}
.active {
color: #f00;
}
This approach requires a browser that implements the classList API, the children property of a DOM node, as well as the addEventListener() method of a Node.
References:
addEventListener.
Element.classList.
ParentNode.children.
I need to change the td background to grey and text in another td when the user's mouse goes over the first mentioned td.
I have done this so far:
<td onMouseOver="this.style.background='#f1f1f1'" onMouseOut="this.style.background='white'">
but this only changes the background of the first td and does not change the text in the second td.
Any ideas please?
Have a look at this:
function highlightNext(element, color) {
var next = element;
do { // find next td node
next = next.nextSibling;
}
while (next && !('nodeName' in next && next.nodeName === 'TD'));
if (next) {
next.style.color = color;
}
}
function highlightBG(element, color) {
element.style.backgroundColor = color;
}
HTML:
<td onMouseOver="highlightBG(this, 'red');highlightNext(this, 'red')"
onMouseOut="highlightBG(this, 'white');highlightNext(this, 'black')" >
DEMO
Note that adding the event handler in the HTML is not considered to be good practice.
Depending on which browser you want to support (it definitely won't work in IE6), you really should consider the CSS approach which will work even if JS is turned off. Is much less code and it will be easier to add this behaviour to multiple elements:
td:hover {
background-color: red;
}
td:hover + td {
color: red;
}
DEMO
You should give that other td an id and access it from your onmouseover event handler. Maybe you should put that onmouseover code into its own function and call it from the onmouseover.