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