How to make an invisible clickable area that would toggle scrolling (JavaScript)? - javascript

I'm making a small website built entirely with flexbox columns and rows. One of the rows is horizontally scrollable — howewer, I want it to be scrolled not only with scrolling, but also by clicking either on left or right half of the visible part of the row. Plus, hovering on the left side should change cursor to cursor: w-resize and on the left side — to cursor: e-resize.
see screenshot
see a sketch of what i'm trying to achieve
<div class="project">
<div class="project-images">
<div id="project-image"><img id="image-standart-height" style="max-height: 400px" src="/Users/andrei/Desktop/All Work/portfolio and cv/my website/images/bahorvesna.png" alt=""></div>
<div id="project-image"><img id="image-standart-height" style="max-height: 400px" src="/Users/andrei/Desktop/All Work/portfolio and cv/my website/images/bahorvesna.png" alt=""></div>
<div id="project-image"><img id="image-standart-height" style="max-height: 400px" src="/Users/andrei/Desktop/All Work/portfolio and cv/my website/images/bahorvesna.png" alt=""></div>
<div id="project-image"><img id="image-standart-height" style="max-height: 400px" src="/Users/andrei/Desktop/All Work/portfolio and cv/my website/images/bahorvesna.png" alt=""></div>
</div>
<div class="project-text">
Bahor/Vesna
</div>
</div>
#project-image {
flex: 0 0 auto;
max-height: 400px;
}
#image-standart-height {
max-height: 400px;
}
.project-text {
display: flex;
gap: 20px;
align-items: baseline;
}
I was thinking of creating an invisible layer on a different z-axis level, but I assume it would be difficult not to ruin the flexbox.
Is it possible to make a function in JS to detect when a mouse is entering a certain area of the visible part of the div, change cursor and make the area clickable?

You can also do it without adding any extra div's, by checking the position of the mouse pointer relative to the container. Try it out by clicking the left and right half of the box in the example.
If you have multiple elements, all you need to do is loop the set of elements and put the two event listeners inside the loop.
const containers = document.querySelectorAll('.container');
containers.forEach((container) => {
container.addEventListener('click', (e) => {
const clientRect = e.currentTarget.getBoundingClientRect();
if ((e.pageX - clientRect.left) < clientRect.width / 2)
console.log('Clicked left half');
else
console.log('Clicked right half');
});
container.addEventListener('mousemove', (e) => {
const clientRect = e.currentTarget.getBoundingClientRect();
if ((e.pageX - clientRect.left) < clientRect.width / 2)
container.style.cursor = 'w-resize';
else
container.style.cursor = 'e-resize';
});
});
.container {
width: 300px;
height: 100px;
border: 1px solid #000;
margin: 50px auto;
}
<div class="container"></div>
<div class="container"></div>
<div class="container"></div>
<div class="container"></div>
<div class="container"></div>

Related

css scroll-snap: focusing on the element which got snapped to

I implemented a horizontal grid with some cards in them. The grid uses CSS scroll-snap and it works nicely when navigating with mouse/touchscreen.
The problem occurs when the grid is navigated using a keyboard. Pressing tab after navigating through the grid with arrow keys causes the view to jump back to the element that got the focus, not the card which is current snapped to.
My ideal behaviour when pressing tab is, to focus on the card which is currently snapped to.
Any suggestions to make this possible?
As far as I can tell there is currently no way to handle this natively. Nils Schwebel's answer is not going to be very elegant, but it looks like the best way to go.
Here's a working example:
Note: I've added quite a bit of pure decoration to make it easier to understand, so you may need to pick out the relevant parts after some testing.
const main = document.getElementById("Main"),
sections = document.getElementsByClassName("section");
main.addEventListener('scroll', (e) => {
// Grab the position yo are scrolled to (the top of the viewport)
let pos = main.scrollTop;
for (let i = 0, l = sections.length; i < l; i++) {
// Since our stap-align is centered, get the position of the middle of the viewport relative to the current section's top (if your snap items are not full-height, it might require using half the viewport's height instead)
let relativePos = sections[i].offsetTop - pos + (sections[i].offsetHeight / 2);
// Check if the point we found falls within the section
if (relativePos >= 0 && relativePos < sections[i].offsetHeight) {
sections[i].focus();
break;
}
}
});
body {
margin: unset;
}
main {
display: flex;
flex-wrap: wrap;
align-items: stretch;
justify-content: center;
width: 100%;
height: 100vh;
background-color: #222;
overflow-y: auto;
scroll-snap-type: y mandatory;
}
section {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100vh;
scroll-snap-align: center;
}
section:focus {
border: none;
outline: none;
}
#s1 {
background-color: #d72748;
}
#s2 {
background-color: #b51f7e;
}
#s3 {
background-color: #e64869;
}
#s4 {
background-color: #e79946;
}
section h2 {
color: white;
}
<main id="Main">
<section class="section" id="s1" tabindex="1" aria-labelledby="a1">
<h2 id="a1">AREA 1</h2>
</section>
<section class="section" id="s2" tabindex="1" aria-labelledby="a3">
<h2 id="a2">AREA 2</h2>
</section>
<section class="section" id="s3" tabindex="1" aria-labelledby="a2">
<h2 id="a3">AREA 3</h2>
</section>
<section class="section" id="s4" tabindex="1" aria-labelledby="a4">
<h2 id="a4">AREA 4</h2>
</section>
</main>
I would add a scroll listener and just check if the element is at the top of the scroll view. You may be able to modify one of these solutions: How to check if element is visible after scrolling?

