JavaScript Integration Bug in pn.pane.HTML in Panel 1.x

Dear Community,

I would like to create a dynamic landing page capable of retrieving data from a pre-configured database for my panel service. Currently, I serve an index.py file that includes navigation links to report1 and report2 as the landing page using the following command:

panel serve index.py report1.py report2.py --index index

This setup works well, and the navigation bar is defined as follows:

html = """
<style>
  nav {
    background-color: #333;
    padding: 10px;
  }
  ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: flex-end; /* Align to the right */
  }
  li {
    margin-left: 20px; /* Adjust spacing between links */
  }

  a {
    color: #fff;
    text-decoration: none;
    font-weight: bold;
    position: relative; 
    transition: color 0.3s; 
  }
  a.active {
    font-weight: bold;
    font-size: 40px;
  }
  a:hover {
    color: #ff9900;
  }

  /* Tooltip styling */
  .tooltip {
    display: none;
    position: absolute;
    background-color: #333;
    color: #fff;
    padding: 5px;
    border-radius: 5px;
    top: -30px;
    left: 50%;
    transform: translateX(-50%);
  }
</style>
<nav>
  <ul>
    <li><a href="report1">Report1<span class="tooltip">Tooltip for Report1</span></a></li>
    <li><a href="report2">Report2<span class="tooltip">Tooltip for Report2</span></a></li>
   </ul>
</nav>
"""

pn.pane.HTML(html, sizing_mode="stretch_both").servable()

However, I’m encountering issues when trying to further customize my landing page with JavaScript. For instance, in the provided example, I’m attempting to add tooltips using JavaScript, but it’s not functioning.

"""
<style>
  nav {
    background-color: #333;
    padding: 10px;
  }
  ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: flex-end; /* Align to the right */
  }
  li {
    margin-left: 20px; /* Adjust spacing between links */
  }

  a {
    color: #fff;
    text-decoration: none;
    font-weight: bold;
    position: relative; /* Create a positioning context for tooltips */
    transition: color 0.3s; /* Add a smooth color transition effect */
  }
  a.active {
    font-weight: bold;
    font-size: 40px;
  }
  a:hover {
    color: #ff9900; /* Change the link color on hover */
  }

  /* Tooltip styling */
  .tooltip {
    display: none;
    position: absolute;
    background-color: #333;
    color: #fff;
    padding: 5px;
    border-radius: 5px;
    top: -30px;
    left: 50%;
    transform: translateX(-50%);
  }
</style>
<nav>
  <ul>
    <li><a href="report1">Report1<span class="tooltip">Tooltip for Report1</span></a></li>
    <li><a href="report2">Report2<span class="tooltip">Tooltip for Report2</span></a></li>
  </ul>
</nav>

<script>
  // JavaScript code to show tooltips
  const links = document.querySelectorAll("a");
  
  links.forEach((link) => {
    link.addEventListener("mouseenter", () => {
      const tooltip = link.querySelector(".tooltip");
      tooltip.style.display = "block";
    });
    link.addEventListener("mouseleave", () => {
      const tooltip = link.querySelector(".tooltip");
      tooltip.style.display = "none";
    });
  });
</script>
""" 

I’ve noticed that this issue has persisted since Panel 1.x (it worked with version 0.14.4) and has been reported by @philippjfr as a bug on GitHub: pane.HTML does not embed outside sites with JavaScript as of 1.x; working in 0.14 · Issue #5274 · holoviz/panel · GitHub.

Unfortunately, it doesn’t seem to be on the milestone list for the next version (1.3.0). I’m seeking advice on possible workarounds or alternatives to address this issue. Please provide suggestions.

Thank you very much,

With best regards,

Sergey

The problem is that HTML will get embedded in the Shadow DOM (think isolated subpages). This means that your query selector will fail. You have multiple options. If you use a custom template, that code will not be in the shadow dom and will work. You can also rewrite your code to try to not use query selector. Lastly, use a ReactiveHTML element, this will give you direct access to the elements in your HTML. You can use those also to get the shadowroot and run selector queries, if you really want.

