Balancing height in column-count CSS columns that contain only images - javascript

I'm trying to balance the height between the columns in an image gallery I've made with the following code:
section {
background: magenta;
/* So you can see the area I don't want to appear */
}
.gallery {
width: 100%;
line-height: 0;
-webkit-column-count: 2;
-webkit-column-gap: 0;
-moz-column-count: 2;
-moz-column-gap: 0;
column-count: 2;
column-gap: 0;
}
<section class="gallery">
<div>
<img src="http://lorempixel.com/800/1000/">
</div>
<div>
<img src="http://lorempixel.com/800/600/">
</div>
<div>
<img src="http://lorempixel.com/800/200/">
</div>
<div>
<img src="http://lorempixel.com/800/700/">
</div>
<div>
<img src="http://lorempixel.com/800/900/">
</div>
<div>
<img src="http://lorempixel.com/800/400/">
</div>
<div>
<img src="http://lorempixel.com/800/200/">
</div>
<div>
<img src="http://lorempixel.com/800/600/">
</div>
<div>
<img src="http://lorempixel.com/800/700/">
</div>
<div>
<img src="http://lorempixel.com/800/600/">
</div>
<div>
<img src="http://lorempixel.com/800/550/">
</div>
<div>
<img src="http://lorempixel.com/800/700/">
</div>
<div>
<img src="http://lorempixel.com/800/600/">
</div>
<div>
<img src="http://lorempixel.com/800/1000/">
</div>
<div>
<img src="http://lorempixel.com/800/700/">
</div>
</section>
Here's a JSFiddle: https://jsfiddle.net/auan2xnj/1/
The images all have different heights but all have the same width. My column-fill is set to balance (as by default).
The problem:
In the JSFiddle, it looks pretty good, but on the website I'm developing, some enormous gaps have appeared comparing the height of the columns. I'm guessing this is because of the orders the images have in the HTML, considering that's the exact same order they'll be put in the columns by the CSS. This is a screenshot from my project, using exactly the same code from the JSFiddle:
Behaviour:
When I give the .gallery element a hardcoded height value, the columns always balance much better. This is a problem because in my website, images are added dynamically and I'm never going to know the exact height of all the galleries.
Request:
I would like to find a piece of code (whatever it is, I think I can implement some JS) that fixes this issue, either by reordering the images in the HTML so that the outcome is the best possible, or whatever way there is in order to set a height dynamically so that the problem is solved.

EDIT:
If you don't need to keep line-height: 0 you would simply use .gallery img {display:block} and remove line-height, it's all you need. That would be the best. table-cell and so on can have some side-effects. For example vertical-align: middle leave a space under the images, and is just a hack.
https://jsfiddle.net/bruLwktv/
Challange accepted, here is the solution: ;)
The algorithm makes sure every images is loaded and then partition them into both coloumns in a way the have about the closest total height possible to create a minimal gap.
Using The greedy algorithm for the Partition problem to create Balanced partitions.
var gallery = document.getElementsByClassName("gallery")[0]
var images = gallery.getElementsByTagName("img")
var notLoaded = 0
window.onload = function() {
for (var i = images.length; i--;) {
if (images[i].width == 0) {
// let the image tell us when its loaded
notLoaded++
images[i].onload = function() {
if (--notLoaded == 0) {
allImgLoaded()
}
}
}
}
// check if all images are already loaded
if (notLoaded == 0) allImgLoaded()
}
function allImgLoaded() {
// Partition images
var imgs = partitionImages(images)
// reorder DOM
for (var i = images.length; i--;) {
gallery.appendChild(imgs[i])
}
}
function partitionImages(images) {
var groupA = [], totalA = 0
var groupB = [], totalB = 0
// new array width img and height
var imgs = []
for (var i = images.length; i--;) {
imgs.push([images[i], images[i].height])
}
// sort asc
imgs.sort(function(a, b) {
return b[1] - a[1]
});
// reverse loop
for (var i = imgs.length; i--;) {
if (totalA < totalB) {
groupA.push(imgs[i][0])
totalA += imgs[i][1]
} else {
groupB.push(imgs[i][0])
totalB += imgs[i][1]
}
}
return groupA.concat(groupB)
}
section {
background: magenta;
/* So you can see the area I don't want to appear */
}
.gallery {
width: 100%;
line-height: 0;
-webkit-column-count: 2;
-webkit-column-gap: 0;
-moz-column-count: 2;
-moz-column-gap: 0;
column-count: 2;
column-gap: 0;
}
<section class="gallery">
<div><img src="http://lorempixel.com/800/1000/"></div>
<div><img src="http://lorempixel.com/800/600/"></div>
<div><img src="http://lorempixel.com/800/200/"></div>
<div><img src="http://lorempixel.com/800/700/"></div>
<div><img src="http://lorempixel.com/800/900/"></div>
<div><img src="http://lorempixel.com/800/400/"></div>
<div><img src="http://lorempixel.com/800/200/"></div>
<div><img src="http://lorempixel.com/800/600/"></div>
<div><img src="http://lorempixel.com/800/700/"></div>
<div><img src="http://lorempixel.com/800/600/"></div>
<div><img src="http://lorempixel.com/800/550/"></div>
<div><img src="http://lorempixel.com/800/700/"></div>
<div><img src="http://lorempixel.com/800/600/"></div>
<div><img src="http://lorempixel.com/800/1000/"></div>
<div><img src="http://lorempixel.com/800/700/"></div>
</section>

