CSS Transition Not Working with Javascript [duplicate] - javascript

This question already has answers here:
Transitions on the CSS display property
(37 answers)
Closed 3 years ago.
I'm sure that I'm making a rookie mistake here, but I've researched this solution all day, and I can't seem to understand why my code below isn't working.
The use case is a button that opens up a modal box inside a semi-transparent overlay, with the overlay covering everything else on the screen, including the button that opened it. The button is currently opening the modal and overlay just fine, and clicking anywhere outside of the modal box does indeed close it. But I don't understand why my set CSS transition isn't working.
I'm at a loss on this one, so I'd very much appreciate any advice that more seasoned developers can offer. Thank you so much in advance!
Best,
Josh
var modalOverlay = document.getElementById('modalOverlay');
var modalButton = document.getElementById('modalButton');
modalButton.addEventListener('click', openModal);
window.addEventListener('click', closeModal);
function openModal() {
modalOverlay.style.display = "flex";
modalOverlay.style.opacity = "1";
}
function closeModal(event) {
if (event.target == modalOverlay) {
modalOverlay.style.opacity = "0";
modalOverlay.style.display = "none";
}
}
.modal-overlay {
display: none;
opacity: 0;
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
transition: all 1s ease-in-out;
}
.modal-box {
width: 200px;
height: 200px;
background-color: blue;
}
<button id="modalButton" class="modal-button">Open Modal</button>
<div id="modalOverlay" class="modal-overlay">
<div id="modalBox" class="modal-box"></div>
</div>

display is not a property that can be transitioned. You need to make your animation take multiple steps. When you click the button, the modal should be made flex, but it should still be transparent. Then you need to transition the opacity up to 1 which is what CSS transitions can do.
You need to do the inverse whenever you close the modal. Transition back to opacity 0 and after the transition is done, mark it display: none.
var modalOverlay = document.getElementById('modalOverlay');
var modalButton = document.getElementById('modalButton');
modalButton.addEventListener('click', openModal);
window.addEventListener('click', closeModal);
function openModal() {
// This will cause the browser to know
// that the element is display flex for a frame
requestAnimationFrame(() => {
modalOverlay.classList.add("modal-overlay--open");
// Then when we wait for the next frame
// the browser will now know that it needs
// to do the transition. If we don't make
// them separate actions, the browser
// will try to optimize the layout and skip
// the transition
requestAnimationFrame(() => {
modalOverlay.classList.add("modal-overlay--open-active");
});
});
}
function closeModal(event) {
if (event.target == modalOverlay) {
modalOverlay.classList.remove("modal-overlay--open-active");
setTimeout(() => {
modalOverlay.classList.remove("modal-overlay--open");
}, 1100);
}
}
.modal-overlay {
display: none;
opacity: 0;
position: fixed;
top: 0;
left: 0;
z-index: 10;
width: 100vw;
height: 100vh;
align-items: center;
justify-content: center;
background-color: rgba(0,0,0,0.5);
transition: all 1s ease-in-out;
}
.modal-overlay.modal-overlay--open {
display: flex;
}
.modal-overlay.modal-overlay--open-active {
opacity: 1;
}
.modal-box {
width: 200px;
height: 200px;
background-color: blue;
}
<button id="modalButton" class="modal-button">Open Modal</button>
<div id="modalOverlay" class="modal-overlay">
<div id="modalBox" class="modal-box"></div>
</div>
Let's draw a little insight from how some other frameworks deal with these kinds of transitions. For example, Vue.js separates its enter/leave transitions into 6 sorts of phases:
Enter: Starting state, added before entry (for you, display: flex and fully transparent)
Enter Active: The transitioning state that sets the transition "target" (opacity towards 1 in your case)
Enter To: What it should be after the transition is complete (we aren't going to bother with this)
Leave: About to start leaving (nothing really needs to change here for us)
Leave Active: Set the target state for your element so that it knows what to transition to (for us, we just remove the class that says opacity: 1)
Leave To: We don't need this either
The main thing that we need to consider is that we need the browser to have the element in the page and "rendered" so that it will consider it for transitioning. That's why we add, in our example, the modal-overlay--open class which makes it flex. We then wait just a second and add the transition target class modal-overlay--open-active which causes the element to actually transition.
Then we do the same thing in reverse: remove modal-overlay--open-active so the browser knows to transition the element back to the "normal" style. We set a timeout to remove the display: flex class after the transition is done. You could use event listeners for this, but it's overkill for such an example.

The display property doesn't play well with transition. Instead, just toggle between opacity 1 and 0.
const modalOverlay = document.getElementById('modalOverlay');
const modalButton = document.getElementById('modalButton');
modalButton.addEventListener('click', openModal);
window.addEventListener('click', closeModal);
function openModal() {
modalOverlay.style.opacity = "1";
modalButton.style.opacity = "0";
}
function closeModal(event) {
if (event.target == modalOverlay) {
modalOverlay.style.opacity = "0";
modalButton.style.opacity = "1";
}
}
.modal-overlay {
opacity: 0;
width: 100vw;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background-color: rgba(0, 0, 0, 0.5);
}
.modal-overlay,
.modal-button {
transition: opacity 1s ease-in-out;
}
.modal-box {
width: 200px;
height: 200px;
background-color: blue;
}
.modal-button {
position: absolute;
z-index: 2;
}
<button id="modalButton" class="modal-button">Button</button>
<div id="modalOverlay" class="modal-overlay">
<div id="modalBox" class="modal-box"></div>
</div>

Related

How to disable click events when execution is going on in React?

I have a screen
where I want to disable all the events when execution is going on.
When I click on the Execute button, an API is called which probably takes 4-5 minutes to respond. During that time, I don't want the user to click on the calendar cells or Month navigation arrows.
In short, I want to disable all the events on the center screen but not the left menu.
Is it possible to do that?
Yes sure, you can add a class with css pointer-events rule. Set it on the whole table and it will disable all events. Just add it when request starts and remove it when it ends. You can achieve that, by having a boolean variable isLoading in your state, or redux store and based on that add or remove the no-click class.
.no-click {
pointer-events: none;
}
You can use classic loading overlay box over your content when some flag (i.e. loading) is true.
Other way to do it is to use pointer-event: none in CSS. Use same flag to set class to your content block.
Here is a working example in codesanbox:
https://codesandbox.io/s/determined-dirac-fj0lv?file=/src/App.js
Here is code:
export default function App() {
const [loadingState, setLoadingState] = useState(false);
const [pointerEvent, setPointerEvent] = useState(false);
return (
<div className="App">
<div className="sidebar">Sidebar</div>
<div
className={classnames("content", {
"content-pointer-event-none": pointerEvent
})}
>
<button onClick={() => setLoadingState(true)}>
Show loading overlay
</button>
<button onClick={() => setPointerEvent(true)}>
Set pointer event in css
</button>
{loadingState && <div className="loading-overlay"></div>}
</div>
</div>
);
}
.App {
font-family: sans-serif;
text-align: center;
display: flex;
position: absolute;
width: 100%;
height: 100%;
}
.content {
flex: 1;
position: relative;
}
.loading-overlay {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.4);
z-index: 1;
}
.content-pointer-event-none {
pointer-events: none;
}

