I am trying to dynamically change button sizes based on which row each element falls in a flexbox container, similar to this:
Internet Radio Genre example
I am using ReactJS and have created an array containing various genres, which I hope to be resized. I am mapping over the array to create each button like so:
class ButtonContainer extends React.Component {
constructor(props) {
super(props);
};
render() {
let items = this.props.data;
let anItem = items.map((item, i) => {
return (
<button className='singleButton'>{item}</button>
)
});
return (<div className='buttonContainer'>{anItem}</div>)
}
}
and the CSS:
.buttonContainer {
display:flex;
flex-flow: row wrap;
justify-content: space-around;
margin-top:1.5rem;
}
I am wondering if there is a way to select which items that fall after a specific breakpoint with JS or CSS? There may just be a totally different way to go about this that has not occurred to me also.
JSFiddle example
Thanks!
In JavaScript you can get the current location of an element in a variety of ways. One of those is element.getBoundingClientRect(). Once you can have the Y location on the page for the element, <button> elements in this case, you can determine where the row breaks are.
In the code below, the row breaks are determined when the Y location value changes by more than slopY from one element being changed to the next. We know that we start at the top row and proceed right to left, top to bottom. It will also work for left to right display. Only the top to bottom traversal matters. The key is that we know we handle all buttons in the first row prior to encountering any in the second row, etc.
The code applies a different class to the elements (<buttons>) in each row. In addition, for testing, it changes the height and applies a different background color to the buttons in each row.
The buttons are not automatically maintained with the correct class (or the size and color applied for testing) when the layout changes. Thus, you will need to re-run applyRowClassesMultipleTimes() if the buttons change layout (e.g. the container they are in is resized). This can be dealt with by adding an appropriate event handler.
In briefly trying, I did not get ReactJS to run with your code in a Stack Overflow snippet. Thus, for the snippet below, I just copied the DOM that your code generates in the JSFiddle you linked.
The code below is also available as a JSFiddle, which includes your ReactJS code.
function applyRowClasses(buttonSelector,rowClassBase){
//The selector passed as the first argument should select all elements for which
// it is desired to have the class applied.
//The second argument is the text portion of the class to be applied. The row
// number (0 at the top) will be appended to that text for the class of each row.
// (e.g. 'row' will result in classes 'row0', 'row1', 'row2', etc.)
var children = document.querySelectorAll(buttonSelector);
var replaceRowRegExp = new RegExp('\\s*' + rowClassBase + '\\d+(\\b|$)','g');
var child;
var testColor = ['red','orange','yellow','green','blue','purple','crimson'
,'cyan','lightgreen','lightblue','Fuchsia'];
var row = -1;
var rowY = 0;
var slopY = 5;
var newHeight = 120;
var newHeightDelta = -10;
for(var i=0,numChildren=children.length;i<numChildren;i++) {
child = children[i];
var rect = child.getBoundingClientRect();
if(rect.top > rowY+slopY){
//New row
row++;
rowY = rect.top;
newHeight += newHeightDelta;
}
var childClass = child.className;
//Remove any old row class. Need to do this because we would need to re-apply
// these classes when the window size changes, or if the layout changes
// in response to the changes we are making here.
childClass = childClass.replace(replaceRowRegExp,'');
childClass += ' ' + rowClassBase + row;
child.className = childClass;
//For testing change background color and height
child.style.backgroundColor = testColor[(row % testColor.length)];
child.style.height = newHeight + 'px';
}
}
function applyRowClassesMultipleTimes(buttonSelector,rowClassBase,count){
//Our changes may affect the button layout. Change
// multiple times giving the browser a chance to re-draw.
// We could calculate what our changes will be, but it's just as
// easy to iterate a few times.
if(count<=1){
return;
}
applyRowClasses(buttonSelector,rowClassBase);
count--;
setTimeout(applyRowClassesMultipleTimes,0,buttonSelector,rowClassBase,count);
}
applyRowClassesMultipleTimes('#root button','row',4);
#root {
display:flex;
color:#444;
}
.buttonContainer {
display:flex;
flex-flow: row wrap;
justify-content: space-around;
margin-top:1.5rem;
}
<body>
<div xmlns="http://www.w3.org/1999/xhtml" id="root"><div data-reactroot=""><div>Popular Genres</div><div class="buttonContainer"><button class="singleButton">Jazz</button><button class="singleButton">Top 40</button><button class="singleButton">Country</button><button class="singleButton">Blues</button><button class="singleButton">Easy Listening</button><button class="singleButton">Rock</button><button class="singleButton">Classical</button><button class="singleButton">Chillout</button><button class="singleButton">80s</button><button class="singleButton">Oldies</button><button class="singleButton">Dance</button><button class="singleButton">Ambient</button><button class="singleButton">Reggae</button><button class="singleButton">Trance</button><button class="singleButton">Hip Hop</button><button class="singleButton">Smooth Jazz</button><button class="singleButton">70s</button><button class="singleButton">House</button><button class="singleButton">Lounge</button><button class="singleButton">Drum and Bass</button><button class="singleButton">Metal</button><button class="singleButton">Meditation</button><button class="singleButton">Heavy Metal</button><button class="singleButton">60s</button><button class="singleButton">Techno</button><button class="singleButton">Pop</button><button class="singleButton">Soul</button><button class="singleButton">90s</button><button class="singleButton">Psytrance</button><button class="singleButton">Latin</button><button class="singleButton">Funk</button><button class="singleButton">Rap</button><button class="singleButton">Bollywood</button><button class="singleButton">50s</button><button class="singleButton">Hindi</button><button class="singleButton">Rockabilly</button><button class="singleButton">Minimal</button><button class="singleButton">Greek</button><button class="singleButton">Comedy</button><button class="singleButton">Alternative</button><button class="singleButton">Reggaeton</button><button class="singleButton">New Age</button><button class="singleButton">Salsa</button><button class="singleButton">Bluegrass</button><button class="singleButton">edm</button><button class="singleButton">Manele</button><button class="singleButton">Indie</button><button class="singleButton">Japanese</button><button class="singleButton">Dancehall</button><button class="singleButton">Classic Rock</button><button class="singleButton">Disco</button><button class="singleButton">Dubstep</button><button class="singleButton">Folk</button><button class="singleButton">goth</button><button class="singleButton">Punk</button><button class="singleButton">Garage</button><button class="singleButton">New Wave</button></div></div></div>
</body>
The following is what the buttons look like without applying the color and sizes:
#root {
display:flex;
color:#444;
}
.buttonContainer {
display:flex;
flex-flow: row wrap;
justify-content: space-around;
margin-top:1.5rem;
}
<body>
<div xmlns="http://www.w3.org/1999/xhtml" id="root"><div data-reactroot=""><div>Popular Genres</div><div class="buttonContainer"><button class="singleButton">Jazz</button><button class="singleButton">Top 40</button><button class="singleButton">Country</button><button class="singleButton">Blues</button><button class="singleButton">Easy Listening</button><button class="singleButton">Rock</button><button class="singleButton">Classical</button><button class="singleButton">Chillout</button><button class="singleButton">80s</button><button class="singleButton">Oldies</button><button class="singleButton">Dance</button><button class="singleButton">Ambient</button><button class="singleButton">Reggae</button><button class="singleButton">Trance</button><button class="singleButton">Hip Hop</button><button class="singleButton">Smooth Jazz</button><button class="singleButton">70s</button><button class="singleButton">House</button><button class="singleButton">Lounge</button><button class="singleButton">Drum and Bass</button><button class="singleButton">Metal</button><button class="singleButton">Meditation</button><button class="singleButton">Heavy Metal</button><button class="singleButton">60s</button><button class="singleButton">Techno</button><button class="singleButton">Pop</button><button class="singleButton">Soul</button><button class="singleButton">90s</button><button class="singleButton">Psytrance</button><button class="singleButton">Latin</button><button class="singleButton">Funk</button><button class="singleButton">Rap</button><button class="singleButton">Bollywood</button><button class="singleButton">50s</button><button class="singleButton">Hindi</button><button class="singleButton">Rockabilly</button><button class="singleButton">Minimal</button><button class="singleButton">Greek</button><button class="singleButton">Comedy</button><button class="singleButton">Alternative</button><button class="singleButton">Reggaeton</button><button class="singleButton">New Age</button><button class="singleButton">Salsa</button><button class="singleButton">Bluegrass</button><button class="singleButton">edm</button><button class="singleButton">Manele</button><button class="singleButton">Indie</button><button class="singleButton">Japanese</button><button class="singleButton">Dancehall</button><button class="singleButton">Classic Rock</button><button class="singleButton">Disco</button><button class="singleButton">Dubstep</button><button class="singleButton">Folk</button><button class="singleButton">goth</button><button class="singleButton">Punk</button><button class="singleButton">Garage</button><button class="singleButton">New Wave</button></div></div></div>
</body>
Related
I have a custom icon element that is only displayed when its specific row in the table is hovered over, but when I scroll down without moving my mouse it doesn't update the hover and maintains the button on the screen and over my table's header. How can I make sure this doesn't happen?
export const StyleTr = styled.tr`
z-index: ${({ theme }) => theme.zIndex.userMenu};
&:hover {
background-color: ${({ theme, isData }) =>
isData ? theme.colors.primary.lighter : theme.colors.white};
div {
visibility: visible;
}
svg[icon] {
display: initial;
}
}
`;
I was just working on something similar to this for a web scraper recently.
Something like this should work:
function checkIfIconInViewport() {
// define current viewport (maximum browser compatability use both calls)
const viewportHeight =
window.innerHeight || document.documentElement.clientHeight;
//Get our Icon
let icon = document.getElementById('icon');
let iPos = icon.getBoundingClientRect();
//Show if any part of icon is visible:
if (viewportHeight - iPos.top > 0 && iPos.bottom > 0) {
icon.style.visibility = visibile;
}
else { icon.style.visibility = hidden; }
//Show only if all of icon is visible:
if (iPos.bottom > 0 && iPos.top >= 0) {
{
icon.style.visibility = visibile;
}
else { icon.style.visibility = hidden; }
//Add && iPos.bottom <= viewportHeight to the if check above for very large elements.
{
//Run function everytime that the window is scrolled.
document.addEventListener('scroll', checkIfIconInViewport);
Basically, every time a scroll event happens, we just check to see if the top & bottom of our element (the icon in your case) are within the bounds of the viewport.
Negative values, or values greater than the viewport's height mean that the respective portion of the element is outside the viewport's boundary.
Hopefully this helps! If you are dealing with a large quantity of objects, it may make sense to bundle the objects you are tracking together into an array and check each of them in a single function call to avoid saving function definitions for each individual object.
Edit: I just realized that I misunderstood your issue a bit. I think you can get by with just the bottom part of the code, and when a scroll event happens, set the icon's visibility to hidden. Assuming you want to hide it whenever the user scrolls?
Have you tried getting the scroll position of the DOM, then disabling (removing) the element once a certain scroll position is reached?
I am trying to recreate the WoW talent calculator as seen here - https://classicdb.ch/?talent#h
Project files - https://codepen.io/jjchrisdiehl/pen/gNQLgR
This is a project to help better understand Javascript, so please avoid any jQuery workarounds for this - thanks.
If you look at the HTML code, you'll see I have the HTML set up as div's with the class 'item' and then I have another div nested inside of the 'item' div with the class 'points'.
<div class="item two nature_wispsplode button" data-max="5">
<div class="points"></div>
</div>
<div class="item three fire_fireball button" data-max="3">
<div class="points"></div>
</div>
The idea is to append a Javascript event listener called logMouseButton to every div with the class 'item'. This will listen for a click and log whether it was a left or right mouse click.
/* Get item div element for addEventListener*/
let itemButton = document.getElementsByClassName("item");
/* Apply logMouseButton to every itemButton */
for (var i = 0; i < itemButton.length; i++) {
itemButton[i].addEventListener("mouseup", logMouseButton, false);
}
Now the logMouseButton code was hacked from the Moz page on MouseEvents .button. My thoughts are to use a switch to manage adding or subtracting points to each item's individual counters.
https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
/* Set Counter */
var counter = 0;
/* Add or subtract points based on button clicked */
function logMouseButton(e) {
/* Set the max number of clicks in points counter based off of data-max attribute */
var maxPoints = this.getAttribute("data-max");
if (typeof e === "object") {
switch (e.button) {
case 0:
if (counter == 0 || counter < maxPoints) {
counter = counter + 1;
}
document.querySelector(".item .points").innerHTML = counter;
// alert(counter);
break;
case 1:
log.textContent = "Middle button clicked.";
break;
case 2:
if (counter > 0) {
counter = counter - 1;
}
document.querySelector(".item .points").innerHTML = counter;
break;
default:
log.textContent = `Unknown button code: ${btnCode}`;
}
}
}
Left click increments, right click decrements. As you can see in my codepen project, the right/left clicks work, but only for one item.
My question is - how would I apply this to each item individually so that they manage their own counter independently of the other items?
Thanks!
Note - I managed to get the counters working with some direction from klugjo. I ended up logging the HTML value of 'points', incrementing the value, then adding the new value back to the innerHTML: https://codepen.io/jjchrisdiehl/pen/JgXzKe
If you have any insights as to why this isnt the best way to do it, or why another way is better, let me know!
You need to access the div that corresponds to the element that was clicked.
Using document.querySelector(".item .points") will always select the first element in the DOM.
You can use e.target to access the element that was clicked, and since what you need is the only child of that element, you can replace
document.querySelector(".item .points").innerHTML = counter;
with
e.target.children[0].innerHTML = counter;
Then you will run into another issue, which is that your counter is global and common to all the buttons.
So you will have to use a hashmap (JS Object) instead of a single integer for counter
var counter = {};
An approach is to select the .item elements and create an array of .item length (number of .item elements in the page) to store the counter for each one individually.
Here's a demo, it contains some helpful comments :
/** selecting the elements with ".item" class and declaring an array to store each element counter separately **/
const items = document.querySelectorAll('.item'),
countArr = new Array(items.length);
/** loop through the elements and add "mouseup" listener (to ensure catching both left and right clicks) for each element **/
/**
* el: the current element from the ".item" elements collection
* i: the index of that elemnt in the collection
**/
items.forEach((el, i) => {
countArr[i] = 0; /** initialize each array element with 0 so we can count later (new Array puts "undefined" as the array elements values) **/
/** add the "mouseup" listener **/
el.addEventListener('mouseup', e => {
let txtCount = el.querySelector('.count'); /** selecting the "span" that contains the counter from the current elemnt (don't forget that we're looping in ".item" elements collection) **/
if(e.which === 1) countArr[i]++; /** left click **/
else if(e.which === 3) countArr[i]--; /** right click **/
txtCount.textContent = countArr[i]; /** update the "span" wih the calculated counter as left click adds 1 and right click removes 1 from the counter of each element **/
});
});
/** basic styling for the demo **/
.item {
display: inline-block;
margin: 15px 0;
padding: 8px;
border: 2px solid teal;
user-select: none;
}
.item .count {
display: inline-block;
background-color: #181818;
color: #fff;
font-size: 1.2rem;
font-weight: bold;
padding: 8px;
border-radius: 4px;
}
<div class="item">counter = <span class="count">0</span></div>
<div class="item">counter = <span class="count">0</span></div>
<div class="item">counter = <span class="count">0</span></div>
I have a responsive website, with some jQuery code, of which some is the following:
<script>
$(document).ready(function(){
$("#D1000C36LPB3").click(function(){$("#D1000C36LPB3_details").show();});
$("#D1200C36LPB3").click(function(){$("#D1200C36LPB3_details").show();});
$("#D1-3CA36LPB3").click(function(){$("#D1-3CA36LPB3_details").show();});
$("#D1-0CA36LPB3").click(function(){$("#D1-0CA36LPB3_details").show();});
$("#D700S36LPB3").click(function(){$("#D700S36LPB3_details").show();});
$("#D700S24LMB3").click(function(){$("#D700S24LMB3_details").show();});
});
</script>
All of the div elements above (#D1000C36LPB3_details, #D1200C36LPB3_details, #D1-3CA36LPB3_details...) have a CSS display property value of none, so by default they aren't visible until you click on one of the div elements above (#D1000C36LPB3, #D1200C36LPB3, #D1-3CA36LPB3...) and then the corresponding div is displayed.
However, when the jQuery script runs, it sets the corresponding div display value to block. When the viewport's/window's width is smaller than say 400 px, I want the script to display them with position: fixed;.
My suggestion
I've figured out I can display them with fixed position using:
$("#corresponding_ID").css("display", "fixed");
But I still have to not let jQuery run the first script (the one using .show()).
Don't set css styles directly this way. As already commented, use e.g. a .visible class and let css media queries decide. Example:
#media screen and (max-width: 399px) {
.visible {
display: fixed;
}
}
#media screen and (min-width: 400px) {
.visible {
display: block;
}
}
Then, in your click handler, go as follows:
$("#D1000C36LPB3").click(function(){$("#D1000C36LPB3_details").addClass('visible');});
Also, if your details containers all follow that naming scheme with affixing _details to the id, it'd be easier to put all ids in an array and iterate over that:
$(document).ready(function(){
var ids = [ "#D1000C36LPB3", "#D1200C36LPB3", "#D1-3CA36LPB3", "#D1-0CA36LPB3", "#D700S36LPB3", "#D700S24LMB3"];
for (var i = 0; i < ids.length; i++) {
$(ids[i]).on('click', function () { $(ids[i]+'_details').addClass('visible'); }
}
};
Easy way to check for browser width with Jquery:
var width = $(window).width();
if (width >= 1024) {
-- Code to execute here --
}else{
-- Other code to execute here --
}
Then you can adjust the width you are looking and update the >= based on what you want to do.
Let me know if this doesn't make sense.
Please take a look at this basic example:
http://tympanus.net/Blueprints/ResponsiveFullWidthGrid/
Now imagine that by clicking a cat box, I would need (especially on small to medium screens) to add a 100%-width text box (say a description of the clicked cat) below the clicked cat's row. That text box should push down the rest of the rows.
I am full css/js/frontend developer but I never faced a problem like this. It's also the first time I'm going to use a flexbox layout. With a fixed layout would be quite trivial, but in this case I cannot figure out a good way of doing it. One of the things to solve for example is: where should I put the box (relative to the clicked box?), and should I change position via javascript based on current items-per-row or maybe there a smarter css way?
Any idea is appreciated.
That was an interesting challenge :)
The only way to know where to place the expanded area (I called it infoBox), is to identify the 1st node of the next line, and then insert it before it. If there is no node on the last line, we can append it to the end of the ul.
I've also added a window.resize event handler that will close the infoBox, so it won't break the responsive layout, and a close button.
Working example - fiddle.
HTML was copy paste from the codrop article.
JS
var rfgrid = document.querySelector('.cbp-rfgrid');
var items = Array.from(document.querySelectorAll('.cbp-rfgrid > li'));
/** Create infoBox **/
var infoBox = document.createElement('div');
infoBox.classList.add('infoBox');
infoBox.innerHTML = '<div class="close">X</div><div class="content"></div>';
infoBoxClose = infoBox.querySelector('.close');
infoBoxContent = infoBox.querySelector('.content');
/** add close button functionality **/
infoBoxClose.addEventListener('click', function() {
rfgrid.removeChild(infoBox);
});
/** remove infoBox on resize to maintain layout flow **/
window.addEventListener('resize', function() {
rfgrid.removeChild(infoBox);
});
items.forEach(function (item) {
item.addEventListener('click', function (event) {
event.preventDefault();
var insertReference = findReference(this); // get refence to next line 1st node
infoBoxContent.innerHTML = items.indexOf(this); // example of changing infoBox content
if(insertReference) {
rfgrid.insertBefore(infoBox, insertReference); // insert infoBox before the reference
} else {
rfgrid.appendChild(infoBox); // insert infoBox as last child
};
});
});
/** find reference to 1st item of next line or null if last line **/
function findReference(currentNode) {
var originalTop = currentNode.offsetTop; // get the clicked item offsetTop
do {
currentNode = currentNode.nextSibling; // get next sibling
} while (currentNode !== null && (currentNode.nodeType !== 1 || currentNode.offsetTop === originalTop)); // keep iterating until null (last line) or a node with a different offsetTop (next line)
return currentNode;
}
CSS (in addition to the original)
.infoBox {
position: relative;
width: 100%;
height: 200px;
padding: 20px 0 0 0;
clear: both;
background: paleturquoise;
}
.infoBox > .close {
position: absolute;
top: 5px;
right: 5px;
cursor: pointer;
}
CSS
.page{
width: 275px;
hight: 380px;
overflow: auto;
}
HTML
<div class="page">dynamic text</div>
How to create a new div when the dynamic text is overflowing past the fixed height of the div?
Example:
<div class="page">Some dynamic texts will appear here</div>
When the dynamic text is overflowing past the fixed height of the div, the content above will be appear like this.
<div class="page">Some dynamic</div>
<div class="page">texts will</div>
<div class="page">appear here</div>
I've tried using wordwrap function in PHP wordwrap($dynamic_text, 600, '</div><div class="page">'); it's can running, but it had a problem when the character was copied from Ms.Words.
So, by detecting the overflowing text, cut it, and then paste it into the new div element is the better solustion, i guess. But, I don't know how to do this solution using JQuery or Javascript.
Any ideas? Thanks in advance!
You can do it, and it's way more than just a couple lines of code. It took a very experienced developer a couple days. Sorry, can't share the code.
Javascript: Put the whole content into the div. You may keep it hidden or out of the DOM for a while. Traverse the div's children. Find the one whose top+scrollHeight exceeds the div's height. Traverse it recursively. Eventually, you will either find an indivisible element, e.g., an image, that doesn't fit, or a position within a text node to split the text at. Remove that part and all further elements from the div. Add them to a new one.
There are a lot of details to address, so it's not simple. But doable.
I just had something similar working today while I was searching for an answer but there doesn't seem to be anything straight forward.
Although I am using Array.reduce() you should be able to do this with Array.forEach() or any other iterating code you like.
words.reduce(function(acc, value)
This is done by calculating if the element will overflow if we add another word to it before we actually render it. The hacky thing here is to add another block element inside of it with visibility: hidden.
element.innerHTML = '<div style="visibility: hidden; height: 100%; width=100%">' + textToBeAdded + '</div>';
That way the block element still takes its parents dimensions and the parent element can be checked for overflow.
The way to check for overflow is to compare the element's scrolling height to its height:
if (element.scrollHeight > element.offsetHeight)
If it overflows we leave it as is and create a new element and put the current value (word) in the iteration. Then we attach it to the same DOM tree as the previous element (as its parent's child... like having a new brother 😜)
var newPageEl = document.createElement('div');
newPageEl.classList = 'page';
newPageEl.textContent = word;
parentElement.appendChild(newPageEl);
Hope this makes sense.
var page = document.getElementsByClassName('page')[0];
if (page.scrollHeight > page.offsetHeight) {
// is overflowing
fixOverflow(page);
}
function fixOverflow(element) {
var words = element.textContent.split(' ');
// delete previous text content
element.textContent = '';
words.reduce(function(acc, value) {
// store current element's text
var currentElementText = element.textContent.toString().trim();
var textToBeAdded = currentElementText + ' ' + value;
element.innerHTML = '<div style="visibility: hidden; height: 100%; width=100%">' + textToBeAdded + '</div>';
if (element.scrollHeight > element.offsetHeight) {
// is overflowing with the new word
element.innerHTML = "";
// leave the last page element as is
element.textContent = currentElementText;
// create another element with the new value to be added
// ** IMPORTANT replace the memory of the previous element variable
element = createPageElement(value);
// add it to the same DOM tree as the previous page element
page.parentElement.appendChild(element); // could also be document.getElementById('page-container').appendChild(element);
} else {
// if not overflowing add another word
element.innerHTML = currentElementText + ' ' + value;
}
}, "");
}
function createPageElement(text) {
// create element with class page
var newPageEl = document.createElement('div');
newPageEl.classList = 'page';
newPageEl.textContent = text;
return newPageEl;
}