defmodule Philtre.Block.Table do
@moduledoc """
Implementation for a table section/component of the editor
To add to editor, use `/table`.
The current implementation starts of with a single cell, to which additional
rows and cells can be added and removed from.
"""
use Phoenix.Component
alias Philtre.Block
@behaviour Block
defstruct id: nil, header_rows: [[""]], rows: [[""]]
@impl Block
def id(%__MODULE__{id: id}), do: id
@impl Block
def type(%__MODULE__{}), do: "table"
@impl Block
def data(%__MODULE__{header_rows: header_rows, rows: rows}) do
%{"header_rows" => header_rows, "rows" => rows}
end
@impl Block
def normalize(id, %{"header_rows" => header_rows, "rows" => rows}) do
%__MODULE__{
id: id,
header_rows: header_rows,
rows: rows
}
end
def render_live(assigns) do
~H"""
<div class="philtre__table" data-block>
<table>
<thead>
<.head {assigns} />
</thead>
<tbody>
<.body {assigns} />
</tbody>
</table>
<button title="Add a column" phx-click="add_column" phx-target={@myself}>+</button>
<button title="Add a row" phx-click="add_row" phx-target={@myself}>+</button>
</div>
"""
end
defp head(assigns) do
cell_count = cell_count(assigns[:block])
~H"""
<%= for {row, row_index} <- Enum.with_index(@block.header_rows) do %>
<tr>
<%= for {cell, cell_index} <- Enum.with_index(row) do %>
<th>
<!-- each column of the first header row gets the remove column button -->
<%= if row_index == 0 do %>
<button
disabled={cell_count <= 1}
title="Remove this column"
phx-click="remove_column"
phx-value-index={cell_index}
phx-target={@myself}>-</button>
<% end %>
<.cell
{assigns}
cell={cell}
cell_index={cell_index}
cell_type="head"
row_index={row_index}
/>
</th>
<% end %>
</tr>
<% end %>
"""
end
defp body(assigns) do
~H"""
<%= for {row, row_index} <- Enum.with_index(@block.rows) do %>
<tr>
<% cell_count = Enum.count(row) %>
<%= for {cell, cell_index} <- Enum.with_index(row) do %>
<td>
<.cell
{assigns}
cell={cell}
cell_index={cell_index}
cell_type="body"
row_index={row_index}
/>
<!-- last column of a row gets the remove row button -->
<%= if cell_index == cell_count - 1 do %>
<button
disabled={Enum.count(@block.rows) <= 1}
title="Remove this row"
phx-click="remove_row"
phx-value-index={row_index}
phx-target={@myself}>-</button>
<% end %>
</td>
<% end %>
</tr>
<% end %>
"""
end
defp cell(assigns) do
rows =
assigns.cell
|> String.split("\n")
|> Enum.map(fn line ->
line
|> String.codepoints()
|> Enum.chunk_every(50)
|> Enum.map(&Enum.join/1)
end)
|> List.flatten()
height = rows |> Enum.count() |> Kernel.max(1)
width =
rows
|> Enum.reject(&(&1 == ""))
|> Enum.map(&String.length/1)
|> Enum.max(fn -> 1 end)
|> Kernel.max(1)
~H"""
<form phx-change="update_cell" phx-target={@myself}>
<input type="hidden" name="cell_type" value={@cell_type} />
<input type="hidden" name="cell_index" value={@cell_index} />
<input type="hidden" name="row_index" value={@row_index} />
<textarea
name="cell"
type="text"
rows={height}
cols={width}
><%= @cell %></textarea>
</form>
"""
end
def render_static(%{} = assigns) do
~H"""
<table>
<thead>
<%= for row <- @block.header_rows do %>
<tr>
<%= for cell <- row do %>
<th><%= cell %></th>
<% end %>
</tr>
<% end %>
</thead>
<tbody>
<%= for row <- @block.rows do %>
<tr>
<%= for cell <- row do %>
<td><%= cell %></td>
<% end %>
</tr>
<% end %>
</tbody>
</table>
"""
end
def handle_event("add_row", %{}, socket) do
%__MODULE__{rows: rows} = table = socket.assigns.block
new_row = List.duplicate("", cell_count(table))
new_table = %{table | rows: rows ++ [new_row]}
%{blocks: blocks} = editor = socket.assigns.editor
index = Enum.find_index(blocks, &(&1.id === table.id))
new_editor = %{editor | blocks: List.replace_at(blocks, index, new_table)}
send(self(), {:update, new_editor})
{:noreply, socket}
end
def handle_event("add_column", %{}, socket) do
%__MODULE__{rows: rows, header_rows: header_rows} = table = socket.assigns.block
new_rows = Enum.map(rows, &(&1 ++ [""]))
new_header_rows = Enum.map(header_rows, &(&1 ++ [""]))
new_table = %{table | rows: new_rows, header_rows: new_header_rows}
%{blocks: blocks} = editor = socket.assigns.editor
index = Enum.find_index(blocks, &(&1.id === table.id))
new_editor = %{editor | blocks: List.replace_at(blocks, index, new_table)}
send(self(), {:update, new_editor})
{:noreply, socket}
end
def handle_event("remove_column", %{"index" => index}, socket) do
index = String.to_integer(index)
%__MODULE__{rows: rows, header_rows: header_rows} = table = socket.assigns.block
new_rows =
Enum.map(rows, fn columns ->
columns
|> Enum.with_index()
|> Enum.reject(fn {_row, row_index} -> row_index === index end)
|> Enum.map(fn {row, _row_index} -> row end)
end)
new_header_rows =
Enum.map(header_rows, fn columns ->
columns
|> Enum.with_index()
|> Enum.reject(fn {_row, row_index} -> row_index === index end)
|> Enum.map(fn {row, _row_index} -> row end)
end)
new_table = %{table | rows: new_rows, header_rows: new_header_rows}
%{blocks: blocks} = editor = socket.assigns.editor
index = Enum.find_index(blocks, &(&1.id === table.id))
new_editor = %{editor | blocks: List.replace_at(blocks, index, new_table)}
send(self(), {:update, new_editor})
{:noreply, socket}
end
def handle_event("remove_row", %{"index" => index}, socket) do
index = String.to_integer(index)
%__MODULE__{rows: rows} = table = socket.assigns.block
new_rows = List.delete_at(rows, index)
new_table = %{table | rows: new_rows}
%{blocks: blocks} = editor = socket.assigns.editor
index = Enum.find_index(blocks, &(&1.id === table.id))
new_editor = %{editor | blocks: List.replace_at(blocks, index, new_table)}
send(self(), {:update, new_editor})
{:noreply, socket}
end
def handle_event(
"update_cell",
%{
"cell" => content,
"cell_index" => cell_index,
"row_index" => row_index,
"cell_type" => "body"
},
socket
) do
cell_index = String.to_integer(cell_index)
row_index = String.to_integer(row_index)
%__MODULE__{rows: rows} = table = socket.assigns.block
row = Enum.at(rows, row_index)
new_row = List.replace_at(row, cell_index, content)
new_rows = List.replace_at(rows, row_index, new_row)
new_table = %{table | rows: new_rows}
%{blocks: blocks} = editor = socket.assigns.editor
index = Enum.find_index(blocks, &(&1.id === table.id))
new_editor = %{editor | blocks: List.replace_at(blocks, index, new_table)}
send(self(), {:update, new_editor})
{:noreply, socket}
end
def handle_event(
"update_cell",
%{
"cell" => content,
"cell_index" => cell_index,
"row_index" => row_index,
"cell_type" => "head"
},
socket
) do
cell_index = String.to_integer(cell_index)
row_index = String.to_integer(row_index)
%__MODULE__{header_rows: rows} = table = socket.assigns.block
row = Enum.at(rows, row_index)
new_row = List.replace_at(row, cell_index, content)
new_rows = List.replace_at(rows, row_index, new_row)
new_table = %{table | header_rows: new_rows}
%{blocks: blocks} = editor = socket.assigns.editor
index = Enum.find_index(blocks, &(&1.id === table.id))
new_editor = %{editor | blocks: List.replace_at(blocks, index, new_table)}
send(self(), {:update, new_editor})
{:noreply, socket}
end
defp cell_count(%__MODULE__{rows: []}), do: 1
defp cell_count(%__MODULE__{rows: [row | _]}), do: Enum.count(row)
end