Prevent modal from closing when click didn't initiate outside of content?

I've created a simple modal that is allowed to be closed when you click outside of the content area. This is by design but it has an unintended side-effect. If I click anywhere in the content area (for example in a text field) and drag the mouse to beyond the content area and then release the click it will close the modal. I often have a habit of doing this and I can see how average users will perceive this as a bug so I'm trying to nip it prior to release.
var modal = document.getElementById("modal-container");
function openModal() { modal.classList.add("active"); }
function closeModal() { modal.classList.remove("active"); }
window.onclick = function (event) {
if (event.target == modal)
closeModal();
}
html, body {
margin: 0;
height: 100%;
}
.modal-container.active { top: 0; }
.modal-container {
position: absolute;
top: -500vh;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
<button onclick="openModal();">Open the Modal</button>
<div id="modal-container" class="modal-container">
<div class="modal-content">
<input type="text" />
</div>
</div>
To test it properly:
Click the 'Open the Modal' button.
Click in the text box at the center of the white panel.
Enter some text.
Press the left mouse button down in the text box.
Drag the mouse beyond the bounds of the white panel.
Release the mouse button.
The modal should now be closed.
Is there a way to prevent this without tracking the coordinates of the mouse?
Perhaps onmousedown instead of click?
That worked! Just need more coffee this morning I suppose. Going to write up a thorough answer later today for future readers.
Before you answer yourself with a valid cause (as noted in your Question Edit) -
take in consideration:
onmousedown might not always be the desired UX. (Sometimes experienced users to undo a mousedown not being registered as a click they on purpose move the mouse over another element for the mouseup event just to retain the current state.)
Remove inline JavaScript
Assign listeners using Element.addEventListener() to any button having the data-modal attribute
Use data-modal="#some_modal_id" even no the container element
Finally: use if (evt.target !== this) return;
const el_dataModal = document.querySelectorAll('[data-modal]');
function toggleModal(evt) {
if (evt.target !== this) return; // Do nothing if the element that propagated the event is not the `this` button which has the event attached.
const id = evt.currentTarget.getAttribute('data-modal');
document.querySelector(id).classList.toggle('active');
}
el_dataModal.forEach(el => el.addEventListener('click', toggleModal));
html, body {
margin: 0;
height: 100%;
}
.modal-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
opacity: 0; /* ADDED */
transition: 0.26s; /* ADDED */
visibility: hidden; /* ADDED */
}
.modal-container.active {
opacity: 1; /* ADDED */
visibility: visible; /* ADDED */
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
<button data-modal="#modal-container">Open the Modal</button>
<div id="modal-container" class="modal-container" data-modal="#modal-container">
<div class="modal-content">
<input type="text">
<br><br>
<button data-modal="#modal-container">CLOSE MODAL TEST</button>
</div>
</div>
This is working example. Think, it matches that one you need))
var clickTarget = null;
var modal = document.getElementById("modal-container");
function openModal() {
modal.classList.add("active");
document.body.addEventListener('mousedown', onModalMouseDown, false);
document.body.addEventListener('mouseup', onModalMouseUp, false);
}
function closeModal() {
modal.classList.remove("active");
document.body.removeEventListener('mousedown', onModalMouseDown);
document.body.removeEventListener('mouseup', onModalMouseUp);
}
function onModalMouseDown(event) {
clickTarget = event.target;
}
function onModalMouseUp() {
if (clickTarget === modal) {
closeModal();
}
}
html, body {
margin: 0;
height: 100%;
}
.modal-container.active { top: 0; }
.modal-container {
position: absolute;
top: -500vh;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
.modal-trigger-btn {
margin: 20px;
font-size: 16px;
}
<button onmousedown="openModal();" class="modal-trigger-btn">Open the Modal</button>
<div id="modal-container" class="modal-container">
<div class="modal-content">
<input type="text" placeholder="Start to drag outside..."/>
</div>
</div>
To answer this question myself, I thought about how the onclick event was working. A click is defined as the mouse button being pressed down, and then released. Both of those points have to occur to cause the onclick event to be raised (though you can't really have one without the other happening at some point before or after).
I haven't found any real documentation on the execution path below so it based on logical deduction. If you have any documentation on this please link it in a comment so that I can review it and adjust my answer for future readers.
User presses down the mouse button.
The onmousedown event is raised.
User releases the mouse button.
The onmouseup event is raised.
The onmouseclick event is raised.
I did write a test up to verify these results:
var ePath = document.getElementById("executionPath");
document.body.onmousedown = function (event) { ePath.innerHTML += "On Mouse Down<br>"; }
document.body.onmouseup = function (event) { ePath.innerHTML += "On Mouse Up<br>"; }
document.body.onclick = function (event) { ePath.innerHTML += "On Click<br>"; }
html, body { height: 100%; }
<p id="executionPath">Click the Window<br></p>
I believe the unintended behavior is caused by when the target is set for the onclick event. I think there are three possibilities (below from most to least likely) for when this is set, none of which I can confirm or deny:
The target is set when the mouse button is released.
The target is set when the mouse button is pressed down, then again when the mouse button is released.
The target is set continuously.
After analyzing my thoughts I determined that for my scenario onmousedown is likely to be the best solution. This will ensure that the modal closes only if the user initiates the click outside of the content area. A good way to couple this with onmouseup to ensure a full click is still achieved is demonstrated below. Though in my case I am okay with simply using onmousedown:
var initialTarget = null;
var modal = document.getElementById("modal-container");
function openModal() { modal.classList.add("active"); }
function closeModal() { modal.classList.remove("active"); }
window.onmousedown = function (event) { initialTarget = event.target; }
window.onmouseup = function (event) {
if (event.target == initialTarget)
closeModal();
}
html, body { height: 100%; }
.modal-container.active { top: 0; }
.modal-container {
position: absolute;
top: -500vh;
left: 0;
width: 100%;
height: 100%;
display: grid;
background-color: rgba(0, 0, 0, 0.75);
}
.modal-content {
height: 50%;
width: 50%;
margin: auto;
background-color: #fff;
display: flex;
align-items: center;
justify-content: center;
}
<button onclick="openModal();">Open the Modal</button>
<div id="modal-container" class="modal-container">
<div class="modal-content">
<input type="text" />
</div>
</div>
The snippet above ensures that the click starts and ends on the modal container prior to closing the modal. This prevents the modal from closing if the user accidentally initiates a click outside of the content area and drags their mouse into the content area to complete the click. The same is true in the reverse order, and the modal will only close if the click is initiated and completed on the modal container.
The only thing I can't figure out is when the target for onclick is set which is probably more important in a proper explanation on the root cause of this issue so feel free to let me know!

How to delay a toggleClass event to prevent spam changes?

I have used the jQueryUI toggleClass delay function however I realised that it creates a delay before the event happens, rather than setting a time before it can be activated again.
I have a few DIVs that switch between classes when they are hovered over using the toggleClass method. However if the cursor is moved quickly over them they keep swapping and it looks buggy. I would like to prevent this by perhaps allowing the toggle to only happen once every 1 second or something.
Is this possible?
$(".landingImage").hover(function () {
var curHeight = this.clientHeight;
$(this).siblings('.imageCover').css("height", curHeight / 1.25);
$(".leftLanding").toggleClass("extraMargin");
$(".rightLanding").toggleClass("extraMargin");
$(this).siblings(".imageCenter").fadeOut(50);
}, function () {
$(this).siblings('.imageCover').css("height", "0px");
$(this).siblings(".imageCenter").fadeIn(600);
});
#landing-images {
position: relative;
width: 100%;
height: auto;
overflow: auto;
margin-top: 6%;
margin-bottom: 5%;
}
.leftLanding {
display: flex;
position: relative;
width: 85%;
margin-left: 3%;
cursor: pointer;
transition: all 0.5s ease;
}
.rightLanding {
display: flex;
position: relative;
width: 85%;
margin-right: 3%;
cursor: pointer;
transition: all 0.5s ease;
}
.extraMargin {
margin-left: 12%;
margin-right: 12%;
}
.landingImage {
position: relative;
display: block;
width: 100%;
height: auto;
z-index: 90;
transition: all 0.5s ease;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="landing-images">
<a href="menu.html"><div class="leftLanding left">
<div class="imageCover">
</div>
<div class="imageCenter">
</div>
<img class="landingImage" src="assets/landingIMG1.png">
</div></a>
<a href="contact.html"><div class="rightLanding right">
<div class="imageCover">
</div>
<div class="imageCenter">
</div>
<img class="landingImage" src="assets/landingIMG3.png">
</div></a>
<a href="burritos.html"><div class="leftLanding left">
<div class="imageCover">
</div>
<div class="imageCenter">
</div>
<img class="landingImage" src="assets/landingIMG2.png">
</div></a>
</div>
If you want to conditionally delay the hover event, you can delay the action using window.setTimeout.
The idea is that you
- set the change to wait for a short while
- set the mouse out behaviour to cancel the pending change.
This code will do something like it:
var delay;
$(".landingImage").hover(function () {
window.setTimeout(doit,250); // queue action
}, function () {
cancel(); // clear hover, if still pending
$(this).siblings('.imageCover').css("height", "0px");
$(this).siblings(".imageCenter").fadeIn(600);
});
function doit() {
var $landingImage=$(".landingImage");
var curHeight = $landingImage.clientHeight;
$landingImage.siblings('.imageCover').css("height", curHeight / 1.25);
$(".leftLanding").toggleClass("extraMargin");
$(".rightLanding").toggleClass("extraMargin");
$landingImage.siblings(".imageCenter").fadeOut(50);
delay=undefined;
}
function cancel() {
if(delay) delay=window.clearTimeout(delay);
}
Because setTimeout is a window method, this is no longer valid. Here I have set a variable to the original element. I generally prefix jQuery variables with $, but that is just a matter of taste.
I haven’t tested in your environment, of course.
As regards doing it the CSS way:
If you want to avoid instant changes, you can add the following to your CSS:
transition-delay: .25s;
or whatever suits you.
transition-delay can also be combined with the general transition property (put it last), but try this first to see how it works.

Animating height property :: HTML + CSS + JavaScript

I have noticed this 'issue' lately when trying some stuff.
Say I want to create a drop-down menu or an accordion.
This is my HTML:
<div class="wrapper" onclick="toggle()">
I want to be animated!
<div class="content">
Was I revealed in a timely fashion?
</div>
</div>
Stylesheets:
.wrapper {
background: red;
color: white;
height: auto;
padding: 12px;
transition: 2s height;
}
.content {
display: none;
}
.content.visible {
display: block;
}
JavaScript:
function toggle () {
var content = document.getElementsByClassName('content')[0];
var test = content.classList.contains('visible');
test ? content.classList.remove('visible') :
content.classList.add('visible');
}
I am trying to achieve a nice, smooth animation when we toggle the state of the content. Obviously this does not work. Anyone can explain to me why it does not work and how to fix it? Many thanks.
Link to the JSFiddle.
First things first, some CSS properties CANNOT be transitioned, display is one of them, additionally only discrete values can be transitioned, so height: auto cannot as well.
In your case the problem is with height: auto, while there are a few hacks for doing this, if you are just showing and hiding stuff, why not add, and use jQuery's toggle instead?
$(".content").toggle("slow");
jsFiddle
--EDIT (without jQuery)--
Because it's the auto that is giving us problems, we can use javascript to replace auto with a value in pixels and then use the css transition normally, if your content doesn't have a scroll, we can easily take that value from the scrollHeight property:
function toggle () {
var content = document.getElementsByClassName('content')[0];
var test = content.classList.contains('visible');
console.log(test);
if (test) {
content.classList.remove('visible')
content.style.height = "0px";
} else {
content.classList.add('visible');
content.style.height = content.scrollHeight + "px";
}
}
Css
.wrapper {
background: red;
color: white;
height: auto;
padding: 12px;
transition: 2s height;
}
.content {
height: 0px;
display: block;
transition: 2s height;
overflow: hidden;
} /* totally removed .content.visible */
jsFiddle

List rotation with limited elements

I have div container with list (cards) inside. When I hover it, cards start to moving (translateX animation). container's width is 300px, elements count in container:3, each element width:100px.
So you can see 3 elements in container together overflow:hidden. What I want to make?, is that when there is no element to show translateX animation -100px = 100px blank space after third element, it start from 1 elements in the list immediately after last, with no blank space.
For now, I have no idea how it could be done without duplicates and etc.
Here is what I have at the moment:
Fiddle (Hover cards to see translation animation)
UPD 1:
The code and data (cards count, container size) was taken for example, i'll try to explain better what i want: My goal is to built list of cards and after button was pressed, the list will start moving (like in example with translateX animation) for some time (for example translateX: 12491px, animation-duration: 15s;) and stops. But problem is that amount of crads in the list would be in range of 3-40 cards (each card is 100px width & height). So, when i'll set translateX: 12491px for example, it will be out of range and after the last card in the list would appear blank space. I want first and last card to be tied somehow and after the last card immediately appears first card in the list and etc.. Maybe i am searching for solution in a wrong way, but i guess you understand the main idea.
UPD 2:
I found that cs:go uses animation that i wanted to write on html\css\js. Here is video: youtube.com
html:
<div class="container">
<div class="cards">
<div class="card">
1
</div>
<div class="card">
2
</div>
<div class="card">
3
</div>
</div>
</div>
css:
.container
{
width:300px;
height: 100px;
border: 2px solid black;
overflow: hidden;
}
.card
{
float:left;
height: 100px;
width: 100px;
background-color:blue;
box-sizing: border-box;
border: 2px solid red;
color: white;
font-size: 23px;
}
.cards:hover
{
transform: translateX(-100px);
transition-duration: 3s;
animation-duration: 3s;
animation-fill-mode: forwards;
}
start from 1 elements in the list immediately after last, with no
blank space
This is beyond CSS and you will need Javascript for that. Because, you have tagged the question with Javascript and not jQuery, my answer would be limited to pure Javascript only. Look ma, no JQuery ;)
I have no idea how it could be done without duplicates
Here is a DIY (do it yourself) idea..
The main trick is to show at least one item less than the total you have. If you have 3 cards, show only 2. If you have 4 cards, show only 3. Why, because you need to re-position a card when it goes out of view and wrap it back at the end. If you show exactly the same number of cards that you have, then you cannot break half-a-card and wrap it and you will see some blank space until the first one goes out of view. You get the idea?
Do not use translate or you will end up complicating things for yourself while scripting it out. Keep things simple.
Do not use a wrapper for your cards. Why? Because, we will be re-positioning the cards which have gone out of view. When we do that, the next card will take up its place and immediately go out of view making things further difficult for you.
To keep things simple, arrange your cards with absolute positioning relative to its container. To start with, let all cards stack up at top:0; and left: 0;.
Next wire-up Javascript to position the left property based on the width of each card and arrange them linearly.
Use requestAnimationFrame to control the animation.
Keep track of the left-most card and its left position. When this goes out of view (which is 0 minus width), appendChild this card to its container. This will move the card to the end of cards. Also, change the left property to it based on the last card in the list.
That' all there is to it.
Below is a demo. To make it easy for you to experiment, I have used a settings object to keep the configurable properties which you can easily tweak and see. Look closely at the code and you will find it simple to understand. You can set the iterations settings to 0 to make the animation infinite.
Also, note that you do not need to duplicate or fake the cards. Try the demo and add as many cards you want to.
The inline code comments in the snippet, will further help you understand each line of code and relate to the steps above.
Snippet:
var list = document.querySelector('.cardList'), // cache the container
cards = document.querySelectorAll('.card'), // cache the list of cards
start = document.getElementById('start'), // buttons
stop = document.getElementById('stop'),
reset = document.getElementById('reset'),
raf, init = 0, counter = 0, lastCard, currentIteration = 0, // general purpose variables
settings = { // settings object to help make things configurable
'width': 100, 'height': 100, 'speed': 2,
'iterations': 2, 'count': cards.length
}
;
start.addEventListener('click', startClick); // wire up click event on buttons
stop.addEventListener('click', stopClick);
reset.addEventListener('click', resetClick);
initialize(); // initialize to arrange the cards at start
function initialize() {
// loop thru all cards and set the left property as per width and index position
[].forEach.call(cards, function(elem, idx) {
elem.style.left = (settings.width * idx) + 'px';
});
init = -(settings.width); // initialize the view cutoff
lastCard = cards[settings.count - 1]; // identify the last card
counter = 0; currentIteration = 0; // reset some counters
settings.speed = +(document.getElementById('speed').value);
settings.iterations = +(document.getElementById('iter').value);
}
function startClick() {
initialize(); raf = window.requestAnimationFrame(keyframes); // start animating
}
function stopClick() { window.cancelAnimationFrame(raf); } // stop animating
function resetClick() { // stop animating and re-initialize cards to start again
window.cancelAnimationFrame(raf);
document.getElementById('speed').value = '2';
document.getElementById('iter').value = '2';
initialize();
}
// actual animation function
function keyframes() {
var currentCard, currentLeft = 0, newLeft = 0;
// iterate all cards and decrease the left property based on speed
[].forEach.call(cards, function(elem, idx) {
elem.style.left = (parseInt(elem.style.left) - settings.speed) + 'px';
});
currentCard = cards[counter]; // identify left-most card
currentLeft = parseInt(currentCard.style.left); // get its left position
if (currentLeft <= init) { // check if it has gone out of view
// calculate position of last card
newLeft = parseInt(lastCard.style.left) + settings.width;
list.appendChild(currentCard); // move the card to end of list
currentCard.style.left = newLeft + 'px'; // change left position based on last card
lastCard = currentCard; // set this as the last card for next iteration
counter = (counter + 1) % settings.count; // set the next card index
if ((settings.iterations > 0) && (counter >= (settings.count - 1))) {
currentIteration++; // check settings for repeat iterations
}
}
if (currentIteration >= settings.iterations) { return; } // when to stop
raf = window.requestAnimationFrame(keyframes); // request another animation frame
};
* { box-sizing: border-box; padding: 0; margin: 0; }
.cardList {
position: relative; height: 100px; width: 300px;
margin: 10px; border: 2px solid #33e;
overflow: hidden; white-space: nowrap;
}
.card {
position: absolute; left: 0; top: 0; text-align: center;
height: 100px; width: 100px; line-height: 100px;
background-color: #99e;
font-family: monospace; font-size: 2em; color: #444;
border-left: 1px solid #33e; border-right: 1px solid #33e;
}
div.controls, button { margin: 10px; padding: 8px; font-family: monospace; }
div.controls input { width: 48px; padding: 2px; text-align: center; font-family: monospace; }
<div class="controls">
<label>Speed <input id="speed" type="number" min="1" max="8" value="2" />x</label>
|
<label>Iterations <input id="iter" type="number" min="0" max="8" value="2" /></label>
</div>
<div class="cardList">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">3</div>
<div class="card">4</div>
</div>
<button id="start">Start</button>
<button id="stop">Stop</button>
<button id="reset">Reset</button>
Fiddle: http://jsfiddle.net/abhitalks/1hkw1v0w/
Note: I have left out a few things in the demo. Especially, although width and height of the cards is part of the settings object, but currently it left fixed. You can easily use the settings object to make the dimensions of the cards configurable as well.
Edit:
(as per Op's comment)
If you want a greater control over distance to scroll, duration and timing-functions (easing), then you could implement those yourself using a library. A couple of such good libraries are the Robert Penner's Easing Functions and a jQuery plugin from GSGD. Although you can implement all of that with pure Javascript, it would be easier if you use a library like jQuery.
Catch here is that in order to do so effectively, you must then duplicate the cards. You can do so easily by cloning the entire list a couple of times.
Although you have not tagged this question with jQuery, here is a small demo (using jQuery to get it done quickly) where you can configure the speed and the distance.
Snippet 2:
var $cardList = $('.cardList').first(),
$cards = $('.card'),
$speed = $('input[name=speed]'),
width = 100,
randomize = true,
distance = 20 * width
;
for (var i = 0; i < 50; i++) {
$cards.clone().appendTo($cardList);
}
function spin() {
var newMargin = 0, newDistance = distance,
speed = +($speed.filter(':checked').val());
if (randomize) {
newDistance = Math.floor(Math.random() * $cards.length * 5);
newDistance += $cards.length * 5;
newDistance *= width;
}
newMargin = -(newDistance);
$cards.first().animate({
marginLeft: newMargin
}, speed);
}
$('#spin').click(function() {
$cards.first().css('margin-left', 0);
spin();
return false;
});
* { box-sizing: border-box; padding: 0; margin: 0; }
.cardList {
height: 100px; width: 302px; position: relative;
margin: 10px; border: 1px solid #33e;
overflow: hidden; white-space: nowrap;
}
.card {
display: inline-block; text-align: center;
height: 100px; width: 100px; line-height: 100px;
background-color: #99e;
font-family: monospace; font-size: 2em; color: #444;
border-left: 1px solid #33e; border-right: 1px solid #33e;
}
.cardList::before, .cardList::after {
content: ''; display: block; z-index: 100;
width: 0px; height: 0px; transform: translateX(-50%);
border-left: 8px solid transparent;
border-right: 8px solid transparent;
}
.cardList::before {
position: absolute; top: 0px; left: 50%;
border-top: 12px solid #33e;
}
.cardList::after {
position: absolute; bottom: 0px; left: 50%;
border-bottom: 12px solid #33e;
}
div.controls, button { margin: 10px; padding: 8px; font-family: monospace; }
div.controls input { width: 48px; padding: 2px; text-align: center; font-family: monospace; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="controls">
<label>Speed: </label>
|
<label><input name="speed" type="radio" value='6000' />Slow</label>
<label><input name="speed" type="radio" value='5000' checked />Medium</label>
<label><input name="speed" type="radio" value='3000' />Fast</label>
</div>
<div class="cardList"><!--
--><div class="card">1</div><!--
--><div class="card">2</div><!--
--><div class="card">3</div><!--
--><div class="card">4</div><!--
--></div>
<button id="spin">Spin</button>
Fiddle 2: http://jsfiddle.net/abhitalks/c50upco5/
If you don't want to modify the dom elements you could take advantage of flex-item's order property;
to do this you'd still need a little JS to add this property after animation has ended;
I also changed to animation instead of transition so it automatically resets the transform property at the end of animation.
$('.cards').mouseenter(function() {
setTimeout(function() {
$('.card').first().css("order", "2");
}, 3000);
});
$('.cards').mouseleave(function() {
$('.card').first().css("order", "-1");
});
.container {
width: 300px;
height: 100px;
border: 2px solid black;
overflow: hidden;
}
.card {
float: left;
/* height: 100px;
width: 100px;*/
background-color: blue;
box-sizing: border-box;
border: 2px solid red;
color: white;
font-size: 23px;
flex: 0 0 25%;
}
.cards:hover {
animation: trans 3s;
}
/**/
.cards {
width: 400px;
height: 100%;
display: flex;
transition: transform 3s;
}
#keyframes trans {
0% {
transform: translateX(0)
}
100% {
transform: translateX(-100px)
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<div class="container">
<div class="cards">
<div class="card">1</div>
<div class="card">2</div>
<div class="card">3</div>
</div>
</div>
fiddle
But if you're OK to use JS I suggest you manipulate the order of DOM elements directly,taking the first child element of .cards and appending it to the end of list at the end of each animation;
try this:
var anim;
$('.cards').mouseenter(function(){
anim = setInterval(function(){
$('.cards').append($('.card').first())
},3000)
});
$('.cards').mouseleave(function(){
clearInterval(anim)
});
.container{
width:300px;
height: 100px;
border: 2px solid black;
overflow: hidden;
}
.card{
float:left;
/* height: 100px;
width: 100px;*/
background-color:blue;
box-sizing: border-box;
border: 2px solid red;
color: white;
font-size: 23px;
/**/
flex:0 0 25%;
}
.cards:hover{
animation: trans 3s infinite;
}
/**/
.cards{
width:400px;
height:100%;
display:flex;
}
#keyframes trans {
0% {
transform: translateX(0)
}
100% {
transform: translateX(-100px)
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<div class="container">
<div class="cards">
<div class="card">
1
</div>
<div class="card">
2
</div>
<div class="card">
3
</div>
</div>
</div>
in case you want one card to be present at same time both at the beginning and at the end of card-list you'll need to make a deep-copy / clone of the element;
here's an example;
Update 2:
I wrote a jquery plugin that may act the way you want:
you can add as many cards as you want, right now the "translateX" is random (the script will choose randomly the final card)
link to the demo
Update:
I know, I used duplicates, but now my code works on three cards:
I added three "fake" cards
Each "real" card has it's own animation
the "fake" cards will be overlapped by the real ones once their cycle is finished ("when there is no element to show" as you asked)
check the snippet:
.container {
width: 300px;
height: 100px;
border: 2px solid black;
overflow: hidden;
}
.card {
float: left;
height: 100px;
width: 100px;
background-color: blue;
box-sizing: border-box;
border: 2px solid red;
color: white;
font-size: 23px;
}
.cards {
width: 600px;
}
.container:hover .card1{
animation: 1600ms slide1 infinite linear;
}
.container:hover .card2{
animation: 1600ms slide2 infinite linear;
}
.container:hover .card3{
animation: 1600ms slide3 infinite linear;
}
.fakecard{z-index:-1000;}
.container:hover .fakecard{
animation: 1600ms fakeslide infinite linear;
}
#keyframes slide1 {
0% { transform: translateX(0px); }
33% { transform: translateX(-100px); }
33.1% { transform: translateX(+200px); }
100% { transform: translateX(0px); }
}
#keyframes slide2 {
0% { transform: translateX(0px); }
66% { transform: translateX(-200px); }
66.1% { transform: translateX(100px); }
100% { transform: translateX(0px); }
}
#keyframes slide3 {
0% { transform: translateX(0px); }
99% { transform: translateX(-300px); }
99.1% { transform: translateX(+300px); }
100% { transform: translateX(0px); }
}
#keyframes fakeslide {
0% { transform: translateX(0px); }
99% { transform: translateX(-300px); }
99.1% { transform: translateX(+300px); }
100% { transform: translateX(0px); }
}
<div class="container">
<div class="cards">
<div class="card card1">
1
</div>
<div class="card card2">
2
</div>
<div class="card card3">
3
</div>
<div class="card fakecard">
1 (fake)
</div>
<div class="card fakecard">
2 (fake)
</div>
<div class="card fakecard">
3 (fake)
</div>
</div>
</div>
Previous answer:
Is this what you are trying to achieve?
I don't think you can do it without duplicates...
If not, can you explain better what you are trying to achieve here?
[snipped code removed]
Here is the same effect that you mentioned, with a little tweak on your CSS and a helpful hand from jQuery.
CSS
Change your selector for the translateX animation to apply on each of the .card boxes when their immediate parent is hovered, and not the .cards (which is the immediate parent of the .cards). This is because you'd want the cards to move to the left, and not the window through which they appear while making the movement.
That is,
.cards:hover .card {
transform: translateX(-100px);
transition-duration: 1.5s;
animation-duration: 1.5s;
animation-fill-mode: forwards;
}
jQuery
var $container = $('.container');
var cardWidth = 100;
$container.on('mouseenter', function (e) {
e.preventDefault();
var $card0Clone = $('.card').eq(0).clone(); // clone of the first .card element
$('.cards').append($card0Clone);
updateWidth();
});
$container.on('mouseleave', function (e) {
e.preventDefault();
var $cards = $('.card');
$cards.eq(0).remove(); // remove the last .card element
});
function updateWidth() {
$('.cards').width(($('.card').length) * cardWidth); // no of cards in the queue times the width of each card would result in a container fit enough for all of them
}
Code Explained
As you move in the mouse pointer, a clone of the first card is created, and appended to the end of the cards collection. Further, as you move the mouse out of the hover area, the original .card (which was cloned earlier) will be removed from the head of the queue - hence, producing a cyclic effect.
The real trick though is with the updateWidth function. Every time the mouse enters the .container the width of the .cards' immediate parent (i.e. .cards div) is updated, so that .cards div is wide enough to fit in all the .cards, and therefore, making sure that each of the cards push against each other and stay in one line at the time the translation animation is being done.
Here is a simple technique that manipulates the Dom to create your desired effect
Javascript:
document.querySelector('.cards').addEventListener('mousedown', function(e) {
if (e.clientX < (this.offsetWidth >> 1)) {
this.appendChild(this.removeChild(this.firstElementChild));
} else {
this.insertBefore(this.lastElementChild, this.firstElementChild);
}});
then in you css use the nth-of-type selector to position elements as required.
Here is your fiddle
If you are using mouseover you might need to wait for transitionend event before firing again.
Check out this demo
Here I used JQuery, you can configure your animation using two variables
var translateX = 1000; //adjust the whole distance to translate
var stepSpeed = 100; //adjust the speed of each step transition in milliseconds
After setting your variables, on the click event of the cards do the following:-
Get the number of the steps required based on translateX
Loop for the number of steps
Inside each loop (each step) move the cards 1 step to the left, then put the first card to the end of the cards to form the connected loop, then return back the cards to it's initial position
Here is the code:
var stepsNumber = translateX/100;
for(var i=0; i< stepsNumber; i++)
{
$('.cards').animate({'left' : -100}, stepSpeed,function(){
$('.cards div:last').after($('.cards div:first'));
$('.cards').css({'left' : '0px'});
});
}

Categories