Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions nicegui/elements/mermaid.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ export default {
const { svg, bindFunctions } = await mermaid.render(element.id + "_mermaid", content);
element.innerHTML = svg;
bindFunctions?.(element);
if (this.clickInstance) {
await this.$nextTick();
this.attachClickHandlers(element);
};
} catch (error) {
const { svg, bindFunctions } = await mermaid.render(element.id + "_mermaid", "error");
element.innerHTML = svg;
Expand All @@ -44,9 +48,37 @@ export default {
}
is_running = false;
},
attachClickHandlers(element) {
const clickables = element.querySelectorAll("g.node");
console.log(clickables)
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,
},
};
33 changes: 21 additions & 12 deletions nicegui/elements/mermaid.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Dict, Optional

from ..events import GenericEventArguments, Handler
from .mixins.content_element import ContentElement


Expand All @@ -11,25 +12,33 @@ class Mermaid(ContentElement,
]):
CONTENT_PROP = 'content'

def __init__(self, content: str, config: Optional[Dict] = None) -> None:
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.
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
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
``{'logLevel': 'info', ...}`` - log debug info to the console
``{'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.
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
"""

:param content: the Mermaid content to be displayed
:param config: configuration dictionary to be passed to ``mermaid.initialize()``
"""
super().__init__(content=content)
self._props['config'] = config

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()
Expand Down
36 changes: 10 additions & 26 deletions tests/test_mermaid.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import pytest
from selenium.webdriver.common.by import By

from nicegui import ui
Expand Down Expand Up @@ -82,32 +81,17 @@ def test_error(screen: Screen):
screen.should_contain('Parse error on line 3')


@pytest.mark.parametrize('security_level', ['loose', 'strict'])
def test_click_mermaid_node(security_level: str, screen: Screen):
def test_click_mermaid_node(screen: Screen):
ui.mermaid('''
flowchart TD;
X;
click X call document.write("Clicked X")
''', config={'securityLevel': security_level})

ui.mermaid('''
flowchart TD;
Y;
click Y call document.write("Clicked Y")
''', config={'securityLevel': security_level})

ui.mermaid('''
flowchart TD;
Z;
click Z call document.write("Clicked Z")
''', config={'securityLevel': security_level})
A[Node A];
B[Node B];
''', on_node_click=lambda e: label.set_text(str(e.args['nodeText'])))
label = ui.label('')

screen.open('/')
screen.click('Y')
screen.wait(0.5)
screen.should_not_contain('Clicked X')
screen.should_not_contain('Clicked Z')
if security_level == 'loose':
screen.should_contain('Clicked Y')
else:
screen.should_not_contain('Clicked Y')
screen.click('Node A')
assert 'Node A' in label.text

screen.click('Node B')
assert 'Node B' in label.text
14 changes: 7 additions & 7 deletions website/documentation/content/mermaid_documentation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ def main_demo() -> None:
list(ui.context.client.elements.values())[-1].props['config'] = {'securityLevel': 'loose'} # HACK: for click_demo


@doc.demo('Handle click events', '''
You can register to click events by adding a `click` directive to a node and emitting a custom event.
Make sure to set the `securityLevel` to `loose` in the `config` parameter to allow JavaScript execution.
@doc.demo('Handle node events', '''
You can register to click events by adding a callback to the `on_node_click` parameter.
When a callback is specified the `config` is updated to include ``{"securityLevel": "loose"}`` to allow JavaScript execution.
''')
def click_demo() -> None:
ui.mermaid('''
graph LR;
A((Click me!));
click A call emitEvent("mermaid_click", "You clicked me!")
''', config={'securityLevel': 'loose'})
ui.on('mermaid_click', lambda e: ui.notify(e.args))
A((Click Me));
B((Or Click Me));
A --> B;
''', on_node_click=lambda e: ui.notify(e.args))


@doc.demo('Handle errors', '''
Expand Down