Not working with ShadowRoot
I'm trying to upgrade the panel-chemistry Python package to work with Panel 1.x/ Bokeh 3.x. This is done in panel-chemistry #41.
Right now I'm working on the NGLViewer component and I cannot get the mouse working. When I drag or zoom it does not work.
The big change from Bokeh 2 to 3 is that all Bokeh components are now rendered inside shadow root. My hypothesis is that it is the cause.
I have a minimum, reproducible example below
<html>
<head>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ngl.js"></script>
</head>
<body>
<div id="host" style="width:100%; height:100%;"></div>
<div id="viewport" style="width:100%; height:100%;"></div>
</body>
<script type="text/javascript">
var stage = new NGL.Stage( "viewport" );
function finish(o){
o.addRepresentation("cartoon");
o.autoView();
}
stage.loadFile("rcsb://1CRN").then(finish)
window.addEventListener( "resize", function( event ){
stage.handleResize();
}, false );
// I need to host ngl inside shadowroot
const host = document.querySelector("#host");
const viewport = document.querySelector("#viewport");
const shadow = host.attachShadow({ mode: "open" });
shadow.appendChild(viewport)
</script>
</html>
It renders nicely. But when I try to drag or zoom nothing happens
I am a data scientist primarely fluent in Python. Javascript is not my strength.
I don't know if I can change the solution above to support mouse events. Or if something needs to change in NGL for this to ever work.
The way the mouse events work in NGL is that they are attached on the document (not the canvas directly). I think this allows, for example, to detect when the user releases the mouse by dragging the pointer outside of the viewport.
When the user clicks, the mouse down event is captured by the document. The target value of the event is compared with the canvas element which is where the scene is painted.
Normally, target and canvas are the same when the use clicks on the 3D viewer, but in the test case you've written, because of the shadow root, the target element is #host. This is due to the shadow DOM which hides the internals of what's inside the shadow root.
It seems that a way to break the encapsulation from the perspective of the event listener, would be to use the event.composedPath() method and check that the first value in the array is indeed the canvas element we are checking against.
This would require changing all the event handlers to add a test for this case.
My understanding is that the shadow root is useful for encapsulating the stylings. In the case of NGL, as there are no CSS styles applied, it seems there is no benefit.
The shadow DOM offers slots which are placeholders for content to be displayed inside the shadow root. What's inside a slot is visible from the rest of the document.
I've played with your test case to try to see wether this could work. See the toy example here: https://codepen.io/ppillot/pen/vYbaExY
The general idea is to create a <SLOT/> element, append it to the shadow root. When the viewport is added to the host element, it will be appended to the slot and stay visible from the rest of the page.
Thanks so much. Really appreciated.
I've tried exploring this. My understanding is that this will not work for my situation because
- My
viewportelement will not be in the dom. It will be nested deeply inside multiple shadow roots because that is how Bokeh works. - I'm not in control of creating the
shadow root. Bokeh does that. - When I try to
assignas in the code below, I don't get theviewportconnected to theslotinside theshadow root.
Example
import panel as pn
pn.extension("ngl_viewer", sizing_mode="stretch_width")
import param
from panel.reactive import ReactiveHTML
class CustomComponent(ReactiveHTML):
index = param.Integer(default=0)
_template = """
<slot id="slot" name="slot" style="width:400px; height:400px;"></slot>
<div id="viewport" style="width:400px; height:400px;"></div>
"""
__javascript__=["https://unpkg.com/[email protected]/dist/ngl.js"]
_scripts = {
"render": """
document.body.append(viewport)
var stage = new NGL.Stage(viewport.id);
function finish(o){
o.addRepresentation("cartoon");
o.autoView();
}
stage.loadFile("rcsb://1CRN").then(finish)
host = slot.getRootNode().host
console.log(host)
host.append(viewport)
console.log(slot)
slot.assign(viewport)
"""
}
pn.Column(
"# NGL Viewer",
CustomComponent(width=400, height=400, styles={"border": "1px solid black"})
).servable()
pip install panel
panel serve script.py --autoreload --show