136

I am building a toolbar that is going to be included into a page. the div it is going to be included in will default to display:none. Is there a way i can put an event listener on my toolbar to listen for when it becomes visible so it can initialize? or will I have to pass it a variable from the containing page?

Thanks

Liam
  • 22,818
  • 25
  • 93
  • 157
JD Isaacks
  • 51,154
  • 89
  • 267
  • 413
  • Does this answer help? http://stackoverflow.com/questions/1397251/event-detect-when-css-property-changed-using-jquery#answer-1397500 – kangax Sep 23 '09 at 03:46
  • @kangax, thank you. But since its not widely implemented I think I'm going to scratch the whole event listener idea and go a different route. – JD Isaacks Sep 23 '09 at 12:51
  • See this answer for an implementation of an "onVisible" event in JavaScript: http://stackoverflow.com/a/3807340/975097 – Anderson Green Apr 03 '13 at 02:33
  • possible duplicate of [How to implement an 'onVisible' event in Javascript?](http://stackoverflow.com/questions/3806290/how-to-implement-an-onvisible-event-in-javascript) – user Apr 08 '14 at 06:31
  • Could see this please. https://stackoverflow.com/questions/45429746/dont-lose-previous-position-of-rzslider-after-select-the-date-in-angular-js?noredirect=1#comment77835931_45429746 – Varun Sharma Aug 02 '17 at 08:38

10 Answers10

90

Going forward, the new HTML Intersection Observer API is the thing you're looking for. It allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element. It's available in latest versions of Chrome, Firefox and Edge. See https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API for more info.

Simple code example for observing display:none switching:

// Start observing visbility of element. On change, the
//   the callback is called with Boolean visibility as
//   argument:

function respondToVisibility(element, callback) {
  var options = {
    root: document.documentElement,
  };

  var observer = new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      callback(entry.intersectionRatio > 0);
    });
  }, options);

  observer.observe(element);
}

In action: https://jsfiddle.net/elmarj/u35tez5n/5/

joe
  • 588
  • 1
  • 6
  • 17
Elmar Jansen
  • 1,479
  • 1
  • 7
  • 10
  • 2
    > The Intersection Observer API allows you to configure a callback that is called whenever one element, called the target, intersects either the device viewport or a specified element; – stil Nov 14 '17 at 16:24
  • 1
    Note that the InteractionObserver does not work on Safari at the moment. https://caniuse.com/#feat=intersectionobserver It looks like there is a patch in progress. https://bugs.webkit.org/show_bug.cgi?id=159475 – Pic Mickael May 26 '18 at 23:15
  • Until Safari fixes its webkit you can use the solution from @user3588429 reliably – Pic Mickael May 26 '18 at 23:30
  • 12
    When is IE's lack of support for anything cool like this going to stop ruining our lives? – Peter Moore Aug 03 '18 at 22:27
  • 6
    @PeterMoore as soon as you stop trying to support it. Just don't. – johny why Feb 18 '19 at 07:01
  • 5
    LOL. Sometimes it's not our decision. – Peter Moore Feb 20 '19 at 17:03
  • 1
    If, like me, you were hoping to find a brief check of whether an element is visible within scrolled area, see https://stackoverflow.com/questions/5353934/check-if-element-is-visible-on-screen. If you want a callback when the element is on screen, no answer will deign to tell you to connect that hook to a scrollbar handler. – wbharding Jul 03 '19 at 04:27
  • Exactly what I needed! – LStarky Feb 01 '20 at 12:54
  • As far as I can tell the intersection observer does not trigger the callback when a parent elements style is changed from `none` to `block` which causes the child (observed) to become visibile. Otherwise this would be a perfect solution for me. – Dustin Poissant Oct 16 '20 at 23:21
  • 1
    @DustinPoissant: maybe I don't understand the issue you're having, but in my testing it does. Have you tried the fiddle above? I adjusted it to exactly reflect your case (as I understand it) here: https://jsfiddle.net/y7n35jah/13/ . Does this work for you? Or do I misunderstand? – Elmar Jansen Nov 08 '20 at 15:33
  • @ElmarJansen Every small example i try and set up in pens and such it does work properly. But in the productions websites it does not appear to be working. I had to find another solution other than IntersectionObserver, but i wish it would work. I can not disclose the sites here unfortunately, my employer has over 10,000 websites in the automotive sector with billions of daily users so unfortunately i cant not just post the code here for help. But I was thinking it had something to do with the fade in (opacity) that cause it to not work, but as I said we found another non IO solution. – Dustin Poissant Nov 10 '20 at 00:13
  • @ElmarJansen very useful piece of code!! Thanks for sharing. Something was resetting the visible state of one of my elements and could not find out what it was until I used the avbove! – Thierry Dec 16 '20 at 17:02
  • 1
    @DustinPoissant: as far as I can tell, @ElmarJansen is right: `IntersectionOberver` reports changes of visibility, even if determined by the visibility of a parent, and even if this change is the result of a media query. – Benoit Blanchon Feb 21 '21 at 11:03
  • It returned true for all the element I added to observation list even though only first in list is actually visible – Ravinder Payal May 26 '21 at 07:42