Yeah, column implementation is bizarre right now.
If you get rid of "line-height: 0;" this problem becomes much less severe. I have no idea why. Except to say that the line-height thing is a hack to get rid of that omnipresent little space/margin at the bottom of images (remove the line-spacing rule in the jfiddle if you don't know what I'm talking about).
Supposedly that space/margin is there because HTML doesn't know an img element isn't text and so leaves room for the "tails" of characters like "y" and "g" which go below the line. This is a ridiculous bug which I think should've been fixed a decade ago. To me, this is up there with IE<9 levels of stupid implementation.
Anyway, rant over, a way to fix that space/margin without using the line-height hack:
img {
vertical-align: middle;
}
Also, it's not an issue if you're positive all your images will be the same width, but in the fiddle the gap problem is made worse by images overlapping the widths of the columns. You should set
img {
width: 100%
// and, for insurance, I also add:
height: auto;
}
to make sure you're fitting the images into the columns.

I've added some CSS to the <div> elements, let me know if this works:
https://jsfiddle.net/rplittle/auan2xnj/3/
I've also seen this:
div {
-webkit-margin-before: 0;
-webkit-margin-after: 0;
}
(adjust/add prefixes if necessary)
Or you could try Masonry, it's well-built and pretty easy to set up.

Request:
I would like to find a piece of code (whatever it is, I think I can implement >> some JS) that fixes this issue, either by reordering the images in the HTML so >> that the outcome is the best possible, or whatever way there is in order to set a height dynamically so that the problem is solved.
the greedy algorithm is a nice approach, and useful for streams, where you have to dynamically extend the tiles. (like pagination)
I've included a function that does a better balancing by grouping the items and switching items between the groups.
The columns here are implemented by creating 2 divs that are inline-block and 50% width; simple and works across all modern browsers.
function balance(numGroups, items, fetchValue){
function delta(a, b){
var da = a-avg, db = b-avg;
return da*da + db*db;
}
function testAndSwitchItems(a,b){
var valueA, valueB,
lengthA = a.length, lengthB = b.length,
indexA = lengthA, indexB = lengthB,
bestDelta = delta(a.sum, b.sum);
//test every item in this first column against every value in the second column
for(var i = 0; i <= lengthA; ++i){
//accessing a not available index of an Array may deoptimize this function
//that's why i check the index before accessing the element.
valueA = (i < lengthA && a[i] || nullElement).value;
for(var j = 0; j <= lengthB; ++j){
valueB = (j < lengthB && b[j] || nullElement).value;
//test wether it would be an improvement to switch these items
var d = delta(
a.sum + valueB - valueA,
b.sum + valueA - valueB
);
//find the best combination
if(d < bestDelta){
indexA = i;
indexB = j;
bestDelta = d;
}
}
}
var elmA = indexA < lengthA && a[indexA],
elmB = indexB < lengthB && b[indexB],
tmp;
if(elmA && elmB){
//switch items
a[indexA] = elmB;
b[indexB] = elmA;
}else if(elmA){
//move an item from a to b
b.push(elmA);
tmp = a.pop()
if(elmA !== tmp)
a[indexA] = tmp;
}else if(elmB){
//move an item from b to a
a.push(elmB);
tmp = b.pop()
if(elmB !== tmp)
b[indexB] = tmp;
}else{
//no switch would improve the result
return false;
}
//compute the new sums per group
valueA = (elmA || nullElement).value;
valueB = (elmB || nullElement).value;
a.sum += valueB - valueA;
b.sum += valueA - valueB;
return true;
}
function initGroupsAndReturnSum(sum, item, i){
var value = fetchValue? fetchValue(item): item;
//distribute the items
if(i<numGroups){
var group = groups[i] = [];
group.sum = value;
}else{
group = groups[ i%numGroups ];
group.sum += value;
}
group.push({
index: i,
value: value
});
return sum + value;
}
var groups = [],
nullElement = { value: 0 },
//don't care wether items is an Array, a nodeList or a jQuery-object/list, ...
avg = groups.reduce.call(items, initGroupsAndReturnSum, 0) / groups.length;
//start switching items between the groups
do {
for(var i=1, done = true; i<groups.length; ++i)
for(var j=0; j<i; ++j)
//test each combination of groups
if(testAndSwitchItems(groups[j], groups[i])){
//this switch may have affected the computation against other groups
//do another loop
done = false;
}
}while(!done);
function sorter(a,b){
return a.index - b.index
}
function getItem(item){
return items[item.index]
}
return groups.map(function(a){
return a.sort(sorter).map(getItem)
});
}
and a possible way to use it
var groups = balance(2, //num columns
//something Arraylike that contains some objects to be grouped
document.querySelectorAll('img'),
//a function to compute a value out of an Array-item
img => img.naturalHeight / img.naturalWidth
);
https://jsfiddle.net/77o3ptk2/2/
Just for the sake of it, the example also includes a few more styles like a padding around the images and an enumeration, to show how much the array has been shuffled.

