-
-
Notifications
You must be signed in to change notification settings - Fork 844
Mermaid on_node_click
, with NICEGUI_HANDLER
drop-in approach
#4862
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
You sucked me back in @evnchn 😂 Although an original promoter of designating a handler name in the markdown (that's then replaced, altered, etc) I moved on to the idea that a handler doesn't need to be defined in markdown at all, I mentioned this in #4845 though it caused some confusion/miss-understanding about how it could work. After the initial feedback on #4845 I reworked my logic and started using the below. I think there are still improvements to be made, further tests to be run, and discussions about the cons to be had (and I know they will be big ones for the developers), but... here it is: The no markdown solution to The Code# mermaid.py
from ..events import GenericEventArguments, Handler
from .mixins.content_element import ContentElement
class Mermaid(ContentElement,
component='mermaid.js',
dependencies=[
'lib/mermaid/mermaid.esm.min.mjs',
'lib/mermaid/chunks/mermaid.esm.min/*.mjs',
]):
CONTENT_PROP = 'content'
def __init__(self, content: str, config: Optional[Dict] = None, on_node_click: Optional[Handler[GenericEventArguments]] = None) -> None:
"""Mermaid Diagrams
Renders diagrams and charts written in the Markdown-inspired `Mermaid <https://mermaid.js.org/>`_ language.
The mermaid syntax can also be used inside Markdown elements by providing the extension string 'mermaid' to the ``ui.markdown`` element.
The optional configuration dictionary is passed directly to mermaid before the first diagram is rendered.
This can be used to set such options as
``{'securityLevel': 'loose', ...}`` - allow running JavaScript when a node is clicked, applied automatically when `on_node_clicked` is specified
``{'logLevel': 'info', ...}`` - log debug info to the console
Refer to the Mermaid documentation for the ``mermaid.initialize()`` method for a full list of options.
:param content: the Mermaid content to be displayed
:param config: configuration dictionary to be passed to ``mermaid.initialize()``
:param on_node_click: callback that is invoked when a node is clicked, applies ``{'securityLevel': 'loose'}`` automatically
"""
super().__init__(content=content)
self._props['config'] = config or {}
if on_node_click:
self.on('nodeClick', on_node_click)
self._props['config']['securityLevel'] = 'loose'
self._props['clickInstance'] = True
def _handle_content_change(self, content: str) -> None:
self._props[self.CONTENT_PROP] = content.strip()
self.run_method('update', content.strip()) // mermaid.js
import mermaid from "mermaid";
export default {
template: `<div></div>`,
data: () => ({
last_content: "",
}),
mounted() {
this.initialize();
this.update(this.content);
},
methods: {
initialize() {
try {
mermaid.initialize(this.config || {});
} catch (error) {
console.error(error);
this.$emit("error", error);
}
},
async update(content) {
if (this.last_content === content) return;
this.last_content = content;
const element = this.$el
try {
const { svg, bindFunctions } = await mermaid.render(element.id + "_mermaid", content);
element.innerHTML = svg;
bindFunctions?.(element);
if (this.clickInstance) {
await this.$nextTick();
this.attachClickHandlers();
};
} catch (error) {
const { svg, bindFunctions } = await mermaid.render(element.id + "_mermaid", "error");
element.innerHTML = svg;
bindFunctions?.(element);
const mermaidErrorFormat = { str: error.message, message: error.message, hash: error.name, error };
console.error(mermaidErrorFormat);
this.$emit("error", mermaidErrorFormat);
}
},
attachClickHandlers() {
const clickables = this.$el.querySelectorAll("g.node");
clickables.forEach(node => {
if (node.getAttribute('data-listener-added')) return;
node.setAttribute('data-listener-added', 'true');
node.style.cursor = "pointer";
const nodeText = node.textContent.trim();
const nodeId = node.id;
node.addEventListener("click", () => {
this.$emit("nodeClick", {
node: this.getNodeName(nodeId),
nodeId,
nodeText,
});
});
});
},
getNodeName(domId) {
if (!domId) return undefined;
const parts = domId.split("-");
if (parts.length >= 3) return parts.slice(1, -1).join("-");
if (parts.length === 2) return parts[1];
return domId;
},
},
props: {
config: Object,
content: String,
clickInstance: Boolean,
},
}; # test.py
from nicegui import ui
ui.mermaid('''
graph LR;
A --> B;
''', on_node_click=lambda e: ui.notify(e))
ui.mermaid('''
graph LR;
A --> B;
''', on_node_click=lambda e: ui.notify(e))
ui.run() The Money ShotThe Pros, Cons, and InbetweensPros
Cons
Inbetweens
The Extra FunctionalityMermaid
|
Looks very promising, @thetableman!
If we write tests against such potential regressions, we can handle such cases when updating Mermaid.
But we still need to the queue for Mermaid's native click handlers (as in #4853), right? |
Yes, the queue may still be needed if multiple diagrams render at the same time and affect the This js was straight out of my app dev environment with a stable release, so didn't have the changes implemented in the recent PR that reintroduced the queue. |
Should I submit a dedicated PR or do we feel we have enough viable options to resolve this already? |
I'm going to be busy for a while, so IMO it's best that you open a dedicated PR where the maintainers can review your solution from ground zero. |
Merge conflict, cannot resolved with the web editor. See you next time, then! |
Motivation
We're trying to add node click functionality to Mermaid diagram without resorting to global
emitEvent
.Apparently, #4861 suffers, when you ought to include
{handler}
in your diagram, which you very much could, in:Source: #4861 (comment)
Implementation
Thought process:
{handler}
is a legit Mermaid syntax.{{
and}}
anymoreNICEGUI_HANDLER
norminally, and you can change the placeholder keyword if you ought to haveNICEGUI_HANDLER
in your diagram.Implementation follows:
function_placeholder
function_placeholder
with random initialized functionfunction_name
which doesthis.$emit("nodeClicked", {node: node, param: param})
Progress
Pytest and documentation pending, since we're unsure of the idea.
Results