Javascript scrollIntoView only in immediate parent

How do I make scrollIntoView only scroll the immediate parent (e.g. div with overflow-y: scroll;) and not the whole page?
I have a web interface I'm making for an internal, very-specific purpose. Among other elements on my page is a div with a specified height and overflow-y is scroll.
I have data which will periodically appear in this area and I want it to always be scrolled to the bottom (e.g. the console output of a subprocess on some remote server).
If I use scrollIntoView, it scrolls the overflow-y div..... but also scrolls the whole page.
On a computer with a large monitor, this isn't an issue, but on my laptop with a smaller screen it also scrolls the whole window, which is definitely not the intended/desired behavior.
I think what you might be looking for is
.scrollIntoView({block: "nearest", inline: "nearest"});
Where supported (basically anywhere except IE and SafarIE) this will do the 'least' movement to show the element; so if the outer container is visible, but the target element is hidden inside that container -- then it should scroll the inner container but not the page.
I think you're looking for a combination of scrollTop and scrollHeight. You can use the first to set where you want the div to scroll to, and the second to get the height of all the info in the div:
var scrollyDiv = document.getElementById("container");
scrollyDiv.scrollTop = scrollyDiv.scrollHeight
setInterval(() => {
var textnode = document.createElement("P");
textnode.innerHTML = "Whatever"
scrollyDiv.appendChild(textnode);
scrollyDiv.scrollTop = scrollyDiv.scrollHeight
}, 1000)
#container {
height: 200px;
overflow-y: scroll;
}
#big-content {
height: 400px;
background-color: green;
}
<div id="container">
<div id="big-content"></div>
<p>The bottom</p>
</div>
I tried to reproduce your case and I think that scrollIntoView() will not work as you wish. Try to use scrollTop instead.
Hope it will save your time.
const btn = document.getElementById('js-scroll');
btn.addEventListener('click', () => {
const targets = document.querySelectorAll('.scrollable');
targets.forEach(t => {
// Scroll each item
t.scrollTop = t.scrollHeight;
});
});
.scroll-btn {
position:fixed;
left: 10px;
top: 10px;
padding: 10px;
font-weight: 700;
font-size: 16px;
}
.content {
min-height: 100vh;
background-color: #efefef;
padding: 35px 10px;
}
.scrollable {
margin: 15px; 5px;
padding: 5px;
background-color: #fafafa;
height: 500px;
overflow-y: scroll;
overflow-x: hidden;
}
.scrollable-data {
padding: 10px;
margin-bottom: 1px;
min-height: 600px;
background-color: #55b7ab;
}
<button id="js-scroll" class="scroll-btn">Scroll</button>
<section class="content">
<div class="scrollable">
<div class="scrollable-data">Some data 1</div>
<div class="scrollable-data">Some data 1</div>
</div>
<div class="scrollable">
<div class="scrollable-data">Some data 2</div>
<div class="scrollable-data">Some data 2</div>
</div>
<div class="scrollable">
<div class="scrollable-data">Some data 3</div>
<div class="scrollable-data">Some data 3</div>
</div>
</section>
Solution for React 16
Here is an answer for this problem using React 16, for people who want to have a scrollable element scroll to its bottom upon some event. I was having the problem that scrollIntoView() was causing the whole window to adjust, which was undesirable; just need the scrolling element to scroll to the bottom (like a terminal) on demand.
Add a ref to the element with overflowY: "scroll", and use the formula this.body.current.scrollTo(0, this.body.current.scrollHeight) like so:
constructor() {
...
this.body = React.createRef() // Create a ref to your scrollable element
}
...
componentDidUpdate(prevProps, prevState) { // or whatever action you want
// The Important Part, where you scroll to the y-coord
// that is the total height, aka the bottom.
// .current is important, as you want the version of
// that ref that is rendered right now.
this.body.current.scrollTo(0, this.body.current.scrollHeight)
...
}
...
render() {
return (
<div style={style.container}>
<div ref={this.body} style={style.body}> // React ref tagged here
....
</div>
</div>
}

How to align html?

I'm using display flex to display multiple items in one big container (parentDiv). The code is working fine but I get big problems with horizontal centering the items (especially If there are only a few items they should get horizontally centered) so I was using justify-content what leads to big issues:
The parent div is not able to display all items anymore. The first item that gets displayed is the item "04" while it should be "01". How to avoid this?
Please have a look at this code:
#bigDiv {
position: absolute;
width: 100%;
height: 100%;
}
#parentDiv {
width: 90%;
height: 50%;
overflow-y: hidden;
overflow-x: scroll;
text-align: center;
white-space: nowrap;
background: red;
display: flex;
justify-content: center;
align-items: center;
}
.item {
color: white;
background: blue;
flex: 0 0 4%;
margin: 0 3%;
}
.item::after {
content: "";
display: block;
padding-bottom: 100%;
}
<div id="bigDiv">
<div id="parentDiv">
<div class="item">01</div>
<div class="item">02</div>
<div class="item">03</div>
<div class="item">04</div>
<div class="item">05</div>
<div class="item">06</div>
<div class="item">07</div>
<div class="item">08</div>
<div class="item">09</div>
<div class="item">10</div>
<div class="item">11</div>
<div class="item">12</div>
<div class="item">13</div>
<div class="item">14</div>
<div class="item">15</div>
<div class="item">16</div>
</div>
</div>
See this image:
My intentions: The parent div should be able to show all of the items (starting with "01" - and the last element should be the "16"-one)
Note: If there are only 4 or less items they should get centered horizontally. (The reason why I added justify-content).
You're fiting 160% into 100%. And you want it centered. And it works: the 160% total width of the resulting children is nicely centered.
But you're also expecting whatever is outside the parent to be accessible.
It's pretty much like making a child element go outside of its parent by -30% to the left or to the top (by any other method) and expecting the parent to allow you to scroll to it. It's not going to happen!
If it did, the child would no longer be placed at -30%, it would be placed at 0%. Scrollbars will never scroll to left or top negative space. It's by design. You need to take it into consideration when designing your page.
Whenever you center a bigger child into a smaller parent you won't be able to use parent's scrollbars to scroll to the beginning of the child. So anything preventing the child positioning in the parent's left negative space will fix it.

