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

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());

Related

loop through elements and set CSS styling at same time

So I want to loop through elements with a class and then loop the individual element and gradually decrease the "left" css property.
let move = document.getElementsByClassName("move");
for (var i = 0; i < move.length; i++) {
const left = move[i].getBoundingClientRect().left;
const elementId = move[i].id;
for (let j = left; j > -20; j--) {
document.getElementById(elementId).style.left = j + "%";
await new Promise(resolve => setTimeout(resolve, 20));
}
}
However, I'm using "await" to delay so that it moves slowly and doesn't zip across the screen. I want it so that all the elements have their CSS left property decrease at the same time. But instead, because of the await, the first element increases its left property and when its left property reaches the end, only then the next element goes. Please advise
While I think it would be better to use pure CSS transitions/animations for this, you could use a while loop to iterate over the elements with the move class, subtracting 1% from their left value until they are completely off screen (right <= 0).
const start = async () => {
const move = document.getElementsByClassName("move");
const canMoveLeft = () => {
const move = document.getElementsByClassName("move");
for (let i = 0; i < move.length; i++) {
if (move[i].getBoundingClientRect().right > 0) return true;
}
return false;
};
while (canMoveLeft()) {
for (let i = 0; i < move.length; i++) {
const { left, right } = move[i].getBoundingClientRect();
if (right > 0) {
const windowWidth = window.innerWidth;
const leftVal = `${Math.round((left / windowWidth) * 100) - 1}%`;
document.getElementById(move[i].id).style.left = leftVal;
}
}
await new Promise((resolve) => setTimeout(resolve, 20));
}
};
From what I understood, you want to animate left property of elements with class "move".
If that's the case you can do:
1st:
add transition property in move class
.move {
transition: left 2s;
}
2nd:
let move = document.getElementsByClassName("move");
for(let item of move) {
item.style.left = "-20%";
}

Why is the ForEach only registering the first element from the array?

Goal
I'm trying to limit the height of each Award Category to the total height of 3 award divs. The reason for this is that it is dynamic and the height may vary depending on what the user puts in for the award info.
Link to CodePen for more context: https://codepen.io/Kaleshe/pen/QWpdJQq
Problem
For some reason, only the first element is being registered and I can't put my finger on why.
According to the error the next item in the loop is undefined, but it is picked up if I console log it outside of the loop like this awardCatContainers[0].querySelectorAll('.award')
Code
const awardCatContainers = document.querySelectorAll( '.award-category-container' );
const button = document.querySelector( '.award .button' );
awardCatContainers.forEach( (container) => {
let awards = container.querySelectorAll( '.award' );
let height = getContainerHeight(awards);
container.style.maxHeight = height + 'px';
});
function getContainerHeight(containerChildren) {
let height = 0;
for ( let a = 0; a <= 3; a++ ) {
height += containerChildren[a].offsetHeight;
}
return height;
}
Edit/Resolution
After looking at the solution provided by H. Udara I was able to confirm that my method worked. Leading me to do further debugging.
I then realised that the error was due to there not being a check for if a category has 3 or less awards. After adding in this check and a const to specify the max elements each category should show, the code now works.
const containers = document.querySelectorAll( '.award-category-container' );
const maxElems = 4;
for ( let c = 0; c < containers.length; c++) {
let awards = containers[c].querySelectorAll( '.award' );
if ( awards.length >= maxElems ) {
let height = setContainerHeight(awards);
containers[c].style.maxHeight = height + 'px';
}
}
// Takes an array of children and uses the total offsetHeight of the first 3 elements to create a height
function setContainerHeight(containerChildren) {
let height = 0;
for ( let a = 0; a < maxElems; a++ ) {
height += containerChildren[a].offsetHeight;
}
return height;
}
The querySelector returns a static NodeList. So when this code is run, probably only one element is rendered in the page. What you can try doing is adding a timeout to this code. This will help you debug the issue.
But do not make this your final code. Because you can never be certain of the timeout period. If the network is too slow it could take well over 3 seconds to load the elements. Look into promises or callbacks and implement your final code. If you are making AJAX calls to get data before creating the elements, use the callback provided by AJAX to determine if the AJAX call is complete.
const awardCatContainers = document.querySelectorAll( '.award-category-container' );
const button = document.querySelector( '.award .button' );
// set a timeout
setTimeout(() => {awardCatContainers.forEach( (container) => {
let awards = container.querySelectorAll( '.award' );
let height = getContainerHeight(awards);
container.style.maxHeight = height + 'px';
})}, 3000);
function getContainerHeight(containerChildren) {
let height = 0;
for ( let a = 0; a <= 3; a++ ) {
height += containerChildren[a].offsetHeight;
}
return height;
}
You need to loop the awards and apply the styling:
awardCatContainers.forEach( (container) => {
let awards = container.querySelectorAll( '.award' );
let height = getContainerHeight(awards);
//container.style.maxHeight = height + 'px';
for (let award of awards) award.style.maxHeight = height + 'px';
});
It's possible that I misunderstood your problem, in which case you will need to provide more information.
getContainerHeight is also strange, you go from 0 to 3. It would help a lot in understanding your exact intention if I knew what your structure is.

