Source code for trame.html

import types
from trame.internal.triggers.controller import ControllerFunction

import trame.internal as tri


def str_key_prefix(txt):
    if txt.startswith("{") or txt.startswith("`"):
        return ":"

    return ""


def py2js_key(key):
    return key.replace("_", "-")


[docs]def js2py_key(key): return key.replace("-", "_")
[docs]def build_attr_names(name_prefix, key_names, kwargs): """Used to generate a list of attr_names with a common name_prefix.""" attr_names = [] for key_name in key_names: safe_name_prefix = js2py_key(name_prefix) safe_name = js2py_key(key_name).replace(".", "_") if "<name>" in safe_name: safe_header, safe_tail = safe_name.split("<name>") header, tail = key_name.split("<name>") for key in kwargs: if key.startswith(header): dyna_name = key[len(header) : -len(tail)] attr_names.append( ( f"{safe_name_prefix}_{safe_header}{dyna_name}{safe_tail}", f"{name_prefix}:{header}{dyna_name}{tail}", ) ) else: attr_names.append( (f"{safe_name_prefix}_{safe_name}", f"{name_prefix}:{key_name}") ) return attr_names
class ElementContextManager: def __init__(self): self.element_stack = [] def enter(self, elem): self.element_stack.append(elem) def exit(self, elem): if len(self.element_stack) and elem == self.element_stack[-1]: self.element_stack.pop() def add_child(self, elem): if len(self.element_stack): self.element_stack[-1].add_child(elem) HTML_CTX = ElementContextManager() key_names = [ "delete", "down", "enter", "esc", "left", "right", "space", "tab", "up", ] v_on_names = [ "click.capture", "click.once", "click.prevent", "click.prevent.self", "click.self.prevent", "click.self", "click.stop.prevent", "click.stop", "click", "scroll.passive", "scroll", "submit.prevent", "submit", *["keyup." + k for k in key_names], *["keydown." + k for k in key_names], ] v_bind_names = ["class", "style"]
[docs]class AbstractElement: """ A Vue component which can integrate with the rest of trame See Vue docs |vue_doc_link| for more info .. |vue_doc_link| raw:: html <a href="https://vuejs.org/v2/guide/instance.html" target="_blank">here</a> .. |mdn_doc_link| raw:: html <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes" target="_blank">here</a> .. |mdn_event_link| raw:: html <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element#mouse_events">here</a> :param name: The name of the element, like 'div' for a ``<div/>`` element :type name: str :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None :param __properties: Provide more attribute names that should be handle :param __events: Provide more event names that should be handle Html attributes - See |mdn_doc_link| for more info :param id: See |mdn_doc_link| for more info :param classes: Match the HTML `class` attribute. See |mdn_doc_link| for more info :param style: See |mdn_doc_link| for more info Vue attributes - See |vue_doc_link| for more info :param ref: See |vue_doc_link| for more info :param v_model: See |vue_doc_link| for more info :param v_if: See |vue_doc_link| for more info :param v_show: See |vue_doc_link| for more info :param v_for: See |vue_doc_link| for more info :param v_on: See |vue_doc_link| for more info :param v_bind: See |vue_doc_link| for more info :param key: See |vue_doc_link| for more info Events - See |mdn_event_link| for more info :param click: See |mdn_event_link| for more info :param mousedown: See |mdn_event_link| for more info :param mouseup: See |mdn_event_link| for more info :param mouseenter: See |mdn_event_link| for more info :param mouseleave: See |mdn_event_link| for more info :param contextmenu: See |mdn_event_link| for more info """ _next_id = 1 def __init__(self, _elem_name, children=None, **kwargs): AbstractElement._next_id += 1 self._id = AbstractElement._next_id self._elem_name = _elem_name self._allowed_keys = set() self._attr_names = kwargs.get("__properties", []) self._event_names = kwargs.get("__events", []) self._attributes = {} self._py_attr = kwargs self._children = [] if children: if isinstance(children, list): self._children.extend(children) else: self._children.append(children) # Add standard Vue attr/event handling self._attr_names += [ "id", "ref", ("classes", "class"), "style", ("key", ":key"), # default vue.js directives "v_bind", "v_else_if", "v_else", "v_for", "v_html", "v_if", "v_model", "v_on", "v_once", "v_pre", "v_show", "v_text", ] self._attr_names += build_attr_names("v-on", v_on_names, kwargs) self._attr_names += build_attr_names("v-bind", v_bind_names, kwargs) self._event_names += [ "click", "mousedown", "mouseup", "mouseenter", "mouseleave", "contextmenu", ] # Add ourself to context if any HTML_CTX.add_child(self) def _attr_str(self): return " ".join(self._attributes.values()) def _update_allowed_keys(self): if hasattr(self, "_attr_names") and hasattr(self, "_event_names"): for items in [self._attr_names, self._event_names]: for item in items: if isinstance(item, str): self._allowed_keys.add(item) else: self._allowed_keys.add(item[0]) # ------------------------------------------------------------------------- # Buildin API # ------------------------------------------------------------------------- def __getitem__(self, name): return self._py_attr[name] def __setitem__(self, name, value): if name in self._allowed_keys: self._py_attr[name] = value else: print(f"Attribute {name} is not defined for {self._elem_name}") def __getattr__(self, name): if name[0] == "_": raise AttributeError() return self._py_attr[name] def __setattr__(self, name, value): if name[0] == "_": self.__dict__[name] = value elif name == "children": self._children = value elif name in self._allowed_keys: self._py_attr[name] = value else: self.__dict__[name] = value if name in ["_attr_names", "_event_names"]: self._update_allowed_keys() # ------------------------------------------------------------------------- # helpers # -------------------------------------------------------------------------
[docs] def ttsSensitive(self): """ Calling this function on an element will make it fully recreate itself every time the layout update. Internally it is managed by adding a `key=` attribute which use a layout timestamp. This is especially useful for component that manage other elements outside of themself like VSelect in Vuetify. """ self._attributes["__tts"] = f':key="`w{self._id}-${{tts}}`"' return self
[docs] def attrs(self, *names): """ Calling this function will process the provided attribute names and configure its internal so the macthing HTML string could easily be generated later on. :param names: The names attribute to process :type names: *str """ _app = tri.get_app_instance() for _name in names: js_key = None name = _name if isinstance(_name, tuple): name = _name[0] js_key = _name[1] if name in self._py_attr: if js_key is None: js_key = py2js_key(name) value = self._py_attr[name] if value is None: continue if ( _app._debug and js_key.startswith("v-") and not isinstance(value, (tuple, list)) ): print( f'Warning: A Vue directive is evaluating your expression and trame would expect a tuple instead of a plain type. <{self._elem_name} {js_key}="{value}" ... />' ) if isinstance(value, (tuple, list)): if len(value) > 1 and value[0] not in _app.state: _app.state[value[0]] = value[1] if js_key.startswith("v-"): self._attributes[name] = f'{js_key}="{value[0]}"' elif js_key.startswith(":"): self._attributes[name] = f'{js_key}="{value[0]}"' else: self._attributes[name] = f':{js_key}="{value[0]}"' elif isinstance(value, bool): if value: self._attributes[name] = js_key elif isinstance(value, (str, int, float)): self._attributes[name] = f'{js_key}="{value}"' else: print( f"Error: Don't know how to handle attribue name '{name}' with value '{value}' in {self.__class__}::{self._elem_name}" ) return self
[docs] def events(self, *names): """ Calling this function will process the provided event names and configure its internal so the macthing HTML string could easily be generated later on. :param names: The names events to process :type names: *str """ _app = tri.get_app_instance() for _name in names: js_key = None name = _name if isinstance(_name, tuple): name = _name[0] js_key = _name[1] if name in self._py_attr: if js_key is None: js_key = py2js_key(name) js_key = f"@{js_key}" value = self._py_attr[name] if value is None: continue if isinstance(value, str): self._attributes[name] = f'{js_key}="{value}"' elif isinstance(value, (types.FunctionType, types.MethodType, ControllerFunction)): trigger_name = tri.trigger_key(value) self._attributes[name] = f"{js_key}=\"trigger('{trigger_name}')\"" elif isinstance(value, tuple): trigger_name = value[0] if isinstance(trigger_name, (types.FunctionType, types.MethodType, ControllerFunction)): trigger_name = tri.trigger_key(trigger_name) if len(value) == 1: self._attributes[ name ] = f"{js_key}=\"trigger('{trigger_name}')\"" if len(value) == 2: self._attributes[ name ] = f"{js_key}=\"trigger('{trigger_name}', {value[1]})\"" if len(value) == 3: self._attributes[ name ] = f"{js_key}=\"trigger('{trigger_name}', {value[1]}, {value[2]})\"" else: print( f"Error: Don't know how to handle event name '{name}' with value '{value}' in {self.__class__}::{self._elem_name}" ) return self
[docs] def clear(self): """ Remove all children """ self._children.clear()
[docs] def hide(self): """ Hide element while keeping it in the DOM. (display: none) """ self._attributes["__style"] = 'style="display: none"'
[docs] def add_child(self, child): """ Add a component to this component's children :param child: The component to add as a child :type child: str | AbstractElement """ self._children.append(child)
[docs] def add_children(self, children): """ Add components to this component's children. The provided children is expected to be a list. :param children: The list of components to add to the children :type children: list """ self._children += children
@property def children(self): """ Children components """ return self._children
[docs] def set_text(self, value): """ Replace children with a single text child element :param value: The text for the new text child element :type value: str """ self.clear() self._children.append(value)
@property def html(self): """ Return a string representation of the HTML component """ # Build attributes self.attrs(*self._attr_names) self.events(*self._event_names) # Compute HTML str if len(self._children): out_buffer = [] out_buffer.append(f"<{self._elem_name} {self._attr_str()}>") for child in self._children: if isinstance(child, str): out_buffer.append(child) else: out_buffer.append(child.html) out_buffer.append(f"</{self._elem_name}>") return "\n".join(out_buffer) else: return f"<{self._elem_name} {self._attr_str()} />" # ------------------------------------------------------------------------- # Resource manager # ------------------------------------------------------------------------- def __enter__(self): HTML_CTX.enter(self) return self def __exit__(self, exc_type, exc_value, exc_traceback): HTML_CTX.exit(self)
[docs]class Element(AbstractElement): """ Any html element you would like to use in trame :param _elem_name: The name of the element, like 'div' for a ``<div/>`` element :type _elem_name: str :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None """ def __init__(self, _elem_name, children=None, **kwargs): super().__init__(_elem_name, children, **kwargs)
[docs]class Div(AbstractElement): """ The standard html content div element :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None """ def __init__(self, children=None, **kwargs): super().__init__("div", children, **kwargs)
[docs]class Span(AbstractElement): """ The standard html content span element :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None """ def __init__(self, children=None, **kwargs): super().__init__("span", children, **kwargs)
[docs]class Form(AbstractElement): """ The standard html form element :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None """ def __init__(self, children=None, **kwargs): super().__init__("form", children, **kwargs) self._attr_names += ["action"]
[docs]class Label(AbstractElement): """ The standard html input label element :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None """ def __init__(self, children=None, **kwargs): super().__init__("label", children, **kwargs)
[docs]class Input(AbstractElement): """ The standard html input (form input) element :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None """ def __init__(self, children=None, **kwargs): super().__init__("input", children, **kwargs) self._attr_names += [ "type", "value", "name", "size", "min", "max", "step", "maxlength", "disabled", "readonly", "multiple", "pattern", "placeholder", "required", "autofocus", "src", "width", "height", "alt", "list", "autocomplete", ] self._event_names += ["change", "input"]
[docs]class Template(AbstractElement): """ The standard html content template element. This is mostly used by |slot_doc_link|. .. |slot_doc_link| raw:: html <a href="https://vuejs.org/v2/guide/instance.html" target="_blank">vue's slot system</a> :param children: The children nested within this element :type children: str | list[trame.html.*] | trame.html.* | None :param v_slot: The slot this template corresponds to """ slot_names = set() def __init__(self, children=None, **kwargs): super().__init__("template", children, **kwargs) self._attr_names += ["v_slot"] self._attr_names += build_attr_names("v-slot", Template.slot_names, kwargs)
[docs]class StateChange(AbstractElement): """ Component to react when a state entry change so an event can be triggered :param name: Which part of the state to listen to :type name: str Events :param change: Function to run if state changes :type change: function """ def __init__(self, name, **kwargs): super().__init__("py-state-update", **kwargs) self._attributes["value"] = f':value="{name}"' self._event_names += [ "change", ]
[docs]class Triggers(AbstractElement): """ Component to trigger JS actions from Python :param ref: Name for Vue reference to this object :type ref: str :param triggers: Mapping from names of triggers to expressions or methods in JS which they will call :type triggers: dict[str, str] >>> triggers = trame.html.Triggers(ref="all_triggers", triggers={ "reset_camera": "$refs.view.resetCamera()" }) """ def __init__(self, ref, triggers={}, **kwargs): super().__init__("py-trigger", **kwargs) self._ref = ref self._attributes["ref"] = f'ref="{ref}"' for key, value in triggers.items(): self._attributes[f"_{key}"] = f'@{key}="{value}"'
[docs] def add(self, name, call): """ Add a trigger which can call JS from Python :param name: Reference for this JS method or expression trigger :type name: str :param call: JS method or expression to call when triggered :type call: str >>> triggers.add("created", "console.log('UI is created')") >>> triggers.add("mounted", "console.log('UI is mounted')") >>> triggers.add("beforeDestroy", "console.log('UI is going away')") """ self._attributes[f"_{name}"] = f'@{name}="{call}"'
[docs] def call(self, name, *args): """ Trigger JS code previously added to this object :param name: Reference for this JS method or expression trigger :type name: str :param args: Parameters passed to JS method >>> triggers.call("reset_camera") """ _app = tri.get_app_instance() _app.update(ref=self._ref, method="emit", args=[name, *args])
[docs]class VTKLoading(AbstractElement): """ Component to show the 3 spinning partial circles using the ParaView Red/Green/Yellow colors. :param message: Message to put below the spinning circles :type message: str """ def __init__(self, message="", **kwargs): super().__init__("vtk-loading", message=message, **kwargs) self._attr_names += ["message"]