I have a slide in menu using vanilla javascript for use on phones, but so far all my tests have resulted in the mobile browsers ignoring the first tap (have tried both touchstart & click as events). Starting with the second tap it works beautifully with each and every subsequent tap.
As opening & closing the menu is the only javascript function on the pages, I don't want to load a huge library, I want to keep it simple and small. My code is below:
var b = document.getElementById('menubtn');
b.addEventListener('touchstart', function (e) {
var n = document.getElementById('nav');
var ns = n.style.left;
if (ns == "-600px") {
n.style.left = "0px";
} else {
n.style.left = "-600px";
}
e.preventDefault();
});
Any ways to easily eliminate this need for double clicking the first time?
In the fwiw dept, it is a responsive design, with the nav menu in a column on big screens and a slide in on phones.
Edit: Solved the issue
Following through on Matt Styles comment, I tried using classList.toggle and it solved the issue. The final version is below:
var b = document.getElementById('menubtn');
var n = document.getElementById('nav');
b.addEventListener('touchstart', function () {
n.classList.toggle('shwmenu');
setTimeout(function () {
b.classList.toggle('shwmenu');
}, 500);
});
I added the delayed menubtn code to toggle the icon between closed and open states.
The behaviour you describe could be caused by the following:
In your JS you try to implement some kind of On-Off toggle for your nav element, differentiated on the left CSS property value, with -600 representing the off value and 0 representing the on value.
As you said, toggling between those states seems to work fine, but what happens when your element is NOT initialized on exactly -600? Then on your first tap you will always run into your else clause and set it to -600 first, showing effectively no visual effect on your first tap, just as you describe.
For an instant test, just swap the if-else clause around to turn it on when style.left is not -600 and then maybe work up towards a more dynamic differentiation between your states ;)
I think this is because element.style.left is empty even if you have set left property in your stylesheet. You can use element.offsetLeft instead. Please see here to how it works.
HTMLElement.style - MDN
The style property is not useful for learning about the element's style in general, since it represents only the CSS declarations set in the element's inline style attribute, not those that come from style rules elsewhere, such as style rules in the <head> section, or external style sheets. To get the values of all CSS properties for an element you should use window.getComputedStyle() instead.
Related
I started using cypress nowadays to learn better and deeper. I am faced with a problem that I can't handle. The problem is some kind of GUI effect working with mouse hover. While hovering my mouse on the element I can't see any information change into DOM. There are just ::before and ::after words appearing. I think I have to solve that problem with some javascript tricks. unfortunately, I am new to javascript and I don't have any idea if you help me I would be very happy. Thank you! (I want to assert in some way that grey background and plus icon is shown or not)
generically look like that
after the mouse hovers this grey background and plus icon comes
you can see elements DOM here
you can see the changes after the mouse hovers element
You can use the cypress-real-events plugin.
To install use the command:
npm i cypress-real-events
Then inside your cypress/support/e2e.{js,ts}, write:
import "cypress-real-events/support";
And in your code you can directly write:
cy.get("selector").realHover('mouse')
//Add assertions
Note: Since the above plugin uses Chrome DevTools Protocols to simulate native events, hence this will only work with Chromium-based browsers, so no firefox.
Into cypress' test try to use the method cy.wait("time here") following hover command.
This is very simple but for visual test this is so useful.
In order to access the :before or :after of an element, we have to do a little Cypress and JavaScript magic. This is answer is mostly from this link.
cy.get('.myClass')
.then($els => {
// get Window reference from element
const win = $els[0].ownerDocument.defaultView
// use getComputedStyle to read the pseudo selector
const before = win.getComputedStyle($els[0], ':before')
// read the value of the `content` CSS property
const contentValue = before.getPropertyValue('content')
// The above lines are just how we tell Cypress to get the `:before` value.
// There isn't a ton to understand outside of really diving into how elements and windows work together
expect(content).to.equal('foo');
})
So, using that same example, we can check for any CSS property on the element. For example, font-size:
cy.get('.myClass')
.then($els => {
const win = $els[0].ownerDocument.defaultView
const before = win.getComputedStyle($els[0], ':before')
const fontSize = before.getPropertyValue('font-size') // instead of `'font-size'`, you can substitute any CSS property
expect(fontSize).to.equal('20');
})
This can also be applied to :after, changing out ($els[0], ':before') with ($els[0], ':after').
Looking at your screenshots, I think you have a gray overlay element covering the original incident element.
The only code I can suggest is from the limited information is
cy.get('#IN-578')
.trigger('mouseover')
.should('not.be.visible')
The overlay has opacity 50%, so you can still see the incident details but from Cypress point of view the incident is covered so it will not be "visible".
If you don't have any luck with 'mouseover', try .realHover() from cypress-real-events.
After the above code, you should look for the overlay element in DOM and the + icon in the middle, select that icon and click it to take the action.
I'm using two simple addEventListener mouseenter and mouseleave functions respectively to play and stop animations (Bodymovin/SVG animations, though I suspect that fact is irrelevant).
So, the following works fine:
document.getElementById('animationDiv').addEventListener('mouseenter', function(){
animation.play();
})
(The HTML couldn't be simpler: The relevant part is just an empty div placeholder filled by script - i.e., <div id="animationDiv"></div>.
I can place that in the same file as the one that operationalizes the animation code, or I can place it in a separate "trigger" file, with both files (and other others necessary to processing) loaded in the site footer.
The problem arises when I need to be able to set triggers for any of multiple similar animations that may or may not appear on a given page.
If only one of two animatable elements are present on a page, then one of two sets of triggers will throw an error. If the first of two such triggers is not present, then the second one will not be processed, meaning that the animation will fail. Or at least that's what it looks like to me is happening.
So, just to be clear, if I add the following two triggers for the same page, and the first of the following two elements is present, then the animation will play on mouseenter. If only the second is present, its animation won't be triggered, apparently because of the error thrown on the first.
document.getElementById('firstAnimationDiv').addEventListener('mouseenter', function(){
firstAnimation.play();
})
document.getElementById('secondAnimationDiv').addEventListener('mouseenter', function(){
secondAnimation.play();
})
At present I can work around the problem by creating multiple trigger files, one for each animation, and setting them to load only when I know that the animatable element will be present, but this approach would get increasingly inefficient when I am using multiple animations per page, on pages whose content may be altered.
I've looked at try/catch approaches and also at event delegation approaches, but so far they seem a bit complicated for handling this simple problem, if appropriate at all.
Is there an efficient and flexible standard method for preventing or properly handling an error for an element not found, in such a way that subsequent functions can still be processed? Or am I missing something else or somehow misreading the error and the function failure I've been encountering?
WHY I PICKED THE ANSWER THAT I DID (PLUS WORKING CODE)
I was easily able to make the simple, directly responsive answer by Baoo work.
I was unable to make the answers below by Patrick Roberts and Crazy Train work, though no doubt my undeveloped js skills are entirely at fault. When I have the time, or when the issue next comes up for me in a more complex implementation (possibly soon!), I'll take another look at their solutions, and see if I can either make them work or if I can formulate a better question with fully fledged coding examples to be worked through.
Finally, just to make things clear for people who might be looking for an answer on Bodymovin animations, and whose js is even weaker than mine, the following is working code, all added to the same single file in which a larger set of Bodymovin animations are constructed, relieving me of any need to create separate trigger files, and preventing TypeErrors and impaired functionality.
//There are three "lets_talk" animations that can play - "home," "snug," and "fixed"
//and three types of buttons needing enter and leave play and stop triggers
let home = document.getElementById('myBtn_bm_home');
if (home) home.addEventListener('mouseenter', function() {
lets_talk_home.play();
});
if (home) home.addEventListener('mouseleave', function() {
lets_talk_home.stop();
});
let snug = document.getElementById('myBtn_bm_snug');
if (snug) snug.addEventListener('mouseenter', function() {
lets_talk_snug.play();
});
if (snug) snug.addEventListener('mouseleave', function() {
lets_talk_snug.stop();
});
let fixed = document.getElementById('myBtn_bm_fixed');
if (fixed) fixed.addEventListener('mouseenter', function() {
lets_talk_fixed.play();
});
if (fixed) fixed.addEventListener('mouseleave', function() {
lets_talk_fixed.stop();
});
At typical piece of underlying HTML (it's generated by a PHP function taking into account other conditions, so not identical for each button), looks like this at the moment - although I'll be paring away the data-attribute and class, since I'm not currently using either. I provide it on the off-chance that someone sees something significant or useful there.
<div id="letsTalk" class="lets-talk">
<a id="myBtn" href="#"><!-- a default-prevented link to a pop-up modal -->
<div class="bm-button" id="myBtn_bm_snug" data-animation="snug"></div><!-- "snug" (vs "fixed" or "home" is in both instances added by PHP -->
</a>
</div>
Obviously, a more parsimonious and flexible answer could be - and probably should be - written. On that note, correctly combining both the play and stop listeners within a single conditional would be an obvious first step, but I'm too much of a js plodder even to get that right on a first or second try. Maybe later/next time!
Thanks again to everyone who provided an answer. I won't ask you to try to squeeze the working solution into your suggested framework - but I won't ask you not to either...
Just write your code so that it won't throw an error if the element isn't present, by simply checking if the element exists.
let first = document.getElementById('firstAnimationDiv');
if (first) first.addEventListener('mouseenter', function() {firstAnimation.play();});
You could approach this slightly differently using delegated event handling. mouseover, unlike mouseenter, bubbles to its ancestor elements, so you could add a single event listener to an ancestor element where every #animationDiv is contained, and switch on event.target.id to call the correct play() method:
document.getElementById('animationDivContainer').addEventListener('mouseover', function (event) {
switch (event.target.id) {
case 'firstAnimationDiv':
return firstAnimation.play();
case 'secondAnimationDiv':
return secondAnimation.play();
// and so on
}
});
You could also avoid using id and use a more semantically correct attribute like data-animation as a compromise between this approach and #CrazyTrain's:
document.getElementById('animationDivContainer').addEventListener('mouseover', function (event) {
// assuming <div data-animation="...">
// instead of <div id="...">
switch (event.target.dataset.animation) {
case 'first':
return firstAnimation.play();
case 'second':
return secondAnimation.play();
// and so on
}
});
First, refactor your HTML to add a common class to all of the placeholder divs instead of using unique IDs. Also add a data-animation attribute to reference the desired animation.
<div class="animation" data-animation="first"></div>
<div class="animation" data-animation="second"></div>
The data- attribute should have a value that targets the appropriate animation.
(As #PatrickRobers noted, the DOM selection can be based on the data-animation attribute, so the class isn't really needed.)
Since your animations are held as global variables, you can use the value of data-animation to look up that variable. However, it would be better if they weren't global, but were rather in a common object.
const animations = {
first: null, // your first animation
second: null, // your second animation
};
Then select the placeholder elements by class, and use the data attribute to see if the animation exists, and if so, play it.
const divs = document.querySelectorAll("div.animation");
divs.forEach(div => {
const anim = animations[div.dataset.animation];
if (anim) {
anim.play(); // Found the animation for this div, so play it
}
});
This way you're guaranteed only to work with placeholder divs that exist and animations that exist.
(And as noted above, selection using the data attribute can be done const divs = document.querySelectorAll("div[data-animation]"); so the class becomes unnecessary.)
Now that support for custom css properties is becoming widespread, I had in mind to use them to simplify the creation of scroll-based animations. In Javascript, I'm using style.setProperty('--customProperty', value) to adjust the custom properties on specific elements as the user scrolls.
It works beautifully in Chrome, Firefox, and Safari 10.
But, in Safari 9.1 (which does support custom properties), I can only set the property once. After having been set, it will not update to a new value.
I've got it all in CodePen: https://codepen.io/kirkhadden/pen/JJbXmE/
// Have we scrolled since the last frame?
if (position != wrapper[0].scrollpos) {
// Keeps updating accurately every frame:
window.log.text(position);
// Only happens on the first frame:
wrapper[0].style.setProperty('--scrollpos', position+'px', '');
wrapper[0].scrollpos = position;
} else { // No Change
return false;
}
I can't find any information or even mention of this behavior. I've tested other, simpler uses of style.setProperty() in Safari, and I continue to find that once a property is set, Safari won't update the same property, even if I try to remove the property first.
Is this a bug in Safari 9.1? Is there a work-around? Is there another way to use javascript to set css variables?
Update
So, instead of style.setProperty, I could instead use jQuery's .attr() method to set the property. It's not ideal, since that will overwrite any other style properties, but it works for this.
The bigger problem is that this whole solution is based on the idea of setting ordinary css animations on all my animated elements, but setting the play-state to 'paused', and then using javascript to manipulate the animation-delay according to the scroll position. This allows me to take advantage of inheritance to animate lots of things with minimal DOM manipulation.
Once again, Safari 9.1 is the road block, since it appears that unlike Chrome or Firefox, if the play-state is 'paused', Safari does not start the animation at all, and ignores the animation-delay.
You can try to polyfill CSS variables via JavaScript
For example:
let variables =
{
color: "red",
border: "2px solid blue"
}
// Get STYLE tag
let style = document.getElementById('custom');
// Save its original text
style.dataset.source = style.innerHTML;
function updateStyle()
{
// Replace variables names with their values
style.innerHTML = style.dataset.source.replace(/#(\w+)/g, function(match, name)
{
return variables[name];
});
}
updateStyle();
Now you can use #variableName in your CSS and it'll be replaced with value of variables.variableName
Fiddle: https://jsfiddle.net/JacobDesight/v7mqps84/
#EDIT
You can even create a function for settings variables:
function setProperty(name, value)
{
variables[name] = value;
updateStyle();
}
Now you just simply do something like:
setProperty('color', 'green');
And it will automatically update styles.
Fiddle: https://jsfiddle.net/JacobDesight/v7mqps84/1/
I have a div that can display 3 images (in the background) each indicating the 'state' of some variable: i.e., partial, full and none. For each of these states I have images: partial.gif, full.gif and none.gif (i.e., these are background images of that div)
Need: Circular queue like toggling effect for changing the images in this order partial -> full -> none -> partial
So if the current image is 'partial.gif' and the user clicks the div the background image changes to the next one in the sequence i.e., full.gif (and if it is currently full.gif it changes to none.gif and that to partial.gif and so on).
Naive solution: have a bunch of if/else's or switch-case and check the current one (image) and then decide based on array look up which is the next one. Is this the best way of doing it? Can I leverage jQuery's toggle function somehow?
(PS: It need not be restricted to images, but could also be for different background color sequences etc., I'd like to know what it is a good 'generic' way of doing it i.e., The example may be specific for background images but if I changed part of that code for background-color or font it should still work. I don't want it to be purely generic, but just enough so it is easy to modify for other attributes. If not, that's fine too. Just a thought :)
http://api.jquery.com/toggle-event/
To be precise http://api.jquery.com/toggle-event/#example-0
does exactly what you wanted...
$("#div1").toggle(
function() {
$(this).css("background-image","url(full.png)")
},
function() {
$(this).css("background-image","url()")
},
function() {
$(this).css("background-image","url(partial.png)")
}
});
UPDATE fn.toggle was removed from jQuery
Here are relevant posts
Where has fn.toggle( handler(eventObject), handler(eventObject)...) gone?
Toggle stopped working after jquery update
As long as it's a CSS-based solution (where you can just switch classes), you could do something like this (untested code):
$('#element').click(function() {
// get current css class value.
var class = $(this).attr('class');
// determine/increment number.
var nextNumber = parseInt(class.charAt(class.length - 1)) + 1;
// if too high, reset to first one.
if (nextNumber > 3) {
nextNumber = 1;
}
// remove old class and add new class.
$(this).removeClass(class).addClass('my_class' + nextNumber);
});
Assumption being made here that you only have one CSS class applied to the element at a time. But if that's not the case, I'm sure you can find a workaround/tweak for this.
And this is just generic enough where you can swap out your CSS class definitions without impacting the script functionality.
I have a small portion of code which works well on FF but I can't seem to get it to work on Safari unless I put an alert instruction anywhere inside of the whiles.
Anyone knows what may be the problem ?
var liste_ele = document.getElementsByClassName('accordion_content');
i=0;
while(i<liste_ele.length)
{
var j=0;
var liste_sel = liste_ele[i].getElementsByTagName('select');
while(j<liste_sel.length)
{
liste_sel[j].style.visibility = '';
j++;
}
i++;
}
Why don't you try setting visibility to visible instead of ''.
liste_sel[j].style.visibility = 'visible';
And are they really hidden by setting visibility to hidden or are the hidden by display:none that might also make a difference.
If putting an alert in your while loop solves the problem, it's almost certainly a timing issue. Where in the DOM is this code being run? Are you sure it's being run AFTER the elements you're trying to find are created?
A simple test would be to put your code inside a timeout:
window.setTimeout(function(){
// your code here
},100);
If that works, then your issue is related to order of operations; make sure your DOM is created before attempting to access it.
#jitter : I already tried to set visibility to visible, but I didn't have a result so I just tried '', hoping it would help. And yes, my elements are hidden and not undisplayed, otherwise my script wouldn't run perfect on FF.
#jvenema : This looks like a good solution indeed :)
Even though I don't know why would my elements not be created since they are initialised as visibility:hidden by another script in my firmware before I pass on them with this script :/
Anyway thanks, you just solved my problem (well I had solved it the good way, by modifying the script that sets it to hidden but I was curious :p) ! :)
If you don't need to block off the position then use the style display:none. Otherwise hide it initially as Safari will render the page initially with the style visibility:hidden you just won't be able to toggle it with Javascript. As a workaround just toggle the opacity with the javascript;
document.getElementById('Div').style.opacity = 0; to make it disappear
and
document.getElementById('Div').style.opacity = 100; to make it reappear.
Its holding up for me until Safari gets it together.