Current section

Files

Jump to
beacon_live_admin lib beacon live_admin visual_editor.ex
Raw

lib/beacon/live_admin/visual_editor.ex

defmodule Beacon.LiveAdmin.VisualEditor do
@moduledoc false
@type page :: [element()]
@type element :: map()
@units ~w(px rem em %)
def find_element(page, "root" = _path) when is_list(page) do
%{"tag" => "root", "attrs" => %{}, "content" => page}
end
def find_element(page, path) when is_list(page) and is_binary(path) do
find_ast_element(page, path)
end
def find_element(_page, _path), do: nil
defp find_ast_element(nodes, path) do
case resolve_path(path) do
[] ->
nil
parts ->
find_ast_element_recursive(nodes, parts)
end
end
defp resolve_path(path) when is_binary(path) do
case String.split(path, ".") do
[] ->
[]
parts ->
parts
|> Enum.reduce([], fn
"", acc ->
acc
part, acc ->
[String.to_integer(part) | acc]
end)
|> Enum.reverse()
end
rescue
_ -> []
end
defp find_ast_element_recursive(nodes, [index | []]), do: Enum.at(nodes, index)
defp find_ast_element_recursive(nodes, [index | rest]) do
case Enum.at(nodes, index) do
nil -> nil
node -> find_ast_element_recursive(node["content"], rest)
end
end
# FIXME: update "root" node - it will crash if any property is updated on the root node (choose Up one level up to root)
def update_node(nodes, path, attrs, deleted_attrs) do
path = resolve_path(path)
update_node_recursive(nodes, path, attrs, deleted_attrs)
end
defp update_node(node, attrs, deleted_attrs) do
new_attrs =
node["attrs"]
|> Map.merge(attrs)
|> Map.drop(deleted_attrs)
%{node | "attrs" => new_attrs}
end
defp update_node_recursive(nodes, [index], attrs, deleted_attrs) do
List.update_at(nodes, index, fn node -> update_node(node, attrs, deleted_attrs) end)
end
defp update_node_recursive(nodes, [index | rest], attrs, deleted_attrs) do
List.update_at(nodes, index, fn node ->
%{node | "content" => update_node_recursive(node["content"], rest, attrs, deleted_attrs)}
end)
end
def element_editable?(%{"tag" => tag}) when not is_nil(tag) do
tag not in ["eex", "eex_block", "root"]
end
def element_editable?(_element), do: false
def element_class(element, default \\ "") do
get_in(element, ["attrs", "class"]) || default
end
def element_classes(element) do
element
|> element_class()
|> String.split(" ", trim: true)
end
@doc """
Returns the first utility class in `element` that starts with `class`.
## Examples
iex> find_utility_class(%{"attrs" => %{"class" => "opacity-100"}}, "opacity")
"opacity-100"
iex> find_utility_class(%{"attrs" => %{"class" => "opacity-50 opacity-100"}}, "opacity")
"opacity-50"
iex> find_utility_class(%{"attrs" => %{"class" => "text-red-500 rounded-tl-[20px]"}}, "rounded-")
"rounded-tl-[20px]"
iex> find_utility_class(%{"attrs" => %{"class" => "text-red-500 rounded-tl-[20px]"}}, "border")
nil
"""
@spec find_utility_class(map(), String.t()) :: String.t() | nil
def find_utility_class(element, class) when is_map(element) and is_binary(class) do
classes = get_in(element, ["attrs", "class"]) || ""
class = class <> "-"
classes
|> String.split(" ", trim: true)
|> Enum.find(&String.starts_with?(&1, class))
end
def find_utility_class(_element, _class), do: nil
@doc """
Returns the value of an utility `class` in `element`.
Use `find_utility_class/2` to find the element by class.
## Examples
iex> extract_utility_class_value(%{"attrs" => %{"class" => "opacity-100"}}, "opacity")
"100"
"""
@spec extract_utility_class_value(map(), String.t(), default :: String.t() | nil) :: String.t() | nil
def extract_utility_class_value(element, class, default \\ nil)
def extract_utility_class_value(element, class, default) when is_map(element) and is_binary(class) do
found = find_utility_class(element, class) || ""
strip_square_bracket = fn value ->
value
|> String.replace_prefix("[", "")
|> String.replace_suffix("]", "")
end
case String.split(found, "-") do
[_class, ""] -> default
[^class, value] -> strip_square_bracket.(value)
[_class, _position, value] -> strip_square_bracket.(value)
_ -> default
end
end
def extract_utility_class_value(_element, _class, default), do: default
@doc """
Returns the unit and value of an utility `class` in `element`.
Use `extract_utility_class_value/3` to find and extract the value.
## Examples
iex> extract_utility_class_unit_value(%{"attrs" => %{"class" => "opacity-100"}}, "opacity")
{"base", "100"}
iex> extract_utility_class_unit_value(%{"attrs" => %{"class" => "rounded-tl-[20px]"}}, "rounded")
{"px", "20"}
iex> extract_utility_class_unit_value(%{"attrs" => %{"class" => "rounded-tl-[20px]"}}, "text-red-")
nil
"""
@spec extract_utility_class_unit_value(map(), String.t()) :: {unit :: String.t(), value :: String.t()} | nil
def extract_utility_class_unit_value(element, class) when is_map(element) and is_binary(class) do
case extract_utility_class_value(element, class) do
nil ->
nil
value ->
Enum.reduce_while(@units, nil, fn unit, acc ->
case String.split(value, unit) do
[value] -> {:halt, {"base", value}}
[value, ""] -> {:halt, {unit, value}}
_ -> {:cont, acc}
end
end)
end
end
def extract_utility_class_unit_value(_element, _class), do: nil
def merge_class(element, new) do
current = get_in(element, ["attrs", "class"]) || ""
if Enum.member?(String.split(current, " "), new) do
current
else
Turboprop.Merge.merge([current, new])
end
end
def delete_classes(element, class_regex) do
current = get_in(element, ["attrs", "class"]) || ""
new_classes =
current
|> String.split(" ", trim: true)
|> Enum.reject(&Regex.match?(class_regex, &1))
|> Enum.join(" ")
put_in(element, ["attrs", "class"], new_classes)
end
def parse_number_and_unit(string) do
# Regex to match integers or floats at the start of the string
regex = ~r/^\s*(-?\d+(\.\d+)?)/
case Regex.run(regex, string, return: :index) do
[{start, length} | _] ->
# Extract the numeric part and the remaining text
numeric_part = String.slice(string, start, length)
remaining_text = String.slice(string, (start + length)..-1)
# Try parsing as integer first
case Integer.parse(numeric_part) do
{int, ""} ->
{:ok, int, remaining_text}
_ ->
# If not an integer, parse as float
case Float.parse(numeric_part) do
{float, ""} -> {:ok, float, remaining_text}
_ -> {:error, :not_a_number}
end
end
_ ->
{:error, :not_a_number}
end
end
def parse_integer_or_float(number) when is_number(number) do
{:ok, number}
end
def parse_integer_or_float(string) do
case Integer.parse(string) do
{int, ""} ->
{:ok, int}
_ ->
case Float.parse(string) do
{float, ""} -> {:ok, float}
_ -> :error
end
end
end
end