Skip to content

Commit

Permalink
[REFACTOR] Extensible block - initial phase (#38)
Browse files Browse the repository at this point in the history
* [REFACTOR] Introduce common LiveBlock component

* Remove unused code

* Introduce common StaticBlock component

* Update changelog

* Fix incorrect block transform

Any transform containing a space character ended up being applied
too soon due to a "hack" space character being put into every new cell.

* Add blank `.credo.exs`

* Make ContentEditable less hacky

Rely on building correct cells and selection in hook
rather than backend placing in custom characters

* Document ContentEditable.ts

* Run build

* Simplify spec

* Revert page.spec

* Fix tests

* Revert block.scss
  • Loading branch information
begedin authored Jun 21, 2022
1 parent 1924716 commit 06283e3
Show file tree
Hide file tree
Showing 20 changed files with 344 additions and 187 deletions.
1 change: 1 addition & 0 deletions .credo.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

- [QA] Simplify Playground endpoint
- [QA] Reorganize e2e tests around scopes
- [REFACTOR] Introduce common LiveBlock component
- [REFACTOR] Introduce common StaticBlock component
- [FIX] Transform being applied to soon due to placeholder space character in new cells
- [QA] Add blank `.credo.exs`
- [REFACTOR] Make ContentEditable less hacky
- [QA] Document ContentEditable

## 0.10.1

Expand Down
6 changes: 3 additions & 3 deletions cypress/integration/page.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('ui.page', () => {
});

// not the most robust of tests, as tab is achieved via custom plugin and
// typing withouth changing focus is not easily possible
// typing without changing focus is not easily possible
it('can navigate focused blocks via tab and shift+tab', () => {
visitNew();
section(0).focus().tab();
Expand All @@ -55,12 +55,12 @@ describe('ui.page', () => {
block(1).type('{moveToEnd}{enter}');
block(2).should('exist');
section(2).should('have.attr', 'data-focused');
block(2).type('{moveToEnd}bar{moveToStart}{backspace}');
block(2).focus().type('bar').type('{moveToStart}').type('{backspace}');

section(1).should('have.attr', 'data-focused');
section(0).should('not.have.attr', 'data-focused');

block(1).should('contain.text', 'paragraph. bar').tab({ shift: true });
block(1).should('contain.text', 'paragraph.bar').tab({ shift: true });
section(1).should('not.have.attr', 'data-focused');
section(0).should('have.attr', 'data-focused');
});
Expand Down
2 changes: 1 addition & 1 deletion dist/index.css.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions dist/index.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions dist/index.js.map

Large diffs are not rendered by default.

13 changes: 3 additions & 10 deletions lib/philtre/block/code.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule Philtre.Block.Code do
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
use Phoenix.Component

alias Philtre.Block.ContentEditable
alias Philtre.Editor
Expand All @@ -15,7 +15,7 @@ defmodule Philtre.Block.Code do

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

def render(assigns) do
def render_live(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"""
Expand Down Expand Up @@ -44,14 +44,7 @@ defmodule Philtre.Block.Code do
|> 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
def render_static(%{block: _} = assigns) do
~H"<pre><%= @block.content %></pre>"
end

Expand Down
63 changes: 51 additions & 12 deletions lib/philtre/block/content_editable.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ defmodule Philtre.Block.ContentEditable do
Typing backspace from the start of a P block merges it into the previous block.
"""

use Phoenix.LiveComponent
use Phoenix.Component
use Phoenix.HTML

alias Philtre.Block.ContentEditable
Expand Down Expand Up @@ -51,57 +51,53 @@ defmodule Philtre.Block.ContentEditable do

# component

def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end

def render(%{block: %__MODULE__{type: "p"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "p"}} = assigns) do
~H"""
<p {attrs(@block, @selected, @myself)}>
<.content block={@block} />
</p>
"""
end

def render(%{block: %__MODULE__{type: "pre"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "pre"}} = assigns) do
~H"""
<pre {attrs(@block, @selected, @myself)}><.content block={@block} /></pre>
"""
end

def render(%{block: %__MODULE__{type: "h1"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "h1"}} = assigns) do
~H"""
<h1 {attrs(@block, @selected, @myself)}>
<.content block={@block} />
</h1>
"""
end

def render(%{block: %__MODULE__{type: "h2"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "h2"}} = assigns) do
~H"""
<h2 {attrs(@block, @selected, @myself)}>
<.content block={@block} />
</h2>
"""
end

def render(%{block: %__MODULE__{type: "h3"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "h3"}} = assigns) do
~H"""
<h3 {attrs(@block, @selected, @myself)}>
<.content block={@block} />
</h3>
"""
end

def render(%{block: %__MODULE__{type: "blockquote"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "blockquote"}} = assigns) do
~H"""
<blockquote {attrs(@block, @selected, @myself)}>
<.content block={@block} />
</blockquote>
"""
end

def render(%{block: %__MODULE__{type: "li"}} = assigns) do
def render_live(%{block: %__MODULE__{type: "li"}} = assigns) do
~H"""
<ul>
<li {attrs(@block, @selected, @myself)}>
Expand Down Expand Up @@ -138,11 +134,54 @@ defmodule Philtre.Block.ContentEditable do
data_selection_start_id: block.selection.start_id,
data_selection_start_offset: block.selection.start_offset,
id: block.id,
placeholder: "Type something",
phx_hook: "ContentEditable",
phx_target: myself
}
end

def render_static(%{block: %__MODULE__{type: "p"}} = assigns) do
~H"""
<p><.content block={@block} /></p>
"""
end

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

def render_static(%{block: %__MODULE__{type: "h1"}} = assigns) do
~H"""
<h1><.content block={@block} /></h1>
"""
end

def render_static(%{block: %__MODULE__{type: "h2"}} = assigns) do
~H"""
<h2><.content block={@block} /></h2>
"""
end

def render_static(%{block: %__MODULE__{type: "h3"}} = assigns) do
~H"""
<h3><.content block={@block} /></h3>
"""
end

def render_static(%{block: %__MODULE__{type: "blockquote"}} = assigns) do
~H"""
<blockquote><.content block={@block} /></blockquote>
"""
end

def render_static(%{block: %__MODULE__{type: "li"}} = assigns) do
~H"""
<ul><li><.content block={@block} /></li></ul>
"""
end

def handle_event("update", %{"selection" => selection, "cells" => cells} = attrs, socket) do
Logger.info("update: #{inspect(attrs)}")

Expand Down
4 changes: 2 additions & 2 deletions lib/philtre/block/content_editable/cell.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Philtre.Block.ContentEditable.Cell do

alias Philtre.Editor.Utils

defstruct id: Utils.new_id(), modifiers: [], text: " "
defstruct id: Utils.new_id(), modifiers: [], text: ""

@type id :: String.t()

Expand All @@ -23,7 +23,7 @@ defmodule Philtre.Block.ContentEditable.Cell do
%__MODULE__{
id: Utils.new_id(),
modifiers: [],
text: " "
text: ""
}
end
end
13 changes: 3 additions & 10 deletions lib/philtre/block/table.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ defmodule Philtre.Block.Table do
The current implementation starts of with a single cell, to which additional
rows and cells can be added and removed from.
"""
use Phoenix.LiveComponent
use Phoenix.Component

defstruct id: nil, header_rows: [[""]], rows: [[""]]

def render(assigns) do
def render_live(assigns) do
~H"""
<div class="philtre__table" data-block>
<table>
Expand Down Expand Up @@ -125,7 +125,7 @@ defmodule Philtre.Block.Table do
"""
end

def read_only(%{} = assigns) do
def render_static(%{} = assigns) do
~H"""
<table>
<thead>
Expand All @@ -150,13 +150,6 @@ defmodule Philtre.Block.Table do
"""
end

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

def handle_event("add_row", %{}, socket) do
%__MODULE__{rows: rows} = table = socket.assigns.block

Expand Down
43 changes: 9 additions & 34 deletions lib/philtre/engine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,17 @@ defmodule Philtre.Editor.Engine do
ContentEditable.t(),
%{required(:selection) => map, required(:cells) => list(map)}
) :: Editor.t()
def update(%Editor{} = editor, %ContentEditable{} = block, %{selection: nil, cells: []}) do
%Cell{} = cell = Cell.new()

%ContentEditable{} =
new_block =
block
|> Map.put(:cells, [cell])
|> Map.put(:selection, Selection.new_start_of(cell))
|> resolve_transform()

Editor.replace_block(editor, block, [new_block])
end

def update(%Editor{} = editor, %ContentEditable{cells: old_cells} = block, %{
def update(%Editor{} = editor, %ContentEditable{} = block, %{
selection: selection,
cells: new_cells
}) do
# the new cells are content received from the client side of the ContentEditable hook
# and should be the exact correct content of the updated block
new_cells =
updated_cells =
Enum.map(new_cells, fn %{"id" => id, "modifiers" => modifiers, "text" => text} ->
%Cell{id: id, modifiers: modifiers, text: text}
end)

# the remainder here probably isn't necessary and updated cells should just equal to new cells
# but just in case, until more testing is added, we do not fully trust the frontend and instead
# manually match old with new cell records to update them
new_ids = Enum.map(new_cells, & &1.id)
remaining_cells = Enum.filter(old_cells, &(&1.id in new_ids))

updated_cells =
Enum.map(remaining_cells, fn %Cell{} = cell ->
%Cell{} = params = Enum.find(new_cells, &(&1.id === cell.id))
%{cell | modifiers: params.modifiers, text: params.text}
end)

new_block = resolve_transform(%{block | cells: updated_cells, selection: selection})

Editor.replace_block(editor, block, [new_block])
Expand Down Expand Up @@ -449,7 +424,7 @@ defmodule Philtre.Editor.Engine do
defp empty_block?(%ContentEditable{cells: [cell]}), do: empty_cell?(cell)
defp empty_block?(%ContentEditable{}), do: false

defp empty_cell?(%Cell{text: t}) when t in ["", nil, " ", "&nbsp;", " "], do: true
defp empty_cell?(%Cell{text: ""}), do: true
defp empty_cell?(%Cell{}), do: false

@spec split_cell(Cell.t(), non_neg_integer()) :: {Cell.t(), Cell.t()}
Expand Down Expand Up @@ -554,27 +529,27 @@ defmodule Philtre.Editor.Engine do

@transforms [
%{
prefixes: ["* ", "* "],
prefixes: ["* "],
kind: "li"
},
%{
prefixes: ["# ", "# "],
prefixes: ["# "],
kind: "h1"
},
%{
prefixes: ["## ", "## "],
prefixes: ["## "],
kind: "h2"
},
%{
prefixes: ["### ", "### "],
prefixes: ["### "],
kind: "h3"
},
%{
prefixes: ["```"],
kind: "pre"
},
%{
prefixes: ["> ", "> "],
prefixes: ["> "],
kind: "blockquote"
},
%{
Expand Down Expand Up @@ -654,7 +629,7 @@ defmodule Philtre.Editor.Engine do

new_text =
case replaced do
"" -> " "
"" -> ""
other -> other
end

Expand Down
28 changes: 28 additions & 0 deletions lib/philtre/live_block.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
defmodule Philtre.LiveBlock do
@moduledoc """
Single live component in charge of rendering all types of live blocks.
Current implementation infers block type from struct module and simply
delegates major callbacks to the bloc module.
Later implementations might instead take block type from some sort of registry
and require some sort of return format from the block modules, to decide how
to render them.
Ideally, we want individual blocks to be decoupled from the editor.
"""
use Phoenix.LiveComponent

def update(assigns, socket) do
{:ok, assign(socket, assigns)}
end

def render(%{block: %module{}} = assigns) do
module.render_live(assigns)
end

def handle_event(event, payload, socket) do
%module{} = socket.assigns.block
module.handle_event(event, payload, socket)
end
end
Loading

0 comments on commit 06283e3

Please # to comment.