56
var targetNode = document.getElementById('elementId');
var observer = new MutationObserver(function(){
    if(targetNode.style.display != 'none'){
        // doSomething
    }
});
observer.observe(targetNode, { attributes: true, childList: true });

I might be a little late, but you could just use the MutationObserver to observe any changes on the desired element. If any change occurs, you'll just have to check if the element is displayed.

u01jmg3
  • 668
  • 1
  • 13
  • 30
user3588429
  • 643
  • 5
  • 4
  • 6
    This does not work when the targetNode is not displayed because an ancestor is not displayed but then gets displayed. – Marco Eckstein Dec 18 '18 at 00:40
  • Upvoted! I used this in another stackoverflow answer: https://stackoverflow.com/questions/48792245/how-can-i-get-just-city-and-state-from-the-google-maps-api/53874634#53874634 – Nick Timmer Dec 20 '18 at 21:28
  • Unfortunately, this only works when using inline styles on the element, not when the change is a result of change of CSS-class (which is the more likely scenario, given that in the former situation you probably already have full programatic control). Fiddle that shows the issue: https://jsfiddle.net/elmarj/uye62Lxc/4/. See here for a more complete discussion of how to observe style-changes: https://dev.to/oleggromov/observing-style-changes---d4f – Elmar Jansen Apr 26 '19 at 15:24
  • Actually the correct Observer to use here would be an `IntersectionObserver` - https://stackoverflow.com/a/52627221/2803743 – kano Dec 17 '19 at 15:57
  • This solution is useful for when you want to toggle an element on and off, and have no direct control of that element, and want to react to the display value change – Eran Goldin May 04 '20 at 12:38
16

There is at least one way, but it's not a very good one. You could just poll the element for changes like this:

var previous_style,
    poll = window.setInterval(function()
{
    var current_style = document.getElementById('target').style.display;
    if (previous_style != current_style) {
        alert('style changed');
        window.clearInterval(poll);
    } else {
        previous_style = current_style;
    }
}, 100);

The DOM standard also specifies mutation events, but I've never had the chance to use them, and I'm not sure how well they're supported. You'd use them like this:

target.addEventListener('DOMAttrModified', function()
{
    if (e.attrName == 'style') {
        alert('style changed');
    }
}, false);

This code is off the top of my head, so I'm not sure if it'd work.

The best and easiest solution would be to have a callback in the function displaying your target.

slikts
  • 7,323
  • 1
  • 23
  • 44
  • 1
    Interesting, thank you. A few years later, the status of DOMAttrModified has changed: "Deprecated. This feature has been removed from the Web standards. Though some browsers may still support it, it is in the process of being dropped." (Mozilla) – BurninLeo Jun 29 '17 at 11:26
  • [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) is the new new. – mindplay.dk Mar 13 '19 at 10:39
14

I had this same problem and created a jQuery plugin to solve it for our site.

https://github.com/shaunbowe/jquery.visibilityChanged

Here is how you would use it based on your example:

$('#contentDiv').visibilityChanged(function(element, visible) {
    alert("do something");
});
Shaun Bowe
  • 9,012
  • 11
  • 45
  • 69
8

As @figha says, if this is your own web page, you should just run whatever you need to run after you make the element visible.

However, for the purposes of answering the question (and anybody making Chrome or Firefox Extensions, where this is a common use case), Mutation Summary and Mutation Observer will allow DOM changes to trigger events.

For example, triggering an event for a elements with data-widget attribute being added to the DOM. Borrowing this excellent example from David Walsh's blog:

var observer = new MutationObserver(function(mutations) {
    // For the sake of...observation...let's output the mutation to console to see how this all works
    mutations.forEach(function(mutation) {
        console.log(mutation.type);
    });    
});

// Notify me of everything!
var observerConfig = {
    attributes: true, 
    childList: true, 
    characterData: true 
};

// Node, config
// In this case we'll listen to all changes to body and child nodes
var targetNode = document.body;
observer.observe(targetNode, observerConfig);

Responses include added, removed, valueChanged and more. valueChanged includes all attributes, including display etc.

mikemaccana
  • 81,787
  • 73
  • 317
  • 396
  • Probably because he wants to know when it is actually on screen. I do, I can only assume the downvoter has the same reason. – Dave Hillier Oct 31 '17 at 00:17
  • Doesn't answer the question, and posting only links isn't advised. – Alexander Holsgrove Nov 11 '17 at 21:24
  • @AlexHolsgrove I've added an example, from the link. Since the poster talks about adding a toolbar to the page, it seems like they might be adding a toolbar to a third party page and waiting for an element to be added to the DOM, in which case Mutations is the best API. – mikemaccana Nov 13 '17 at 13:18
  • Note that this still does not cover that part that explains where in the mutation observation process "visibility" comes in. – Mike 'Pomax' Kamermans Oct 08 '19 at 21:47