Lower the margin by time when using slideToggle in Jquery

I have this example
$(document).ready(function() {
$("#btn").click(function() {
removeSecondContainer();
});
});
function removeSecondContainer() {
var container = $("#c2");
container.slideToggle(500, function() {
container.remove();
});
}
.container {
margin: 20px auto;
height: 20px;
width: 100px;
background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="btn">Remove the second container</button>
<div class="container" id="c1">
</div>
<div class="container" id="c2">
</div>
<div class="container" id="c3">
</div>
As you can see, the process is not smooth at all. When the second container gets removed, the margin of the second container gets removed too. This causes a pull from the top.
How can I get this smoothly? I thought about lowering the margin by time to 0 when removing the container.
You are facing a margin collapsing issue. As you may notice you don't have 40px between each container like expected but only 20px.
As you can read here:
In CSS, the adjoining margins of two or more boxes (which might or
might not be siblings) can combine to form a single margin. Margins
that combine this way are said to collapse, and the resulting combined
margin is called a collapsed margin.
So when removing the element you decrease both margin at the top and bottom to leave the maring of the first and last element. And when the height of the element reaches 0 and get removed, you create another margin collapsing between the remaining block and thus the jump from 40px to 20px of margin.
And idea to avoid this is to increase height and use linear-gradient to color only the part you want (and leave transparent the part previously used for the margin). Like that the transition will go smoothly as there is no more margin issue.
$(document).ready(function() {
$("#btn").click(function() {
removeSecondContainer();
});
});
function removeSecondContainer() {
var container = $("#c2");
container.slideToggle(500, function() {
container.remove();
});
}
.container {
margin: auto;
height: 60px;
width: 100px;
background: linear-gradient(to bottom, transparent 33.33%, red 33.33%, red 66.67%, transparent 66.67%);/
box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="btn">Remove the second container</button>
<div class="container" id="c1">
</div>
<div class="container" id="c2">
</div>
<div class="container" id="c3">
</div>
Or use flex by adding another container as there is no margin collpasing with flexbox (Margin collapsing in flexbox):
$(document).ready(function() {
$("#btn").click(function() {
removeSecondContainer();
});
});
function removeSecondContainer() {
var container = $("#c2");
container.slideToggle(500, function() {
container.remove();
});
}
.box {
display: flex;
flex-direction: column;
}
.container {
margin: 20px auto;
height: 20px;
width: 100px;
background: red;
box-sizing: border-box;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="btn">Remove the second container</button>
<div class="box">
<div class="container" id="c1">
</div>
<div class="container" id="c2">
</div>
<div class="container" id="c3">
</div>
</div>
Its due to clearing. The bottom margin is not working well with them. Either use float or remove margin-bottom or margin-top to 0
here is Example
edit: update with remove div
https://jsfiddle.net/f5zw18er/3/
You could potentially use jQuery animate with a callback function. It's slightly different because it doesn't include the slide toggle, but in your example it's only being removed, so this could work.
Here we're removing the margin and also hiding the element with the animation, and then finally removing the element in the callback.
$(document).ready(function() {
$("#btn").click(function() {
removeSecondContainer();
});
});
function removeSecondContainer() {
var container = $("#c2");
container.animate({ 'margin' : '-20px auto', 'opacity': 0 }, 500, function(){
container.remove();
});
}
.container {
margin: 20px auto;
height: 20px;
width: 100px;
background: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<button id="btn">Remove the second container</button>
<div class="container" id="c1">
</div>
<div class="container" id="c2">
</div>
<div class="container" id="c3">
</div>

Create a "slot machine" with dynamic height children

I am working on a layout that is similar in nature to a slot machine. Each of its children will be text, and the layout is responsive (so at different resolutions, the children's text may or may not wrap to multiple lines, thus changing the height).
The layout will have a visible "viewport" of 3 items at a time. So that the "viewport" does not shift around, all children need to have the same height. This got me thinking of 2 different routes, but I can't make either work.
Use display: flex. With a column layout, you can have them all stack. The problem with this is, I don't think you can make all children have the same height unless you specify a calculated height on the flex container (thus, you'd have to use js to calculate the max height of all the flex children)
Use display: grid. Out of the box you can make all children have equal height with grid. The problem is, how do hide the overflow guaranteeing you are only showing 3 at a time in the "viewport" for the slot machine?
This layout may not be possible without js calculations, but because of the fact that it has to be completely responsive, I don't want to have to redo the calculations on every window resize. Can anyone think of a way to do this using pure css?
I made a fiddle to show a bare-bones implementation. The "viewport" is the red box, and each item is in the blue box. In the real world, I would be hiding everything outside of the red box and would not want to have to specifically set height on any container.
var scroll = document.getElementById('scroll');
var wrapper = document.getElementById('wrapper');
scroll.addEventListener('click', function() {
wrapper.classList.toggle('scrolled');
});
.example {
display: flex;
}
.container {
height: 90px;
width: 300px;
outline: 1px solid red;
z-index:1;
}
.wrapper {
display: flex;
flex-direction: column;
transition-property: transform;
transition-duration: 500ms;
}
.wrapper.scrolled {
transform: translateY(-300px);
}
.inner {
outline: 1px solid blue;
flex-grow: 1;
height: 30px;
}
.buttons {
margin-right: 3em;
}
<div class="example">
<div class="buttons">
<button id="scroll">Scroll</button>
</div>
<div class="container">
<div id="wrapper" class="wrapper">
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
<div class="inner">Some text</div>
</div>
</div>
</div>
Hey so I think I get what you're going for here and designed a "slot machine" mechanic that scales its inner and outer objects using vh.
So the container object would have something like...
.container {
width: 300px;
height: 90vh;
outline: 1px solid red;
z-index:1;
overflow: hidden;
}
...and the inner object would have something like...
.inner {
outline: 1px solid blue;
flex-grow: 1;
height: 30vh;
}
I also adjusted the Javascript scrolling to base the amount scrolled off of what object is being scrolled to. This can be automated with something like a timer instead of the button:
var scroll = document.getElementById('scroll');
var wrapper = document.getElementById('wrapper');
var innerArray = document.getElementsByClassName('inner');
var innerNum = 1; // Start at the second element
scroll.addEventListener('click', function() {
if (innerNum == innerArray.length-2) { // Check to see if the last item has been scrolled to
innerNum = 0; // Scroll back to the first item if it has
}
//wrapper.classList.toggle('scrolled');
innerArray[innerNum].scrollIntoView({
behavior: 'smooth'
});
innerNum++;
});
Here is the Codepen that demonstrates the example. I can clarify the code further if you would like me to. My apologies if I was way off the ball on what you were going for!

Categories