Listening for changes in HTMLCollection (or achieving a similar effect)
Asked Answered
O

3

7

I need to create a simple tooltip library that works like this:

every DOM element with a specific attribute combination (like class="tooltip", data-tooltip-text="some text") automatically displays a tooltip (containing text from data attr) on hover.

This behavior must persist through external DOM manipulation. I really like the idea of utilizing a HTMLcollection for this, for its "live" nature, as iterating the whole DOM with every DOM change sounds potentially very demanding.

Now I would love to watch/listen the collection and run a sequence every time it changes (iterate through the nodes, see if they have a listener, add it if they don't).

How do I do this? The watch and observe methods seem to (if I understand correctly) be capable of that, but they are now deprecated. MDN says that Proxy covers most use cases, but does it cover mine (I haven't found a way to make it work)? Or is there some other way I'm missing?

And what about MutationObserver? I assume that deep-observing the whole application and repeatedly fetching a new NodeList via querySelectorAll with every single change would be too demanding (the library should run over a React application). Dynamically committing the HTMLCollection (as a value) into DOM via React and then (shallowly) listening for changes with MutationObserver might work, but I doubt that would be a good idea either.

Openminded answered 7/3, 2019 at 14:24 Comment(3)
Add a mouseover listener on window and check event.target.closest('.tooltip, [data-tooltip-text="foo"]'), then show the tooltip if it matched an element. As for MutationObserver approach, you don't need to recheck everything, just the changes, see also Performance of MutationObserver to detect nodes in entire DOMPneumatic
@wOxxOm I tried both and it seems that the mousover listener really performs much better in large DOMs. Thank you!Openminded
@wox worth posting as answer. This is how things used to work few years ago.Vassell
T
2

Did You considered straigth simple event delegation ? It's quite old and robust solution and seems to fit your case

parent.addEventListener('mouseover', e => {
   var trg = e.target
   if (trg.classList.includes('tooltip')) {
      // show a singleton tooltip consuming trg.dataset.tooltipText
   }
})

Here a simple raw fiddle (check console when hovering a bold one)

Here basically same fiddle but with a working tooltip

Twowheeler answered 3/10 at 22:30 Comment(0)
V
1

You may not need any scripting nowadays:

.tooltip:hover::after {
  content: attr(data-tooltip-text);
  display: inline-block;
  background-color: #FFFFC0;
  padding: 3px;
  border: 1px solid #c0c0c0;
  position: absolute;
  margin-left: -0.5rem;
  margin-top: 1rem;
}

.tooltip {
  cursor: help;
  border-bottom: dashed 1px #AAA;
}
<div>The underlined text contains <span class="tooltip" data-tooltip-text="yes, there we have it">a tooltip</span></div>
<div>Same <span class="tooltip" data-tooltip-text="something to explain and whatnot">here</span></div>
Vendor answered 4/10 at 9:19 Comment(0)
D
0

I'm not very sure of this, because I am not very good at React, but here's what I think should fit your scenario:

class Tooltip {
    constructor() {
        this.initTooltips();
        this.observeDOMChanges();
    }

    initTooltips() {
        const elements = document.querySelectorAll('[class="tooltip"][data-tooltip-text]');
        elements.forEach(element => {
            if (!element.dataset.tooltipInitialized) {
                this.addTooltipListener(element);
                element.dataset.tooltipInitialized = true;
            }
        });
    }

    addTooltipListener(element) {
        const tooltipText = element.getAttribute('data-tooltip-text');
        const tooltip = document.createElement('div');
        tooltip.className = 'tooltip-content';
        tooltip.innerText = tooltipText;
        document.body.appendChild(tooltip);

        element.addEventListener('mouseenter', () => {
            tooltip.style.display = 'block';
            const rect = element.getBoundingClientRect();
            tooltip.style.left = `${rect.left + window.scrollX}px`;
            tooltip.style.top = `${rect.bottom + window.scrollY}px`;
        });

        element.addEventListener('mouseleave', () => {
            tooltip.style.display = 'none';
        });

        // If you want you can remove the tooltip when it's removed from the DOM
        element.addEventListener('DOMNodeRemoved', () => {
            tooltip.remove();
        });
    }

    observeDOMChanges() {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    this.initTooltips(); // Reinitialize tooltips when child elements change
                }
            });
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }
}


const tooltipLibrary = new Tooltip();

I'm not quite sure how Proxy works, but I believe that using Proxy for DOM observation is a bit unconventional. I hope this solves your issue.

Disparity answered 3/10 at 22:23 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.