199

I am building a toolbar that is going to be included into a page. the div is going to be included in will default to display: none.
Is there any 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?

shA.t
  • 16,580
  • 5
  • 54
  • 111
JD Isaacks
  • 56,088
  • 93
  • 276
  • 422
  • 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

11 Answers11

135

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
  • 3,752
  • 1
  • 32
  • 41
Elmar Jansen
  • 1,959
  • 2
  • 9
  • 10
  • 3
    > 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
  • 14
    When is IE's lack of support for anything cool like this going to stop ruining our lives? – Emperor Eto Aug 03 '18 at 22:27
  • 14
    @PeterMoore as soon as you stop trying to support it. Just don't. – johny why Feb 18 '19 at 07:01
  • 7
    LOL. Sometimes it's not our decision. – Emperor Eto 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
  • Is this still the best way to do it in 2021? Are there alternatives to this solution for Angular (v12)? – MikhailRatner Nov 17 '21 at 16:08
  • This doesn't work for elements that are visible outside of viewport. I don't know why this is best voted answer so far, as it's actually triggers just for current viewport elements.. – Kos Jan 28 '22 at 12:27
  • @Kos: maybe I misunderstand your point, but in my testing, this works for elements that are not visible in the viewport. Note that this code sets the "root" option to override the default target (which would indeed be the viewport if not overridden) and observe the root element of the document instead. – Elmar Jansen Feb 02 '22 at 21:32
77
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
  • 712
  • 1
  • 11
  • 31
user3588429
  • 976
  • 6
  • 5
  • 14
    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
  • 1
    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
  • Modified it to _height_ change and worked great for my case. Tnx – Maksim Dimitrov Mar 15 '22 at 10:25
40

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 (event slightly) the intersection observer calls callback and then destroys itself with .disconnect().

Use it like this:

onVisible(document.querySelector("#myElement"), () => console.log("it's visible"));

If you want the callback to trigger when the element becomes fully visible then you should change entry.intersectionRatio > 0 to entry.intersectionRatio === 1.

joe
  • 3,752
  • 1
  • 32
  • 41
17

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
  • 8,020
  • 1
  • 27
  • 47
  • 2
    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
  • 2
    [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver) is the new new. – mindplay.dk Mar 13 '19 at 10:39
15

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,840
  • 11
  • 50
  • 71
10

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
  • 110,530
  • 99
  • 389
  • 494
  • 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
6

A simple solution to this which works even for nested elements is to use the ResizeObserver.

It should work in all modern browsers (https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API).

When an element has css rule display: none applied to it (whether directly or via an ancestor element) then all of its dimensions will be zero. So in order to detect becoming visible we just need an element with non-zero dimensions when visible.

const element = document.querySelector("#element");
const resizeWatcher = new ResizeObserver(entries => {
  for (const entry of entries) {
    console.log("Element", entry.target, 
      (entry.contentRect.width === 0) ? 
      "is now hidden" : 
      "is now visible"
    )
  }
});

resizeWatcher.observe(element)
Yangshun Tay
  • 49,270
  • 33
  • 114
  • 141
BenVida
  • 1,796
  • 1
  • 16
  • 25
  • 1
    This is best because waiting for an element to intersect with the viewport means you can't style until the user scrolls to it. Sounds like nothing major, but there's a difference between styling while the page is scrolling versus styling the moment the element is sized. Using `ResizeObserver` works around the issue of Web Components being defined out of order. Sometimes the element isn't sized yet when `connectedCallback` is fired. – ShortFuse Mar 16 '23 at 20:32
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
  • Use `MutationObserver` instead. Mutation Events are deprecated. https://stackoverflow.com/questions/20420577/detect-added-element-to-dom-with-mutation-observer – Code Novice Feb 18 '23 at 19:10
1

Expanding on Elmar's earlier answer, I used this to put focus on an input box in a Bootstrap navbar submenu.

I wanted the focus to go on the search box when the menu was expanded. .onfocus() wasn't working, I think because the element isn't visible at the time the event is triggered (even with the mouseup event). This worked perfectly though:

<ul class="navbar-nav ms-auto me-0 ps-3 ps-md-0">
    <li class="nav-item dropdown">
        <a class="nav-link dropdown-toggle" title="Search" id="navbardrop" data-bs-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
            <i class="fas fa-search"></i>
        </a>
        <div class="dropdown-menu dropdown-menu-end search-menu">
            <form action="{% url 'search' %}" method="get">
                <div class="form-group row g-1 my-1 pb-1">
                    <div class="col">
                        <input type="text" name="query" id="searchbox" class="form-control py-1 ps-2" value="{% if search_query %}{{ search_query }}{% endif %}">
                    </div>
                    <div class="col-auto">
                        <input type="submit" value="Search" class="btn-primary form-control py-1">
                    </div>
                </div>
            </form>
        </div>
    </li>
</ul>

Then in the js:

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

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

  observer.observe(element);
};

respondToVisibility(document.getElementById("searchbox"), (visible) => {
  if (visible) {
    document.getElementById("searchbox").focus();
  }
});
-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,422
  • 3
  • 20
  • 38
maltalef
  • 1,507
  • 2
  • 16
  • 27
  • 10
    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
-5

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
  • 22,781
  • 10
  • 52
  • 75