元素可见时的事件监听器?

207
我正在构建一个将被包含在页面中的工具栏。包含它的
元素默认为display: none。 有没有办法在我的工具栏上添加一个事件监听器,以便在它变得可见时进行初始化? 还是我必须从包含页面传递一个变量给它?

这个答案有帮助吗?https://dev59.com/2XM_5IYBdhLWcg3wZSI6 - kangax
@kangax,谢谢你。但由于它没有被广泛实现,我想我会放弃整个事件监听器的想法,走另一条路线。 - JD Isaacks
请参考此答案,了解JavaScript中“onVisible”事件的实现方式:https://dev59.com/eW865IYBdhLWcg3wceRU#3807340 - Anderson Green
请问您能看到这个吗?https://stackoverflow.com/questions/45429746/dont-lose-previous-position-of-rzslider-after-select-the-date-in-angular-js?noredirect=1#comment77835931_45429746 - Varun Sharma
11个回答

144

往后看,你需要的是新的HTML Intersection Observer API。它允许您配置一个回调,只要一个元素(称为目标)与设备视口或指定元素相交,就会调用该回调。它在最新版本的Chrome、Firefox和Edge中可用。更多信息请参见https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API

观察display:none切换的简单代码示例:

// 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);
}

实例演示:https://jsfiddle.net/elmarj/u35tez5n/5/


3
Intersection Observer API 允许您配置一个回调函数,每当一个元素(称为目标)与设备视口或指定元素相交时调用。 - stil
14
IE什么时候才能停止因缺乏对这种“酷”的支持而破坏我们的生活? - Emperor Eto
14
一旦你停止试图支持它,就不要这样做。 - johny why
7
LOL。有时候这不是我们的决定。 - Emperor Eto
1
据我所知,@ElmarJansen是正确的:IntersectionOberver报告可见性的变化,即使是由父级的可见性决定的,即使这种变化是媒体查询的结果。 - Benoit Blanchon
显示剩余12条评论

77
var targetNode = document.getElementById('elementId');
var observer = new MutationObserver(function(){
    if(targetNode.style.display != 'none'){
        // doSomething
    }
});
observer.observe(targetNode, { attributes: true, childList: true });

我可能有点晚,但你可以使用MutationObserver来观察所需元素上的任何更改。如果发生任何更改,您只需检查该元素是否已显示。


15
当目标节点未显示,因为其中一个祖先节点未显示但后来被显示时,此方法将无效。 - Marco Eckstein
点赞!我在另一个stackoverflow答案中使用了这个:https://stackoverflow.com/questions/48792245/how-can-i-get-just-city-and-state-from-the-google-maps-api/53874634#53874634 - Nick Timmer
不幸的是,这仅适用于在元素上使用内联样式时,而不是当更改是由CSS类的更改引起的(这是更可能的情况,因为在前一种情况下,您可能已经具有完全的编程控制)。演示该问题的Fiddle:https://jsfiddle.net/elmarj/uye62Lxc/4/。请参阅此处以获取有关如何观察样式更改的更完整讨论:https://dev.to/oleggromov/observing-style-changes---d4f - Elmar Jansen
实际上,在这里使用的正确观察器应该是 IntersectionObserver - https://stackoverflow.com/a/52627221/2803743 - kano
1
这个解决方案适用于当您想要切换一个元素的显示状态,但没有直接控制该元素,并且希望对显示值的更改做出反应时。 - Eran Goldin
修改了高度变化,对我的情况非常有效。谢谢。 - Maksim Dimitrov

42
如果你只是想在一个元素在视口中可见时运行一些代码:
function onVisible(element, callback) {
  new IntersectionObserver((entries, observer) => {
    entries.forEach(entry => {
      if(entry.intersectionRatio > 0) {
        callback(element);
        observer.disconnect();
      }
    });
  }).observe(element);
  if(!callback) return new Promise(r => callback=r);
}

当元素变得可见(即使是稍微可见),交叉观察器会调用回调函数callback,然后通过.disconnect()自行销毁。
使用方法如下:
onVisible(document.querySelector("#myElement"), () => console.log("it's visible"));

或者像这样:
await onVisible(document.querySelector("#myElement"));
console.log("it's visible");

如果你希望在元素完全可见时触发回调函数,那么你应该将entry.intersectionRatio > 0改为entry.intersectionRatio === 1
如果在调用onVisible时元素已经可见,则回调函数将立即触发。
相关:如果你想要立即获取一个关于元素当前可见性的truefalse,请使用this code(根据你的需求修改intersectionRatio)。

对我来说非常有效。谢谢。 - Sinan Eldem
3
这应该是被采纳的答案。 - Paul
1
完美!应该是最佳答案。 - Azghanvi

17

至少有一种方法,但不是很好。您可以像这样轮询元素以检测更改:

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);

DOM标准也规定了变异事件,但我从未有机会使用过它们,也不确定它们的支持程度。您可以像这样使用它们:

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

这段代码是我凭空想出来的,所以我不确定它是否有效。

最好且最简单的解决方案是在显示目标的函数中添加一个回调函数。