You can equalize height of each column using this http://brm.io/jquery-match-height/ . And its also good in mobile responsive. No need for css, just add the library and put this snippet
$(function() {
$('.gallery').find('div').matchHeight(options);
});

Related

Best practice to multiple 'ifs' - Javascript

I built a function to make a responsive carousel with multiples imgs per slide. (couldn't make Owl Carousel work on my Angular project, but not the point here).
I set the amount of img that will be presented by slide based on the current screen width.
Here is my code:
imgsHistoria = [
"../../assets/imgs/historia/hist_01.png",
"../../assets/imgs/historia/hist_02.png",
"../../assets/imgs/historia/hist_03.png",
"../../assets/imgs/historia/hist_04.png",
"../../assets/imgs/historia/hist_05.png",
"../../assets/imgs/historia/hist_06.png",
"../../assets/imgs/historia/hist_07.png",
"../../assets/imgs/historia/hist_08.png",
"../../assets/imgs/historia/hist_09.png",
"../../assets/imgs/historia/hist_10.png",
];
imgsHistoriaArray = [];
resizeCarousel() {
let images = this.imgsHistory;
let cut = this.getCut();
this.imgsHistoryArray = [];
for (var i = 0; i < images.length; i = i + cut) {
this.imgsHistoryArray.push(images.slice(i, i + cut));
}
}
getCut() {
if (this.getScreenWidth < 480) {
return 1
} if (this.getScreenWidth < 576) {
return 2
} if (this.getScreenWidth < 768) {
return 3
} if (this.getScreenWidth < 992) {
return 4
}
return 6;
}
The thing is that I have CodeMetrics installed and it's showing that the getCut() function has complexity 10, which is not great. How can I improve this function?
You could use an array and a loop to reduce the number of if statements:
getCut() {
let widths = [480, 576, 768, 992];
for(let i = 0; i < widths.length; i++) {
let width = widths[i];
if(this.getScreenWidth < width)
return i+1;
}
return 6;
}
Here we define an array containing the widths, loop through until we find the correct value, then return the corresponding number based on its index.

How to: randomize element positions and size within a css "grid" in a React app

I need help with the concept of randomizing html-element positions and sizes within a grid I've defined. I'm working on my first project using React and I've been tasked to create an app where I display two set of black "cards" (rectangles) to the user of which he/she has to choose one.
In the design description I've been given I've been told to randomize the position and size of these rectangle sets, but the amount on each side is always set.
In other words I'm rendering say 3 rectangles on the left side of the screen and then 4 on the right side and then the user tries to answer which side has more rectangles in it.
Below I have a snippet of the relevant code (I hope it shows up reasonably, quite new to this). The itemContents-array just pulls from my server values regarding the item being processed, so in this case it will just return a single digit number as a string, which is used to determine the correct amount of rectangles to render.
for (let index = 0; index < parseInt(itemContents[0]); index++) {
leftSideCards.push(<li id="left-side-items"> █ </li>)
}
for (let index = 0; index < parseInt(itemContents[1]); index++) {
rightSideCards.push(<li id="left-side-items"> █ </li>)
}
choiceArea = (
<div className="card-compare-choiceArea">
<div id="card-first-choice">{leftSideCards}</div>
<span id="space"></span>
<div id="card-second-choice">{rightSideCards}</div>
<label id="saku">Text!</label>
<span id="space2" key="space2">{feedback}</span>
<label id="late">Text!</label>
</div>
);
}
return (
<div className="neure-number-compare">
{choiceArea}
</div>
);
}
As for my CSS:
#card-first-choice {
display: grid;
list-style-type: none;
grid-template-columns: 30% 30% 30%;
grid-gap: 5px;
width: 30vw;
height: 60vh;
border: 2px solid black;
margin-top: 1em;
}
#card-second-choice {
display: grid;
list-style-type: none;
grid-template-columns: 30% 30% 30%;
grid-gap: 10px;
width: 30vw;
height: 60vh;
border: 2px solid black;
margin-top: 10px;
}
So my logic was to create two grids, one for each side ("card-first-choice & card-second-choice") and then have a set amount of columns (3) and then set the array of rectangles as its contents. And so we are at my issues of randomizing to which grid part does a rectangle go and randomizing the size of said rectangle.
Right now my code works fine otherwise but it obviously just renders the squares in order filling the columns one by one and adding rows accordingly.
I'm not sure either if using the UTF-8 geometric shapes to create the black rectangles is a good approach in regards to randomizing it's properties or if I should be using something else to create rectangles (it was just the easiest way I found to do it). Please suggest others if you have a working solution for my problem with it!
Obviously if there's a simpler and better way to go about doing this (I can only assume that's a given), I'm willing to learn/listen to alternatives!
As I'm a a junior dev in my first real project dealing with React for the first time and not being that versed in CSS either, I wholly assume that there are a bunch of things that are redundant or could be done more optimally (be gentle).
For randomizing array you can use How to randomize (shuffle) a JavaScript array?
You can set the size of your grids as maxSize of right and left sizes + 1/3 of total size like this
let gridSize = Math.max(...itemContents.map((item) => parseInt(item)));
gridSize = Math.ceil(gridSize + gridSize / 3);
There is my variant of code
const itemContents = ["5", "7"];
const leftSideCards = [];
const rightSideCards = [];
let gridSize = Math.max(...itemContents.map((item) => parseInt(item)));
gridSize = Math.ceil(gridSize + gridSize / 3);
const addInitialSet = (cardsList) => {
for (let index = 0; index < gridSize; index++) {
cardsList.push(<li id="left-side-items" />);
}
};
const addCards = (cardsList, itemNumber) => {
for (let index = 0; index < itemNumber; index++) {
cardsList[index] = <li id="left-side-items"> █ </li>;
}
};
const shuffle = (array) => {
let currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
};
addInitialSet(leftSideCards);
addInitialSet(rightSideCards);
addCards(leftSideCards, parseInt(itemContents[0]));
addCards(rightSideCards, parseInt(itemContents[1]));
shuffle(leftSideCards);
shuffle(rightSideCards);
const choiceArea = (
<div className="card-compare-choiceArea">
<div id="card-first-choice">{leftSideCards}</div>
<span id="space" />
<div id="card-second-choice">{rightSideCards}</div>
<label id="saku">Text!</label>
<span id="space2" key="space2">
"feedback"
</span>
<label id="late">Grid size {gridSize}</label>
</div>
);
return <div className="neure-number-compare">{choiceArea}</div>;
}