Thank you @rsdenijs for your prompt response; it’s greatly appreciated. @Marc suggested using ReactiveHTML to address my previous issue with Animated Echarts in Panel. Now, I’ve realized that the current issue shares a common root with the previous one, which is that with Panel 1.x, my HTML pane is rendering inside a shadow DOM. However, I’m having difficulty generalizing this solution for the current issue, particularly when I want to interact with specific elements in my HTML, such as showing tooltips on hover.

Would it be possible provide an example of how the following JavaScript code could be integrated with ReactiveHTML?

const links = document.querySelectorAll("a");
links.forEach((link) => {
  link.addEventListener("mouseenter", () => {
    const tooltip = link.querySelector(".tooltip");
    tooltip.style display = "block";
  });
  link.addEventListener("mouseleave", () => {
    const tooltip = link.querySelector(".tooltip");
    tooltip.style.display = "none";
  });
});

I’m trying to integrate this with the following Python code using ReactiveHTML in Panel:

from panel.reactive import ReactiveHTML
import panel as pn

pn.extension()

class Slideshow(ReactiveHTML):
    
    _template = """
    <div id="chartDom" style="height: 100%;width:100%">
        <style>
            ...
        </style>
        <nav>
            <ul>
                <li><a href="report1">Report1<span class="tooltip">Tooltip for Report1</span></a></li>
                <li><a href="report2">Report2<span class="tooltip">Tooltip for Report2</span></a></li>
            </ul>
        </nav>
    </div>
    """
    
    _scripts = {
        "render": """???
;"""
    }

Slideshow(sizing_mode="stretch_both").servable()

An example would be extremely helpful in this context.

With best regards,

Sergey

This should work. I messed with the CSS a bit to make it look decent in the example.
Overall you just do getRootNode() and then you can treat that element as your root.
If you have many links you can also consider generating your template dynamically with jinja2 syntax.
image

from panel.reactive import ReactiveHTML
import panel as pn

pn.extension()


class Slideshow(ReactiveHTML):

    _template = """
    <style>
  nav {
    background-color: #333;
    padding: 10px;
  }
  ul {
    list-style-type: none;
    margin: 0;
    padding: 0;
    display: flex;
    justify-content: flex-end; /* Align to the right */
  }

  li {
    margin-left: 20px; /* Adjust spacing between links */
    margin-right: 20px; /* Adjust spacing between links */
  }

  a {
    color: #fff;
    text-decoration: none;
    font-weight: bold;
    position: relative; 
    transition: color 0.3s; 
  }
  a.active {
    font-weight: bold;
    font-size: 40px;
  }
  a:hover {
    color: #ff9900;
  }

  /* Tooltip styling */
  .tooltip {
    display: none;
    position: absolute;
    color: #fff;
    padding: 5px;
    border-radius: 5px;
    left: 100%;
    top: 20px;
    width: 150px;
    text-align:center;
    transform: translateX(-50%);
    background-color: #888;
  }
</style>
    <div id="chartDom" style="height: 100%;width:100%">
        <nav>
            <ul>
                <li><a href="report1">Report1<span class="tooltip">Tooltip for Report1</span></a></li>
                <li><a href="report2">Report2<span class="tooltip">Tooltip for Report2</span></a></li>
            </ul>
        </nav>
    </div>
    """

    _scripts = {
        "render": """
        root= chartDom.getRootNode()
        const links = root.querySelectorAll("a");
            links.forEach((link) => {
            link.addEventListener("mouseenter", () => {
                const tooltip = link.querySelector(".tooltip");
                tooltip.style.display = "block";
            });
            link.addEventListener("mouseleave", () => {
                const tooltip = link.querySelector(".tooltip");
                tooltip.style.display = "none";
            });
            });
        ;"""
    }


slide = Slideshow()

pn.Column(slide).servable()
2 Likes

Great! Thank you very much @rsdenijs for helping with this. It’s really useful for learning how to use JS in ReactiveHTML. I’m glad to be part of such a helpful and responsive community.

Sergey

2 Likes