from __future__ import annotations
import warnings
from typing import Any
from uuid import uuid4
import dash_mp_components as mpc
from dash import dcc, html
from monty.serialization import loadfn
from crystal_toolkit import MODULE_PATH
from crystal_toolkit.settings import SETTINGS
BULMA_CSS = {
"external_url": "https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css"
}
FONT_AWESOME_CSS = {
"external_url": "https://use.fontawesome.com/releases/v5.6.3/css/all.css"
}
PRIMARY_COLOR = "hsl(171, 100%, 41%)"
# TODO: change "kind" kwarg to list / group is- modifiers together?
"""
Helper methods to make working with Bulma classes easier.
"""
def _update_css_class(kwargs, class_name):
"""
Convenience function to update className while respecting
any additional classNames already set.
"""
if "className" in kwargs:
kwargs["className"] += f" {class_name}"
else:
kwargs["className"] = class_name
[docs]class Section(html.Div):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "section")
super().__init__(*args, **kwargs)
[docs]class Container(html.Div):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "container")
super().__init__(*args, **kwargs)
[docs]class Block(html.Div):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "block")
super().__init__(*args, **kwargs)
[docs]class Columns(html.Div):
def __init__(
self,
*args,
desktop_only=False,
centered=False,
gapless=False,
multiline=False,
**kwargs,
):
_update_css_class(kwargs, "columns")
if desktop_only:
kwargs["className"] += " is-desktop"
if centered:
kwargs["className"] += " is-centered"
if gapless:
kwargs["className"] += " is-gapless"
if multiline:
kwargs["className"] += " is-multiline"
super().__init__(*args, **kwargs)
[docs]class Column(html.Div):
def __init__(self, *args, size=None, offset=None, narrow=False, **kwargs):
_update_css_class(kwargs, "column")
if size:
kwargs["className"] += f" is-{size}"
if offset:
kwargs["className"] += f" -is-offset-{size}"
if narrow:
kwargs["className"] += " is-narrow"
super().__init__(*args, **kwargs)
[docs]class Error(html.Div):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "notification is-danger")
super().__init__(*args, **kwargs)
[docs]class MessageContainer(html.Article):
def __init__(self, *args, kind="warning", size="normal", **kwargs):
if kind:
_update_css_class(kwargs, f"message is-{kind} is-{size}")
else:
_update_css_class(kwargs, f"message is-{size}")
super().__init__(*args, **kwargs)
[docs]class MessageBody(html.Div):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "message-body")
super().__init__(*args, **kwargs)
[docs]class Icon(html.Span):
def __init__(self, kind="download", fill="s", *args, **kwargs):
"""
Font-awesome icon. Good options for kind are "info-circle",
"question-circle", "book", "code".
"""
_update_css_class(kwargs, "icon")
if "fontastic" not in kind:
# fontawesome styles (pre-distributed icons, e.g. download)
super().__init__(html.I(className=f"fa{fill} fa-{kind}"), *args, **kwargs)
else:
# fontastic styles (custom icons, e.g. the MP app icons)
super().__init__(html.I(className=kind), *args, **kwargs)
[docs]class Spinner(html.Button):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "button is-primary is-loading")
kwargs["style"] = {"width": "35px", "height": "35px", "borderRadius": "35px"}
kwargs["aria-label"] = "Loading"
super().__init__(*args, **kwargs)
[docs]class Box(html.Div):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "box")
super().__init__(*args, **kwargs)
[docs]class H1(html.H1):
def __init__(self, *args, subtitle=False, **kwargs):
if subtitle:
_update_css_class(kwargs, "subtitle is-1")
else:
_update_css_class(kwargs, "title is-1")
super().__init__(*args, **kwargs)
[docs]class H2(html.H2):
def __init__(self, *args, subtitle=False, **kwargs):
if subtitle:
_update_css_class(kwargs, "subtitle is-2")
else:
_update_css_class(kwargs, "title is-2")
super().__init__(*args, **kwargs)
[docs]class H3(html.H3):
def __init__(self, *args, subtitle=False, **kwargs):
if subtitle:
_update_css_class(kwargs, "subtitle is-3")
else:
_update_css_class(kwargs, "title is-3")
super().__init__(*args, **kwargs)
[docs]class H4(html.H4):
def __init__(self, *args, subtitle=False, **kwargs):
if subtitle:
_update_css_class(kwargs, "subtitle is-4")
else:
_update_css_class(kwargs, "title is-4")
super().__init__(*args, **kwargs)
[docs]class H5(html.H5):
def __init__(self, *args, subtitle=False, **kwargs):
if subtitle:
_update_css_class(kwargs, "subtitle is-5")
else:
_update_css_class(kwargs, "title is-5")
super().__init__(*args, **kwargs)
[docs]class H6(html.H6):
def __init__(self, *args, subtitle=False, **kwargs):
if subtitle:
_update_css_class(kwargs, "subtitle is-6")
else:
_update_css_class(kwargs, "title is-6")
super().__init__(*args, **kwargs)
[docs]class Tag(html.Div):
def __init__(
self,
tag,
tag_type="primary",
tag_addon=None,
tag_addon_type="primary",
size="normal",
*args,
**kwargs,
):
_update_css_class(kwargs, "tags")
tags = [html.Span(tag, className=f"tag is-{tag_type} is-{size}")]
if tag_addon:
tags.append(
html.Span(tag_addon, className=f"tag is-{tag_addon_type} is-{size}")
)
kwargs["className"] += " has-addons"
super().__init__(tags, *args, **kwargs)
[docs]class TagContainer(html.Div):
def __init__(self, tags: list[Tag], *args, **kwargs):
_update_css_class(kwargs, "field is-grouped is-grouped-multiline")
tags = [html.Div(tag, className="control") for tag in tags]
super().__init__(tags, *args, **kwargs)
[docs]class Textarea(html.Textarea):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "textarea")
super().__init__(*args, **kwargs)
[docs]class Reveal(html.Details):
def __init__(self, children=None, id=None, summary_id=None, title=None, **kwargs):
if children is None:
children = ["Loading..."]
if id is None and isinstance(title, str):
id = title
if isinstance(title, str):
title = H4(
title, style={"display": "inline-block", "verticalAlign": "middle"}
)
contents_id = f"{id}_contents" if id else None
summary_id = summary_id or f"{id}_summary"
kwargs["style"] = {"marginBottom": "1rem"}
super().__init__(
[
html.Summary(title, id=summary_id),
html.Div(
children,
id=contents_id,
style={"marginTop": "0.5rem", "marginLeft": "1.1rem"},
),
],
id=id,
**kwargs,
)
[docs]class Label(html.Label):
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "label")
super().__init__(*args, **kwargs)
[docs]class Modal(html.Div):
def __init__(self, children=None, id=None, active=False, **kwargs):
_update_css_class(kwargs, "modal")
if active:
kwargs["className"] += " is-active"
super().__init__(
[
html.Div(className="modal-background"),
html.Div(
children=children, id=f"{id}_contents", className="modal-contents"
),
html.Button(id=f"{id}_close", className="modal-close is-large"),
],
**kwargs,
)
[docs]class Field(html.Div):
def __init__(
self, *args, addons=False, grouped=False, grouped_multiline=False, **kwargs
):
_update_css_class(kwargs, "field")
if addons:
kwargs["className"] += " has-addons"
if grouped:
kwargs["className"] += " is-grouped"
if grouped_multiline:
kwargs["className"] += " is-grouped-multiline"
super().__init__(*args, **kwargs)
[docs]class Control(html.Div):
"""
Control tag to wrap form elements,
see https://bulma.io/documentation/form/general/
"""
def __init__(self, *args, **kwargs):
_update_css_class(kwargs, "control")
super().__init__(*args, **kwargs)
[docs]def get_data_list(data: dict[str, str]):
"""
Show a formatted table of data items.
:param data: dictionary of label, value pairs
:return: html.Div
"""
contents = []
for title, value in data.items():
if isinstance(title, str):
title = Label(title)
contents.append(
html.Tr(
[
html.Td(title, style={"vertical-align": "middle"}),
html.Td(value, style={"vertical-align": "middle"}),
]
)
)
return html.Table([html.Tbody(contents)], className="table")
[docs]def get_table(rows: list[list[Any]], header: list[str] | None = None) -> html.Table:
"""
Create a HTML table from a list of elements.
:param rows: list of list of cell contents
:return: html.Table
"""
contents = []
for row in rows:
contents.append(html.Tr([html.Td(item) for item in row]))
if not header:
return html.Table([html.Tbody(contents)], className="table")
else:
header = html.Thead([html.Tr([html.Th(item) for item in header])])
return html.Table([header, html.Tbody(contents)], className="table")
DOI_CACHE = loadfn(MODULE_PATH / "apps/assets/doi_cache.json")
[docs]def cite_me(
doi: str = None, manual_ref: str = None, cite_text: str = "Cite me"
) -> html.Div:
"""
Create a button to show users how to cite a particular resource.
:param doi: DOI
:param manual_ref: If DOI not available
:param cite_text: Text to show as button label
:return: A button
"""
if doi:
component = mpc.PublicationButton(id=doi, doi=doi, showTooltip=True)
elif manual_ref:
warnings.warn("Please use the DOI if available.")
component = mpc.PublicationButton(
children=cite_text, id=manual_ref, url=manual_ref
)
return component
[docs]def add_label_help(input, label, help):
"""
Combine an input, label, and tooltip text into a
single consistent component.
"""
return mpc.FilterField(input, id=uuid4().hex, label=label, tooltip=help)
[docs]class Loading(dcc.Loading):
def __init__(self, *args, **kwargs):
super().__init__(
*args, color=PRIMARY_COLOR, type="dot", debug=SETTINGS.DEBUG_MODE, **kwargs
)
[docs]def get_breadcrumb(parts):
if not parts:
return html.Div()
breadcrumbs = html.Nav(
html.Ul(
[
html.Li(
dcc.Link(name, href=link),
className=(None if idx != len(parts) - 1 else "is-active"),
)
for idx, (name, link) in enumerate(parts.items())
]
),
className="breadcrumb",
)
return breadcrumbs