move object using css

I have an array of image objects:
var pics = ["pic1","pic2","pic3","pic4","pic5","pic6"]
I want to loop through and set the style.left value to 1 less than the current value. This loop iterates 100 times.
I got a great suggestion here to use css for this. I created a style called move:
margin-left: -1px;
This works great the first time through the loop:
for (i = 0; i < 100; i++) {
for (j = 0; j < 6; j++) {
var image = document.getElementById(pics[j]);
image.className = image.className + " move"
}
}
The problem is the images do not move after the first iteration though the loop. Any ideas what I can do?
The images do not move after the first iteration because you are applying the move class which sets the margin left to -1. Note it does not subtract 1 from their current left margin it sets it to -1.
So lets say it starts out with a left margin of 10 . When it goes through this loop I think you are expecting it to have a left margin of 9 to move it one pixel closer to the left side of the screen. Instead when it goes through this loop it will skip all the way -1 as the left margin because you are explicitly telling it to do that with your move class.
What you really want to do is use javascript to grab its current left margin , subtract 1 from that and then reset the left margin as the new, now less one, number.
At quick glance I think this tutorial could help you.
You can save off the margin variable and apply css with this:
var margin = -1;
for (i = 0; i < 100; i++) {
for (j = 0; j < 6; j++) {
var image = document.getElementById(pics[j]);
image.style.marginLeft = margin + "px";
margin--;
}
}

Assign random picked color with javascript

