diff --git a/assets/packs/mermaid/main.css b/assets/packs/mermaid/main.css
index d924ddfd..2fd4469f 100644
--- a/assets/packs/mermaid/main.css
+++ b/assets/packs/mermaid/main.css
@@ -1,9 +1,27 @@
-#mermaid button#download {
+#figure {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+}
+
+#figure figcaption {
+ border-radius: .5rem;
+ background-color: rgb(240 245 249);
+ padding: 0.5rem;
+ font-size: .875rem;
+ line-height: 1.25rem;
+ font-weight: 500;
+ color: rgb(97 117 138);
+}
+
+#contents button#download {
position: absolute;
display: none;
}
-#mermaid:hover button#download {
+#contents:hover button#download {
display: inline;
+ right: 0;
}
diff --git a/assets/packs/mermaid/main.js b/assets/packs/mermaid/main.js
index e27ee1d3..418c8a9e 100644
--- a/assets/packs/mermaid/main.js
+++ b/assets/packs/mermaid/main.js
@@ -3,45 +3,61 @@ import "./main.css";
mermaid.initialize({ startOnLoad: false });
-export function init(ctx, {content, title}) {
+export function init(ctx, {graph, caption, download}) {
ctx.importCSS("main.css")
function render() {
- mermaid.render("graph1", content).then(({ svg, bindFunctions }) => {
- ctx.root.innerHTML = `
-
- ${svg}
-
-
- `;
+ mermaid.render("graph1", graph).then(({ svg, bindFunctions }) => {
+ let contents = document.createElement("div");
+ contents.id = "contents";
+ ctx.root.appendChild(contents);
- ctx.root.querySelector("#download").addEventListener("click", (event) => {
- var binaryData = [];
- binaryData.push(svg);
- const downloadBlob = URL.createObjectURL(new Blob(binaryData, {type: "image/svg+xml"}));
-
- const downloadLink = document.createElement("a");
- downloadLink.href = downloadBlob;
- downloadLink.download = `${title}.svg`;
- document.body.appendChild(downloadLink);
-
- downloadLink.dispatchEvent(
- new MouseEvent('click', {
- bubbles: true,
- cancelable: true,
- view: window
- })
- );
-
- document.body.removeChild(downloadLink);
- });
+ let figure = document.createElement("figure");
+ figure.id = "figure";
+ figure.innerHTML = svg;
+ contents.appendChild(figure);
+
+ if (caption) {
+ let figcaption = document.createElement("figcaption");
+ figcaption.textContent = caption;
+ figure.appendChild(figcaption);
+ }
+
+ if (download) {
+ let downloadButton = document.createElement("button");
+ downloadButton.id = "download"
+ downloadButton.title = `Download ${download.title}`
+ downloadButton.textContent = "⇩"
+ contents.prepend(downloadButton);
+
+ contents.querySelector("#download").addEventListener("click", (event) => {
+ var binaryData = [];
+ binaryData.push(svg);
+ const downloadBlob = URL.createObjectURL(new Blob(binaryData, {type: "image/svg+xml"}));
+
+ const downloadLink = document.createElement("a");
+ downloadLink.href = downloadBlob;
+ downloadLink.download = download.filename;
+ contents.appendChild(downloadLink);
+
+ downloadLink.dispatchEvent(
+ new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ view: window
+ })
+ );
+
+ contents.removeChild(downloadLink);
+ });
+ }
if (bindFunctions) {
bindFunctions(ctx.root);
}
// A workaround for https://github.com/mermaid-js/mermaid/issues/1758
- const svgEl = ctx.root.querySelector("svg");
+ const svgEl = figure.querySelector("svg");
svgEl.removeAttribute("height");
});
}
diff --git a/lib/kino/mermaid.ex b/lib/kino/mermaid.ex
index 8b91f765..47947414 100644
--- a/lib/kino/mermaid.ex
+++ b/lib/kino/mermaid.ex
@@ -24,13 +24,45 @@ defmodule Kino.Mermaid do
@type t :: Kino.JS.t()
+ @download_defaults [title: "Diagram", filename: "diagram.svg"]
+
@doc """
Creates a new kino displaying the given Mermaid graph.
+
+ ## Options
+
+ * `:caption` - an optional caption for the rendered diagram. Defaults to `false`.
+
+ * `:download` - whether or not to allow downloading the rendered Mermaid svg.
+ Defaults to `true`.
+
+ Downloads can be further customized by providing a keyword list
+ instead of a boolean, containing:
+
+ * `:title` - The alt text displayed for the download button.
+ * `:filename` - The name of the file to be downloaded.
+
"""
@spec new(binary(), Keyword.t()) :: t()
- def new(content, options \\ []) do
- options = Keyword.validate!(options, [:title])
- title = Keyword.get(options, :title, "diagram")
- Kino.JS.new(__MODULE__, %{content: content, title: title}, export: fn content -> {"mermaid", content} end)
+ def new(graph, options \\ []) do
+ options = Keyword.validate!(options, caption: false, download: true)
+
+ download =
+ if download = Keyword.fetch!(options, :download) do
+ case download do
+ true ->
+ @download_defaults
+
+ download_options when is_list(download_options) ->
+ Keyword.validate!(download_options, @download_defaults)
+ end
+ |> Map.new
+ end
+
+ caption = Keyword.fetch!(options, :caption)
+
+ Kino.JS.new(__MODULE__, %{graph: graph, caption: caption, download: download},
+ export: fn graph -> {"mermaid", graph} end
+ )
end
end