4

You may also try this jQuery plugin: https://github.com/morr/jquery.appear

aemonge
  • 2,208
  • 1
  • 21
  • 24
3

Just to comment on the DOMAttrModified event listener browser support:

Cross-browser support

These events are not implemented consistently across different browsers, for example:

  • IE prior to version 9 didn't support the mutation events at all and does not implement some of them correctly in version 9 (for example, DOMNodeInserted)

  • WebKit doesn't support DOMAttrModified (see webkit bug 8191 and the workaround)

  • "mutation name events", i.e. DOMElementNameChanged and DOMAttributeNameChanged are not supported in Firefox (as of version 11), and probably in other browsers as well.

Source: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Mutation_events

Josh
  • 31
  • 1
2

Javascript events deal with User Interaction, if your code is organised enough you should be able to call the initialising function in the same place where the visibility changes (i.e. you shouldn't change myElement.style.display on many places, instead, call a function/method that does this and anything else you might want).

aleksandrbel
  • 1,267
  • 1
  • 17
  • 33
maltalef
  • 1,415
  • 2
  • 16
  • 25
  • 8
    But how would you be able to detect the change in visibility (i. e., call a function as soon as an element becomes visible)? – Anderson Green Apr 03 '13 at 02:15
  • I thiiiink you can't do that. What you would do instead of trying to detect the change, is knowing exactly where it is provoked, and put your call there. If the change in visibility is not something your own code is directly responsible for (e.g. some lib) and the logic by which that happens is very contrived, I guess you're out of luck? :) – maltalef Apr 03 '13 at 10:10
  • 6
    I downvoted this question because despite all the sense it makes, it's irrelevant to the actual issue. (if I display none on a frame, all children elements become invisible) – Sebas Jan 11 '16 at 00:54
  • 2
    @Sebas, I don't really understand why the visibility of the children matters here. Anyway, if the answer wasn't useful for you particular case (whatever it might be) but points to a solution (or explanation why there isn't a solution) for most programmers, it's still a valid answer in my humble opinion. If you like better one of the other answers (especially one that was made at a later time, when technology improved and new options became available), I believe that a request to change the correct answer would be more appropriate than a downvote. In any case, thanks for the heads-up :) – maltalef Jan 13 '16 at 21:31
  • 2
    @figha, not the children, but if the element you're watching is in a frame that is displayed=none, you won't have any sign of it. – Sebas Jan 13 '16 at 23:16
  • Similar to the issue mentioned by @Sebas is, consider you have descendent elements under the element becoming visible that want to listen for visibility. Even with well organized code you would have to iterate through every descendent element to dispatch an event. And putting the code for them directly where the visibility change is effected is *much* too coupled. – Michael May 12 '16 at 22:24
  • I'm looking for the answer to the initial question as well. In my case I'm trying to track a 3rd party pop up so I'm not the one triggering the event becoming visible. This answer wasn't helpful. – Trev14 Jul 27 '17 at 21:32
1

If you just want to run some code when an element becomes visible in the viewport:

function onVisible(element, callback) {
  new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if(entry.intersectionRatio > 0) {
        callback(element);
        observer.disconnect();
      }
    });
  }).observe(element);
}

When the element has become visible the intersection observer calls callback and then destroys itself with .disconnect().

Use it like this:

onVisible(document.querySelector("#myElement"), () => console.log("it's visible"));
joe
  • 588
  • 1
  • 6
  • 17
-3

my solution:

; (function ($) {
$.each([ "toggle", "show", "hide" ], function( i, name ) {
    var cssFn = $.fn[ name ];
    $.fn[ name ] = function( speed, easing, callback ) {
        if(speed == null || typeof speed === "boolean"){
            var ret=cssFn.apply( this, arguments )
            $.fn.triggerVisibleEvent.apply(this,arguments)
            return ret
        }else{
            var that=this
            var new_callback=function(){
                callback.call(this)
                $.fn.triggerVisibleEvent.apply(that,arguments)
            }
            var ret=this.animate( genFx( name, true ), speed, easing, new_callback )
            return ret
        }
    };
});

$.fn.triggerVisibleEvent=function(){
    this.each(function(){
        if($(this).is(':visible')){
            $(this).trigger('visible')
            $(this).find('[data-trigger-visible-event]').triggerVisibleEvent()
        }
    })
}
})(jQuery);

for example:

if(!$info_center.is(':visible')){
    $info_center.attr('data-trigger-visible-event','true').one('visible',processMoreLessButton)
}else{
    processMoreLessButton()
}

function processMoreLessButton(){
//some logic
}
mhlester
  • 21,143
  • 10
  • 49
  • 71