2
有趣,谢谢。几年后,DOMAttrModified的状态已经改变:“已弃用。此功能已从Web标准中删除。尽管某些浏览器仍可能支持它,但正在被淘汰。”(Mozilla) - BurninLeo
2
MutationObserver是最新的。 - mindplay.dk

15

我曾经也遇到过这个问题,为了解决我们网站上的问题,我创建了一个 jQuery 插件。

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

以下是根据您的示例使用它的方法:

$('#contentDiv').visibilityChanged(function(element, visible) {
    alert("do something");
});

12
这个解决方案使用.is(':visible')以及setTimeout - Hp93

10

如@figha所说,如果这是你自己的网页,你只需要在使元素可见后运行所需的内容。

但是,为了回答问题(以及任何制作Chrome或Firefox扩展程序的人,其中这是常见用例),Mutation SummaryMutation Observer 可以允许DOM更改触发事件。

例如,触发具有data-widget属性添加到DOM中的元素的事件。借鉴David Walsh博客上的这个绝妙示例:

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);

响应包括 added, removed, valueChanged 和更多valueChanged 包括所有属性,包括 display 等。


可能是因为他想知道它何时真正出现在屏幕上。我是这样做的,我只能假设那个点踩者有同样的原因。 - Dave Hillier
不回答问题,仅发布链接也不被建议。 - Alexander Holsgrove
@AlexHolsgrove 我已经添加了一个例子,来自链接。由于发布者谈到要将工具栏添加到页面中,似乎他们可能正在向第三方页面添加工具栏,并等待元素被添加到DOM中,在这种情况下,Mutations是最好的API。 - mikemaccana
请注意,这仍然没有涵盖解释“可见性”在变异观察过程中的位置的部分。 - Mike 'Pomax' Kamermans

6

即使对于嵌套元素,这个简单的解决方案也可以使用 ResizeObserver。

它应该适用于所有现代浏览器 (https://developer.mozilla.org/en-US/docs/Web/API/Resize_Observer_API)

当一个元素应用了css规则display:none(无论是通过直接设置还是通过祖先元素设置)时,它的所有尺寸将为零。因此,为了检测到其变得可见,我们只需要在可见时具有非零尺寸的元素。

const block=document.querySelector("#the-block")
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(block)

1
这是最好的方法,因为等待元素与视口相交意味着在用户滚动到它之前无法进行样式设置。听起来似乎不是很重要,但在页面滚动时进行样式设置与在元素大小调整时进行样式设置之间存在差异。使用ResizeObserver可以解决Web组件定义顺序混乱的问题。有时,在触发connectedCallback时元素尚未被调整大小。 - ShortFuse

3

关于DOMAttrModified事件监听器的浏览器支持,需要进行评论:

跨浏览器支持

这些事件在不同的浏览器中实现不一致,例如:

  • IE 9之前的版本根本不支持突变事件,并且在版本9中无法正确实现其中某些事件(例如DOMNodeInserted)

  • WebKit不支持DOMAttrModified(请参见webkit bug 8191和解决方法)

  • “mutation name events”,即DOMElementNameChanged和DOMAttributeNameChanged在Firefox中不受支持(截至版本11),可能在其他浏览器中也是如此。

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


请使用MutationObserverMutation Events已被弃用。https://dev59.com/tWIj5IYBdhLWcg3wTDVn - Code Novice
请使用MutationObserver代替。Mutation Events已被弃用。https://stackoverflow.com/questions/20420577/detect-added-element-to-dom-with-mutation-observer - undefined

1

在Elmar之前的回答的基础上,我使用了这个方法来将焦点放在Bootstrap导航栏子菜单中的输入框上。

当展开菜单时,我希望焦点放在搜索框上。.onfocus()没有起作用,我认为是因为元素在触发事件时不可见(即使是鼠标抬起事件)。但这个方法完美地解决了问题:

<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>

然后在 JavaScript 中:
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 事件处理与用户交互有关,如果您的代码足够有组织性,您应该能够在可见性更改的同一位置调用初始化函数(即,您不应该在许多地方更改myElement.style.display,而是调用一个执行此操作和其他任何您可能需要的操作的函数/方法)。


10
您如何检测可见性的变化(例如,当元素变为可见时立即调用函数)? - Anderson Green
6
我对这个问题进行了负评,因为尽管它很有道理,但与实际问题无关。(如果我在一个框架上设置display:none,所有子元素都会变得不可见) - Sebas
2
@Sebas,我真的不明白为什么子元素的可见性在这里很重要。无论如何,如果答案对你的特定情况(无论是什么)没有用,但对大多数程序员提供了解决方案(或解释为什么没有解决方案),在我看来它仍然是一个有效的答案。如果你更喜欢其他答案中的一个(尤其是在技术改进和新选项出现后发布的答案),我认为请求更改正确答案比投反对票更合适。无论如何,感谢你的提醒 :) - maltalef
2
@figha,不是子元素的问题,而是如果您观察的元素位于显示=none的框架中,则不会有任何迹象。 - Sebas
与@Sebas提到的问题类似,考虑您在可见元素下有要监听可见性的后代元素。即使代码组织良好,您也必须遍历每个后代元素来分派事件。而将它们的代码直接放在可见性更改受影响的位置上则过于紧密耦合。 - Michael
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接