I have a grid in my page that I want to populate via javascript with 200 elements. The actual code that populate the .grid element is the following:
$(function () {
var $gd = $(".grid");
var blocks="";
for(i=0; i < 200; i++){
blocks += '<div class="block"></div>';
}
$gd.append(blocks);
});
What I'm trying to do now is assign to each element created a random picked color from a list. Lets say red, blue, yellow, green (unexpected eh?). I'd like the values to be the most random possible, and also to avoid the same color to be picked again twice consequentially (just to be clear, it's ok something like red-blue-red-green-blue and so on, NOT red-red-green-yellow).
Maybe this can help in the randomize process, Fisher–Yates Shuffle, but I don't how to implement the non-twice adjacent rule stated above (if it is possible to apply at all).
What would be the best way to achieve this result? I'm also wondering if I could apply a gradient to each .block instead of a flat hex color; I guess the better route for this would be to assign a random class to each element mapped in CSS for the gradient and so on..
If the above script can be optimized performance-wise, I apreciate any suggestions!
Additional info:
I'm using jQuery
The grid is composed with 20 elements per row, for 10 rows
The colors should be 4, but can be raised to 5-7 adding some neutral grey tones if it can help
Here is a pen to experiment with http://codepen.io/Gruber/pen/lDxBw/
Bonus feature request: as stated above I'd like to avoide duplicate adjacent colors. Is it possible to avoid this also "above and below"? I guess its very hard if not impossible to totally avoid this, but well if anyone can find a solution it would be awesome!
Something like this, where the "nope" marked element is prevented, while the "yep" diagonal marked are allowed:
$(function () {
var colors = ["red","blue","green","yellow"];
var $gd = $(".grid");
var previousColor;
var blocks="";
for(i=0; i < 200; i++){
var color = "";
while(color === previousColor) {
color= colors [Math.floor(Math.random()*colors .length)];
}
blocks += '<div class="block" style="color:' + color + '"></div>';
previousColor = color;
}
$gd.append(blocks);
});
First, I'd use classes for the colors:
CSS:
.red { background-color: red; }
.blue { background-color: blue; }
.green { background-color: green; }
.yellow { background-color: yellow; }
And then here's the javascript:
$(document).ready(function() {
var colors = ["red","blue","green","yellow"];
var $gd = $(".grid");
var previousColor;
var previousRow;
var rowSize = 10;
while(rowSize--) previousRow.push("none");
var blocks = "";
for(i=0; i < 200; i++){
var color = colors [Math.floor(Math.random()*colors .length)];
while((color == previousColor) || (color == previousRow[i%rowSize])) {
color = colors [Math.floor(Math.random()*colors .length)];
}
blocks += '<div class="block ' + color + '"></div>';
previousColor = color;
previousRow[i%rowSize] = color;
}
$gd.append(blocks);
});
I started off with something similar to MikeB's code but added a row element so we know what is above your current block.
The first thing I'd like to introduce is a filtered indexing function.
Given an array:
var options = ['red', 'green', 'blue', 'purple', 'yellow']; // never less than 3!
And a filter:
function filterFunc(val) {
var taken = { 'red': 1, 'blue': 1 };
return taken[val] ? 0 : 1;
}
We can take the nth item from the values permitted (==1) by the filter (not a quick way to do it, but until there is a performance constraint...):
// filteredIndex returns nth (0-index) element satisfying filterFunc
// returns undefined if insufficient options
function filteredIndex(options, filterFunc, n) {
var i=-1, j=0;
for(;j<options.length && i<n; ++j) {
i += filterFunc(options[j]);
if(i==n)
break;
}
return options[j];
}
So now we can pick up a value with index 2 in the filtered list. If we don't have enough options to do so, we should get undefined.
If you are populating the colours from, say, the top left corner, you can use as few as 3 colours, as you are only constrained by the cells above and to the left.
To pick randomly, we need to set up the filter. We can do that from a list of known values thus:
function genFilterFunc(takenValues) {
var takenLookup = {};
for(var i=0; i < takenValues.length; ++i) {
takenLookup[takenValues[i]] = 1;
}
var filterFunc = function(val) {
return takenLookup[val] ? 0 : 1;
};
return filterFunc;
}
We can choose a random colour, then, for a cell in a grid[rows][cols]:
function randomColourNotUpOrLeft(grid, row, col, options, ignoreColour) {
var takenlist = [];
if(row > 0 && grid[row-1][col] != ignoreColour) {
takenlist.push(grid[row-1][col]);
}
if(col > 0 && grid[row][col-1] != ignoreColour) {
takenlist.push(grid[row][col-1]);
}
var filt = genFilterFunc(takenlist);
var randomIndex = Math.floor(Math.random()*(options.length-takenlist.length));
var randomColour = filteredIndex(options, filt, randomIndex);
return randomColour;
}
Note here that the random index used depends on how many colours have been filtered out; if there are 4 left we can have 0-3, but if only 2 are left it must be 0-1, etc. When the adjacent cells are the same colour and/or we are near the boundary, there is less constraint about which colour is chosen. Finally fill in a grid:
function fillGridSpeckled(grid, options, nullColour) {
for(var row=0; row<grid.length; ++row) {
for(var col=0; col<grid[row].length; ++col) {
grid[row][col] = randomColourNotUpOrLeft(grid,row,col,options,nullColour);
}
}
}
I've put it all in this jsbin, along with a few bits to demo the code working.

How can you figure out the highest z-index in your document?

In order to set a div containing a transparent text image as the highest z-index in my document, I picked the number 10,000 and it solved my problem.
Previously I had guessed with the number 3 but it had no effect.
So, is there a more scientific way of figuring out what z-index is higher than that of all of your other elements?
I tried looking for this metric in Firebug but couldn't find it.
Stealing some code from abcoder site for the sake of clarity:
var maxZ = Math.max.apply(null,
$.map($('body *'), function(e,n) {
if ($(e).css('position') != 'static')
return parseInt($(e).css('z-index')) || 1;
}));
You could call findHighestZIndex for a particular element type such as <div> like this:
findHighestZIndex('div');
assuming a findHighestZindex function that is defined like this:
function findHighestZIndex(elem)
{
var elems = document.getElementsByTagName(elem);
var highest = Number.MIN_SAFE_INTEGER || -(Math.pow(2, 53) - 1);
for (var i = 0; i < elems.length; i++)
{
var zindex = Number.parseInt(
document.defaultView.getComputedStyle(elems[i], null).getPropertyValue("z-index"),
10
);
if (zindex > highest)
{
highest = zindex;
}
}
return highest;
}
Using ES6 a cleaner approach
function maxZIndex() {
return Array.from(document.querySelectorAll('body *'))
.map(a => parseFloat(window.getComputedStyle(a).zIndex))
.filter(a => !isNaN(a))
.sort()
.pop();
}
I’d like to add my ECMAScript 6 implementation that I use in one of my UserScripts. I’m using this one to define the z-index of specific elements so that they always appear the highest.
In JS, you can additionally set certain attributes or class names to elements that you may want to exclude. For instance, consider your script setting a data-highest attribute on an element that you want to appear as the highest element (e.g. a popup); and consider an element with class name yetHigher that you don’t control, which should be even higher (e.g. a custom context menu). I can exclude these elements with a chained :not selector. Note that :not([data-highest], .yetHigher) is possible, but experimental, and only has limited browser support as of January 2021.
let highestZIndex = 0;
// Then later, potentially repeatedly
highestZIndex = Math.max(
highestZIndex,
...Array.from(document.querySelectorAll("body *:not([data-highest]):not(.yetHigher)"), (elem) => parseFloat(getComputedStyle(elem).zIndex))
.filter((zIndex) => !isNaN(zIndex))
);
The lower five lines can run multiple times and update the variable highestZIndex repeatedly by finding out the maximum between the current highestZIndex value and all the other computed z-indexes of all elements. The filter excludes all the "auto" values.
I believe what you are observing is Voodoo. Without access to your complete style sheet I can of course not tell reliably; but it strikes me as likely that what really has happened here is that you have forgotten that only positioned elements are affected by z-index.
Additionally, z-indexes aren't assigned automatically, only in style sheets, which means that with no other z-indexed elements, z-index:1; will be on top of everything else.
I guess you have to do this yourself ...
function findHighestZIndex()
{
var divs = document.getElementsByTagName('div');
var highest = 0;
for (var i = 0; i < divs .length; i++)
{
var zindex = divs[i].style.zIndex;
if (zindex > highest) {
highest = zindex;
}
}
return highest;
}
There isn't a default property or anything, but you could write some javascript to loop through all elements and figure it out. Or if you use a DOM management library like jQuery, you could extend its methods (or find out if it supports it already) so that it starts tracking element z-indices from page load, and then it becomes trivial to retrieve the highest z-index.
The "ES6" version above is less efficient than the first solution because it does multiple redundant passes across the full array. Instead try:
findHighestZ = () =>
[...document.querySelectorAll('body *')]
.map(elt => parseFloat(getComputedStyle(elt).zIndex))
.reduce((highest, z) => z > highest ? z : highest, 1)
In theory it would be even quicker to do it in one reduce step, but some quick benchmarking showed no significant difference, and the code is gnarlier
The best way to solve this problem is, in my opinion, just to set yourself conventions for what kinds of z-indexes are used for different kinds of elements. Then, you'll find the correct z-index to use by looking back at your documentation.
Using jQuery:
if no elements supplied, it checks all elements.
function maxZIndex(elems)
{
var maxIndex = 0;
elems = typeof elems !== 'undefined' ? elems : $("*");
$(elems).each(function(){
maxIndex = (parseInt(maxIndex) < parseInt($(this).css('z-index'))) ? parseInt($(this).css('z-index')) : maxIndex;
});
return maxIndex;
}
I had to do this for a project recently, and I found that I benefitted a lot from #Philippe Gerber's great answer here, and #flo's great answer (the accepted answer).
The key differences to the answers referenced above are:
Both the CSS z-index, and any inline z-index style are calculated, and use the larger of the two for comparison and calculation.
Values are coerced into integers, and any string values (auto, static, etc) are ignored.
Here is a CodePen for the code example, but it's included here as well.
(() => {
/**
* Determines is the value is numeric or not.
* See: https://stackoverflow.com/a/9716488/1058612.
* #param {*} val The value to test for numeric type.
* #return {boolean} Whether the value is numeric or not.
*/
function isNumeric(val) {
return !isNaN(parseFloat(val)) && isFinite(val);
}
/**
* Finds the highest index in the current document.
* Derived from the following great examples:
* [1] https://stackoverflow.com/a/1118216/1058612
* [2] https://stackoverflow.com/a/1118217/1058612
* #return {number} An integer representing the value of the highest z-index.
*/
function findHighestZIndex() {
let queryObject = document.querySelectorAll('*');
let childNodes = Object.keys(queryObject).map(key => queryObject[key]);
let highest = 0;
childNodes.forEach((node) => {
// Get the calculated CSS z-index value.
let cssStyles = document.defaultView.getComputedStyle(node);
let cssZIndex = cssStyles.getPropertyValue('z-index');
// Get any inline z-index value.
let inlineZIndex = node.style.zIndex;
// Coerce the values as integers for comparison.
cssZIndex = isNumeric(cssZIndex) ? parseInt(cssZIndex, 10) : 0;
inlineZIndex = isNumeric(inlineZIndex) ? parseInt(inlineZIndex, 10) : 0;
// Take the highest z-index for this element, whether inline or from CSS.
let currentZIndex = cssZIndex > inlineZIndex ? cssZIndex : inlineZIndex;
if ((currentZIndex > highest)) {
highest = currentZIndex;
}
});
return highest;
}
console.log('Highest Z', findHighestZIndex());
})();
#root {
background-color: #333;
}
.first-child {
background-color: #fff;
display: inline-block;
height: 100px;
width: 100px;
}
.second-child {
background-color: #00ff00;
display: block;
height: 90%;
width: 90%;
padding: 0;
margin: 5%;
}
.third-child {
background-color: #0000ff;
display: block;
height: 90%;
width: 90%;
padding: 0;
margin: 5%;
}
.nested-high-z-index {
position: absolute;
z-index: 9999;
}
<div id="root" style="z-index: 10">
<div class="first-child" style="z-index: 11">
<div class="second-child" style="z-index: 12"></div>
</div>
<div class="first-child" style="z-index: 13">
<div class="second-child" style="z-index: 14"></div>
</div>
<div class="first-child" style="z-index: 15">
<div class="second-child" style="z-index: 16"></div>
</div>
<div class="first-child" style="z-index: 17">
<div class="second-child" style="z-index: 18">
<div class="third-child" style="z-index: 19">
<div class="nested-high-z-index">Hello!!! </div>
</div>
</div>
</div>
<div class="first-child">
<div class="second-child"></div>
</div>
<div class="first-child">
<div class="second-child"></div>
</div>
<div class="first-child">
<div class="second-child"></div>
</div>
</div>
Array.reduce()
Here's another solution to determine the topmost z-index that uses Array.reduce():
const max_zindex = [...document.querySelectorAll('body *')].reduce((accumulator, current_value) => {
current_value = +getComputedStyle(current_value).zIndex;
if (current_value === current_value) { // Not NaN
return Math.max(accumulator, current_value)
}
return accumulator;
}, 0); // Default Z-Index Rendering Layer 0 (Zero)
ShadowRoot solutions
We must not forget about custom-elements and shadow-root content.
function computeMaxZIndex() {
function getMaxZIndex(parent, current_z = 0) {
const z = parent.style.zIndex != "" ? parseInt(parent.style.zIndex, 10) : 0;
if (z > current_z)
current_z = z;
const children = parent.shadowRoot ? parent.shadowRoot.children : parent.children;
for (let i = 0; i < children.length; i++) {
const child = children[i];
current_z = getMaxZIndex(child, current_z);
}
return current_z;
}
return getMaxZIndex(document.body) + 1;
}
If you're looking to show the IDs of all elements with the highest z indices:
function show_highest_z() {
z_inds = []
ids = []
res = []
$.map($('body *'), function(e, n) {
if ($(e).css('position') != 'static') {
z_inds.push(parseFloat($(e).css('z-index')) || 1)
ids.push($(e).attr('id'))
}
})
max_z = Math.max.apply(null, z_inds)
for (i = 0; i < z_inds.length; i++) {
if (z_inds[i] == max_z) {
inner = {}
inner.id = ids[i]
inner.z_index = z_inds[i]
res.push(inner)
}
}
return (res)
}
Usage:
show_highest_z()
Result:
[{
"id": "overlay_LlI4wrVtcuBcSof",
"z_index": 999999
}, {
"id": "overlay_IZ2l6piwCNpKxAH",
"z_index": 999999
}]
A solution highly inspired from the excellent idea of #Rajkeshwar Prasad .
/**
returns highest z-index
#param {HTMLElement} [target] highest z-index applyed to target if it is an HTMLElement.
#return {number} the highest z-index.
*/
var maxZIndex=function(target) {
if(target instanceof HTMLElement){
return (target.style.zIndex=maxZIndex()+1);
}else{
var zi,tmp=Array.from(document.querySelectorAll('body *'))
.map(a => parseFloat(window.getComputedStyle(a).zIndex));
zi=tmp.length;
tmp=tmp.filter(a => !isNaN(a));
return tmp.length?Math.max(tmp.sort((a,b) => a-b).pop(),zi):zi;
}
};
#layer_1,#layer_2,#layer_3{
position:absolute;
border:solid 1px #000;
width:100px;
height:100px;
}
#layer_1{
left:10px;
top:10px;
background-color:#f00;
}
#layer_2{
left:60px;
top:20px;
background-color:#0f0;
z-index:150;
}
#layer_3{
left:20px;
top:60px;
background-color:#00f;
}
<div id="layer_1" onclick="maxZIndex(this)">layer_1</div>
<div id="layer_2" onclick="maxZIndex(this)">layer_2</div>
<div id="layer_3" onclick="maxZIndex(this)">layer_3</div>
Robust solution to find maximum zIndex in NodeList
You should check both getComputedStyle and style object provided by node itself
Use Number.isNaN instead of isNaN because of isNaN("") === false
function convertToNumber(value) {
const asNumber = parseFloat(value);
return Number.isNaN(asNumber) ? 0 : asNumber;
}
function getNodeZIndex(node) {
const computedIndex = convertToNumber(window.getComputedStyle(node).zIndex);
const styleIndex = convertToNumber(node.style.zIndex);
if (computedIndex > styleIndex) {
return computedIndex;
}
return styleIndex;
}
function getMaxZIndex(nodeList) {
const zIndexes = Array.from(nodeList).map(getNodeZIndex);
return Math.max(...zIndexes);
}
const maxZIndex = getMaxZIndex(document.querySelectorAll("body *"));
Short
[...document.querySelectorAll`*`]
.reduce((a,e,i,t,z=+window.getComputedStyle(e).zIndex||0) => z>a ? z:a ,0);
let z = [...document.querySelectorAll`*`].reduce((a,e,i,t,z=+window.getComputedStyle(e).zIndex||0) => z>a ? z:a ,0);
console.log(z);
<div style="z-index: 100"></div>
<div style="z-index: 3000"></div>
<div style="z-index: 200"></div>
Very simple code using map and filter
function calMaxZIndex() {
return Array.from(document.querySelectorAll('body *'))
.map(a => parseFloat(window.getComputedStyle(a).zIndex || a.style.zIndex))
.filter(a => !isNaN(a))
.sort()
.pop()
}
function getMax() {
const max = calMaxZIndex() ?? 0
console.log({
max
})
}
getMax()
#ticket-box {
text-align: center;
position: fixed;
top: 0;
right: 0;
width: 100%;
background-color: #e9d295;
padding: 5px;
z-index: 6;
}
<div id="menu">
CLOSE
<ul style="text-align:center;list-style-type:none;">
<li>FILM</li>
<li>MUSIC</li>
<li>SPORTS</li>
<li>FINANCE</li>
</ul>
</div>
<div id="ticket-box">Have you bought your tickets for friday's event? No?! Grab yours now!</div>
<center>MENU</center>
Based on previous answers:
function with some modifications
let zIndexMax = () =>
[...document.querySelectorAll('body > *')]
.map(elem => parseInt(getComputedStyle(elem).zIndex, 10) || 0)
.reduce((prev, curr) => curr > prev ? curr : prev, 1);
Prototype
HTMLElement.prototype.zIndexMax = function () {
return [...this.children]
.map(elem => parseInt(getComputedStyle(elem).zIndex, 10) || 0)
.reduce((prev, curr) => curr > prev ? curr : prev, 1);
}
usage
document.querySelector('body').zIndexMax();
After looking through a lot of solutions here on StackOverflow - I've seen that none of them actually works correctly and considers how is zIndex actually working. I have written a solution that also takes into consideration the stacking context. You can refer to this article to understand how stacking context works in CSS.
const getZIndex = el => {
const computedStyle = getComputedStyle(el, null)
const zIndex = computedStyle.getPropertyValue('z-index')
return zIndex !== 'auto' ? parseInt(zIndex) : null
}
const getZIndexWithinStackingContext = (el, context) => {
let zIndex = getZIndex(el)
if (!zIndex) return null
let result = zIndex
while (el.parentElement !== context) {
el = el.parentElement
zIndex = getZIndex(el)
if (zIndex) {
result = zIndex
}
}
return result
}
const createZIndex = (overVisibleOnly = false, context = document.body) => {
const elements = [...document.querySelectorAll('body *')]
let highestZIndex = 0
elements.forEach(el => {
if (overVisibleOnly) {
const isVisible = !!el.offsetParent
if (!isVisible) return
}
const zIndex = getZIndexWithinStackingContext(el, context)
if (zIndex && zIndex > highestZIndex) {
highestZIndex = zIndex
}
})
return highestZIndex + 1
}
Note that this solution considers all elements, and not only positioned ones because they can become positioned after some class is added. But you can fix this easily by just adding a check for the position computed style property.
I have found the provided methods don't work when there have been z-indices changed dynamically on the page (the current methods only grab the originally set z-indices).
This function also works with dynamically added z indices:
function find_max_z_index() {
const all_z = [];
document.querySelectorAll("*").forEach(function(elem) {
all_z.push(elem.style.zIndex)
})
const max_index = Math.max.apply(null, all_z.map((x) => Number(x)));
return(max_index)
}
Here is my two-line function:
const getMaxZIndex = function () {
const elements = [...document.querySelectorAll("body *")];
return Math.max(...elements.map(x => parseInt(getComputedStyle(x, null).zIndex) || 0));
};
console.log(getMaxZIndex());

Categories

Resources