Current section
Files
Jump to
Current section
Files
lib/live_debugger/app/debugger/components_tree/web/components.ex
defmodule LiveDebugger.App.Debugger.ComponentsTree.Web.Components do
@moduledoc """
UI components used in the Components Tree
"""
use LiveDebugger.App.Web, :component
alias LiveDebugger.App.Debugger.Structs.TreeNode
alias LiveDebugger.App.Utils.Parsers
@doc """
Renders a TreeNode component with its children recursively.
## Examples
<.ComponentsTree.Web.Components.tree_node
id="tree-id"
tree_node={@tree}
selected_node_id={@selected_node_id}
max_opened_node_level={2}
/>
"""
attr(:id, :string, required: true)
attr(:tree_node, TreeNode, required: true, doc: "TreeNode struct")
attr(:selected_node_id, :string, required: true)
attr(:max_opened_node_level, :integer,
required: true,
doc: "Maximum level of nesting to be opened by default."
)
# These attributes are used only for the recursive calls
attr(:root?, :boolean, default: true, doc: "Indicates if the node is a root node.")
attr(:level, :integer,
default: 0,
doc: "The level of the node in the tree. Used for indentation and recursive rendering."
)
def tree_node(assigns) do
parsed_node_id = TreeNode.parse_id(assigns.tree_node)
assigns =
assigns
|> assign(:parsed_node_id, parsed_node_id)
|> assign(:label_id, "tree-node-#{parsed_node_id}-#{assigns.id}")
|> assign(:collapsible?, length(assigns.tree_node.children) > 0)
|> assign(:selected?, assigns.tree_node.id == assigns.selected_node_id)
|> assign(:open, assigns.level < assigns.max_opened_node_level)
~H"""
<.collapsible
:if={@collapsible?}
id={"collapsible-#{@id}-#{@parsed_node_id}"}
chevron_class="text-accent-icon h-5 w-5"
open={@open}
label_class={[
"rounded-md py-1 hover:bg-surface-1-bg-hover",
if(@selected?, do: "bg-surface-1-bg-hover")
]}
style={style_for_padding(@level, @collapsible?)}
>
<:label>
<.label
id={@label_id}
tree_node={@tree_node}
parsed_node_id={@parsed_node_id}
selected?={@selected?}
level={@level}
collapsible?={true}
/>
</:label>
<div class="flex flex-col">
<.tree_node
:for={child <- @tree_node.children}
id={@id}
tree_node={child}
selected_node_id={@selected_node_id}
root?={false}
max_opened_node_level={@max_opened_node_level}
level={@level + 1}
/>
</div>
</.collapsible>
<.label
:if={not @collapsible?}
id={@label_id}
tree_node={@tree_node}
parsed_node_id={@parsed_node_id}
selected?={@selected?}
level={@level}
collapsible?={false}
/>
"""
end
attr(:id, :string, required: true)
attr(:tree_node, TreeNode, required: true)
attr(:parsed_node_id, :string, required: true)
attr(:level, :integer, required: true)
attr(:collapsible?, :boolean, required: true)
attr(:selected?, :boolean, required: true)
defp label(assigns) do
assigns =
assigns
|> assign(:padding_style, style_for_padding(assigns.level, assigns.collapsible?))
|> assign(:button_id, "button-#{assigns.id}")
|> assign(:icon, node_icon(assigns.tree_node))
|> assign(:tooltip_content, node_tooltip(assigns.tree_node))
|> assign(:label, node_label(assigns.tree_node))
~H"""
<span
id={@id}
phx-hook="Highlight"
phx-value-search-attribute={@tree_node.dom_id.attribute}
phx-value-search-value={@tree_node.dom_id.value}
phx-value-module={@tree_node.module}
phx-value-type={@tree_node.type}
phx-value-id={TreeNode.parse_id(@tree_node)}
class={[
"flex shrink grow items-center rounded-md hover:bg-surface-1-bg-hover",
if(!@collapsible?, do: "p-1"),
if(@selected?, do: "bg-surface-1-bg-hover font-semibold")
]}
style={if(!@collapsible?, do: @padding_style)}
>
<button
id={@button_id}
phx-click="select_node"
phx-value-node-id={@parsed_node_id}
phx-value-search-attribute={@tree_node.dom_id.attribute}
phx-value-search-value={@tree_node.dom_id.value}
phx-value-module={@tree_node.module}
phx-value-type={@tree_node.type}
phx-value-id={TreeNode.parse_id(@tree_node)}
class="flex min-w-0 gap-1 items-center"
>
<.icon name={@icon} class="text-accent-icon w-4 h-4 shrink-0" />
<.tooltip id={@id <> "-tooltip"} content={@tooltip_content} class="truncate">
<span class="hover:underline">
<%= @label %>
</span>
</.tooltip>
</button>
</span>
"""
end
defp style_for_padding(level, collapsible?) do
padding = (level + 1) * 0.5 + if(collapsible?, do: 0.25, else: 1.5)
"padding-left: #{padding}rem;"
end
defp node_icon(%TreeNode{type: :live_view}), do: "icon-liveview"
defp node_icon(%TreeNode{type: :live_component}), do: "icon-component"
defp node_tooltip(%TreeNode{type: :live_view} = node) do
Parsers.module_to_string(node.module)
end
defp node_tooltip(%TreeNode{type: :live_component} = node) do
"#{Parsers.module_to_string(node.module)} (#{Parsers.cid_to_string(node.id)})"
end
defp node_label(%TreeNode{type: :live_view} = node) do
Parsers.module_to_string(node.module)
end
defp node_label(%TreeNode{type: :live_component} = node) do
"#{short_name(node.module)} (#{Parsers.cid_to_string(node.id)})"
end
defp short_name(module) when is_atom(module) do
module
|> Module.split()
|> List.last()
end
end