lib/philtre/block/code.ex

defmodule Philtre.Block.Code do
  @moduledoc """
  Elixir-side implementation of the code-type block

  This block is used to write code in a synthax-highlighted UI. The frontend
  aspect of it is implemented in `hooks/Code.ts`
  """
  use Phoenix.LiveComponent

  alias Philtre.Block.ContentEditable
  alias Philtre.Editor
  alias Philtre.Editor.Utils

  require Logger

  defstruct id: nil, content: "", language: "elixir", focused: false

  def render(assigns) do
    # data-language is used to get the language in the frontend hook, which is
    # then used by the frontend-based code-highlighting library
    ~H"""
    <div
      class="philtre__code"
      data-block
      data-language="elixir"
      id={@block.id}
      phx-hook="Code"
      phx-update="ignore"
      phx-target={@myself}
    >
      <pre><code class="philtre__code__highlighted"><%= @block.content %></code></pre>
      <textarea
        class="philtre__code__editable"
        spellcheck="false"
        autofocus={@block.focused}
        rows={rows(@block.content)}><%= @block.content %></textarea>
    </div>
    """
  end

  defp rows(content) when is_binary(content) do
    content
    |> String.split("\n")
    |> Enum.count()
  end

  def html(%__MODULE__{} = table) do
    %{block: table}
    |> read_only()
    |> Phoenix.HTML.html_escape()
    |> Phoenix.HTML.safe_to_string()
  end

  def read_only(%{block: _} = assigns) do
    ~H"<pre><%= @block.content %></pre>"
  end

  def handle_event("update", %{"value" => value}, socket) do
    new_block = %{socket.assigns.block | content: value}
    index = Enum.find_index(socket.assigns.editor.blocks, &(&1.id === new_block.id))
    new_blocks = List.replace_at(socket.assigns.editor.blocks, index, new_block)
    new_editor = %{socket.assigns.editor | blocks: new_blocks}

    send(self(), {:update, new_editor})

    {:noreply, socket}
  end

  def handle_event("add_block", %{} = params, socket) do
    Logger.debug("add_block: #{inspect(params)}")

    %__MODULE__{} = block = socket.assigns.block

    cell = ContentEditable.Cell.new()

    new_block = %ContentEditable{
      id: Utils.new_id(),
      type: "p",
      cells: [cell],
      selection: ContentEditable.Selection.new_start_of(cell)
    }

    new_editor = Editor.replace_block(socket.assigns.editor, block, [block, new_block])

    send(self(), {:update, new_editor})

    {:noreply, socket}
  end
end