What is wrong with my merge sort visualization?

I am trying to create a merge sort visualizer.
The merge sort works in the sense that if I give it an array, it'll spit back a correctly sorted array. The problem is that the "visualization" aspect of my program isn't quite working as I thought it would.
The way I attempted the implementation was that mergeSort() would take the two elements being compared, highlight them in red (no longer focusing on this for now to simplify problem), then swap the height if necessary, then turn the two elements back to the default colour:
const {
useState,
useEffect
} = React;
const numberOfBars = 8; //Number of bars shown on screen
const animationSpeed = 100; //How fast the bars animate
function SortingVisualizer() {
const [arr, setArr] = useState([]);
useEffect(() => {
newArray();
}, [])
function newArray() {
const tempArr = [];
for (let i = 0; i < numberOfBars; i++) {
tempArr.push(Math.floor(Math.random() * 100) + 5)
}
setArr(tempArr);
}
return (
<div id = "main-container" >
<button id = "new-array-button" onClick = {() => newArray()}> New Array </button>
<button id = "merge-sort-button" onClick = {() => mergeSort(arr)}> Merge Sort < /button>
<div id = "bar-container" >
{
arr.map((value, index) => (
<div className = 'bar'
key = {
index
}
style = {
{
backgroundColor: "aquamarine",
height: value + "px"
}
}
/>
))
}
</div>
</div >
)
}
// Recursively breaks down unsortedArray into lowIndex, midIndex, highIndex and pass to mergeSortHelper()
// Initialized aux, lowIndex and highIndex in argument.
function mergeSort(unsortedArray, aux = [...unsortedArray], lowIndex = 0, highIndex = unsortedArray.length - 1) {
// Base case
if (highIndex === lowIndex) return;
// Get midIndex
const midIndex = Math.floor((highIndex + lowIndex) / 2);
// Recursively run left side and right side until base case reached.
mergeSort(unsortedArray, aux, lowIndex, midIndex);
mergeSort(unsortedArray, aux, midIndex + 1, highIndex);
// Merge the left sides and right sides
mergeSortHelper(unsortedArray, aux, lowIndex, midIndex, highIndex);
}
// Does the work of sorting list and animating the height swap
function mergeSortHelper(unsortedArray, aux, lowIndex, midIndex, highIndex) {
let auxkey = lowIndex;
let i = lowIndex;
let j = midIndex + 1;
let barI = undefined;
let barJ = undefined;
// While there are elements in left/right sides, put element in auxillary array
// then increment the indexes by 1.
while (i <= midIndex && j <= highIndex) {
let arrayBars = document.getElementsByClassName('bar');
barI = arrayBars[i].style;
barJ = arrayBars[j].style;
if (unsortedArray[i] <= unsortedArray[j]) {
aux[auxkey] = unsortedArray[i];
auxkey++;
i++;
} else {
let tmpHeight = barJ.height; //swaps value at index i and j
barJ.height = barI.height; //swaps value at index i and j
barI.height = tmpHeight; //swaps value at index i and j
aux[auxkey] = unsortedArray[j];
auxkey++;
j++;
}
}
// Move any remaining elements from unsortedArray into auxillary array.
while (i <= midIndex) {
aux[auxkey] = unsortedArray[i];
auxkey++;
i++;
}
// Copy the auxillary array to the unsortedArray so that in the next iteration, the unsortedArray will reflect the new array
for (let k = 0; k <= highIndex; k++) {
unsortedArray[k] = aux[k];
}
}
ReactDOM.render( < SortingVisualizer / > , document.querySelector("#app"))
#new-array-button {
position: absolute;
}
#merge-sort-button {
position: absolute;
left: 100px;
}
#bar-container {
align-items: flex-end;
background-color: lightgray;
display: flex;
height: 200px;
justify-content: center;
}
.bar {
margin: 0 2px;
width: 20px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
<div id="app"></div>
I've been trying to implement the color change and height swap in different parts of my program but it never seems to work correctly. Where am I going wrong with the visualization?
Removed my jsfiddle link from the question as I am using the on-site code snippet now. But in case anyone still wants it, it's here: https://jsfiddle.net/SushiCode/k0954yep/17/

Making a conditional function more efficient

I want to make a function that modifies a variable based on the given argument.
The function checks a variable and the number in that string. Then via the argument, I specify either increase or decrease the number by 1 (++1).
There is an array as well, that if the number is equal to the length of the array, then it turns to 1 and if the number is less than 1 then it is equal the size of the array. This is to make sure the number of the string does not get less than 1 or more than the length of the array.
the string with the number is Music1. So the circle would be like:
...., Music1, Music2, Music3, Music4, Music1, Music2, Music3, ....
var MyArray = ["Music1", "Music2", "Music3", "Music4"];
var currentMusic = "Music1";
$(".increase").on('click tap', nextMusic);
$(".decrease").on('click tap', previousMusic);
function nextMusic() {
unaryChange('plus')
}
function previousMusic() {
unaryChange('minus')
}
function unaryChange(operation) {
if (currentMusic === "Music4") {
currentMusic = "Music1"
} else if (currentMusic === "Music0") {
currentMusic = "Music4"
}
if (operation === "plus") {
currentMusic = currentMusic.replace(/\d+$/, function(n) {
return ++n
});
} else {
currentMusic = currentMusic.replace(/\d+$/, function(n) {
return --n
});
}
console.log(currentMusic);
$(".text").text(currentMusic);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="increase">increase</button>
<button class="decrease">decrease</button>
<p class="text">value</p>
The above method almost does the job, however I am looking for an easier and more professional solution. It does not look efficient. For example, there must be a better way to specify the argument operation instead of a string like plus, or the conditions.
I need this function to be rewritten in a better way, more professionally and works as described.
Thanks in advance.
It is better to work with array index instead of the values
function unaryChange(operation) {
var currentIndex = MyArray.findIndex(function(item) {
return item === currentMusic;
});
if(operation === 'plus') {
newIndex = currentIndex < MyArray.length - 1 && currentIndex + 1 || 0;
} else {
newIndex = currentIndex > 0 ? currentIndex -1 : MyArray.length -1;
}
currentMusic = MyArray[newIndex]
$(".text").text(currentMusic);
}
In this case whatever the size of the array it will work.
A working example https://jsbin.com/rahomorupa/4/edit?html,js,console,output
Building on Joe's answer I'd suggest you define constants for plus and minus as +1 and -1 respectively to simplify the increment/decrement logic, along with the modulus operator to handle the array wrap-around:
const PLUS = 1;
const MINUS = -1;
function unaryChange(operation) {
var currentIndex = MyArray.findIndex(function(item) {
return item === currentMusic;
});
// If it's invoked as unaryChange(PLUS) or unaryChange(MINUS)
// we don't need any conditional logic to handle the increment,
// and with the % operator we don't need additional bounds overflow
// logic. (This latter bit is complicated somewhat by the need to
// handle a minus step from index 0.)
const {length} = MyArray;
const newIndex = ((currentIndex + operation) % length + length) % length;
currentMusic = MyArray[newIndex]
$(".text").text(currentMusic);
}
The % operator returns the remainder of a division, which conveniently loops back around to 0 when used with an array index against the array length:
const array = ['first', 'second', 'third'];
for (let i = 0; i < 20; i++) {
console.log(array[i % array.length]);
}
You can pass a Boolean for plus, use an arrow function, and a ternary operator:
var MyArray = ["Music1", "Music2", "Music3", "Music4"];
var currentMusic = "Music1";
$(".increase").on('click tap', nextMusic);
$(".decrease").on('click tap', previousMusic);
function nextMusic() {
unaryChange(true)
}
function previousMusic() {
unaryChange(false)
}
function unaryChange(plus) {
currentMusic = currentMusic == "Music4" ? "Music1" : (currentMusic == "Music0" ? "Music4" : currentMusic);
currentMusic = currentMusic.replace(/\d+$/, n => plus ? ++n : --n);
console.log(currentMusic);
$(".text").text(currentMusic);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="increase">increase</button>
<button class="decrease">decrease</button>
<p class="text">value</p>
Since you have an array of music, it's better to use that instead. There's no need to operate from the text, you just need to update the array index to the next value and pass it to the function, and let it get the song name directly.
Since we want to be between the boundaries of 0 and the array length, here's what is used to do this:
Get the next song: (currentTrackIndex + 1) % tracks.length. That will get the next index value and apply modulo to it so it will round back if it exceedes the array length.
Get the previous song: (currentTrackIndex - 1 + tracks.length) % tracks.length. It's pretty much the same as getting the next song, save for the case when the index it's already at zero. If you apply modulo to a negative number, you will get a negative result and will mess up your array index. So instead of using a conditional clause ("if (currentTrackIndex === 0 ...)"), let's add the array length. Why? Because since 0 % n == 0 and n % n == 0, adding the array length will not change the modulo result, while keeping your index as a positive number.
(I changed the name from MyArray to tracks and unaryChange to changeTrack, to give it better meaning clarity)
var tracks = ["Music1", "Music2", "Music3", "Music4"];
var currentTrackIndex = 0;
$(".increase").on('click tap', nextMusic);
$(".decrease").on('click tap', previousMusic);
function nextMusic() {
//It will move to the next track. If it's over the array length, it will reset to 0
changeTrack((currentTrackIndex + 1) % tracks.length)
}
function previousMusic() {
//It will move to the previous song. If it's below zero, it will reset to the last track index
changeTrack((currentTrackIndex + tracks.length - 1) % tracks.length)
}
function changeTrack(newTrackIndex) {
currentTrackIndex = newTrackIndex;
var currentTrack = tracks[currentTrackIndex];
console.log(currentTrackIndex);
$(".text").text(currentTrack);
}
Here's how I'd do it. Since it seems that the word Music is just a prefix used to designate a particular unit, I wont store it over and over again in a array.
As for jQuery? Yeah, nah.
"use strict";
function byId(id){return document.getElementById(id)}
window.addEventListener('load', onLoaded, false);
function onLoaded(evt)
{
let prefix = 'Music';
let count = 4, index=0;
byId('increase').addEventListener('click', function(evt){index++; index %= count; update();}, false);
byId('decrease').addEventListener('click', function(evt){index--; if (index<0) index=count-1; update();}, false);
function update()
{
byId('status').textContent = `${prefix}${index+1}`;
}
}
<span id='status'>Music1</span><br>
<button id='increase'>+</button><button id='decrease'>-</button>
I think this is a good start. Accessing the indices of the array versus the values feels a lot cleaner. Using ternaries cleans up a lot of logic into one line as well.
var MyArray = ["Music1", "Music2", "Music3", "Music4"];
var currentMusic = 0;
$(".increase").on('click tap', unaryChange);
$(".decrease").on('click tap', unaryChange);
function unaryChange() {
if (event.target.className === "increase") {
currentMusic = (currentMusic < 3 ? currentMusic + 1 : 0)
} else {
currentMusic = (currentMusic > 0 ? currentMusic -= 1 : 3)
}
console.log(MyArray[currentMusic]);
$(".text").text(MyArray[currentMusic]);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<button class="increase">increase</button>
<button class="decrease">decrease</button>
<p class="text">value</p>

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

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);
});

Categories

Resources