From 142a831bcdcb2ceda50d3f7b8bf5d682be95d80f Mon Sep 17 00:00:00 2001 From: sorax Date: Thu, 7 Dec 2023 20:53:35 +0100 Subject: [PATCH] feat: client-side (js) node editing --- assets/js/app.js | 13 +++-- assets/js/hooks/index.ts | 22 ++++++++ assets/js/hooks/node.ts | 20 +++++++ lib/radiator/outline.ex | 1 - lib/radiator/outline/node.ex | 2 + lib/radiator_web/live/outline_live/index.ex | 53 ++++++++----------- .../live/outline_live/index.html.heex | 31 +++-------- test/radiator_web/live/outline_live_test.exs | 30 ++++++----- 8 files changed, 99 insertions(+), 73 deletions(-) create mode 100644 assets/js/hooks/index.ts create mode 100644 assets/js/hooks/node.ts diff --git a/assets/js/app.js b/assets/js/app.js index df0cdd9f..ff076805 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -18,15 +18,20 @@ // Include phoenix_html to handle method=PUT/DELETE in forms and buttons. import "phoenix_html" // Establish Phoenix Socket and LiveView configuration. -import {Socket} from "phoenix" -import {LiveSocket} from "phoenix_live_view" +import { Socket } from "phoenix" +import { LiveSocket } from "phoenix_live_view" import topbar from "../vendor/topbar" +import { Hooks } from "./hooks" + let csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") -let liveSocket = new LiveSocket("/live", Socket, {params: {_csrf_token: csrfToken}}) +let liveSocket = new LiveSocket("/live", Socket, { + hooks: Hooks, + params: { _csrf_token: csrfToken } +}) // Show progress bar on live navigation and form submits -topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) +topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }) window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) diff --git a/assets/js/hooks/index.ts b/assets/js/hooks/index.ts new file mode 100644 index 00000000..f7de1423 --- /dev/null +++ b/assets/js/hooks/index.ts @@ -0,0 +1,22 @@ +import { createNode } from "./node"; + +export const Hooks = { + outline: { + mounted() { + const container: HTMLElement = this.el + + container.addEventListener("keydown", (event) => { + }) + + container.addEventListener("keyup", (event) => { + }) + + this.handleEvent("insert", ({ nodes }) => { + nodes.forEach(node => { + const li = createNode(node) + container.prepend(li) + }); + }) + } + } +} diff --git a/assets/js/hooks/node.ts b/assets/js/hooks/node.ts new file mode 100644 index 00000000..b8e91a0a --- /dev/null +++ b/assets/js/hooks/node.ts @@ -0,0 +1,20 @@ +interface Node { + uuid?: string + content?: string + creator_id?: number + parent_id?: string + prev_id?: string +} + +export function createNode({ uuid, content }: Node) { + const input = document.createElement("div") + input.innerText = content || "" + input.contentEditable = "plaintext-only" + + const node = document.createElement("li") + node.className = "my-2 ml-2" + node.appendChild(input) + uuid && (node.id = "outline-node-" + uuid) + + return node +} diff --git a/lib/radiator/outline.ex b/lib/radiator/outline.ex index 368aff01..20cbe7c9 100644 --- a/lib/radiator/outline.ex +++ b/lib/radiator/outline.ex @@ -22,7 +22,6 @@ defmodule Radiator.Outline do """ def list_nodes do Node - |> order_by(desc: :updated_at) |> Repo.all() end diff --git a/lib/radiator/outline/node.ex b/lib/radiator/outline/node.ex index ac00b487..f614772d 100644 --- a/lib/radiator/outline/node.ex +++ b/lib/radiator/outline/node.ex @@ -2,6 +2,8 @@ defmodule Radiator.Outline.Node do use Ecto.Schema import Ecto.Changeset + @derive {Jason.Encoder, only: [:uuid, :content, :creator_id, :parent_id, :prev_id]} + @primary_key {:uuid, :binary_id, autogenerate: true} schema "outline_nodes" do field :content, :string diff --git a/lib/radiator_web/live/outline_live/index.ex b/lib/radiator_web/live/outline_live/index.ex index 520961fd..612a010c 100644 --- a/lib/radiator_web/live/outline_live/index.ex +++ b/lib/radiator_web/live/outline_live/index.ex @@ -3,8 +3,6 @@ defmodule RadiatorWeb.OutlineLive.Index do alias Radiator.Accounts alias Radiator.Outline - alias Radiator.Outline.Node - alias RadiatorWeb.Endpoint @topic "outline" @@ -15,7 +13,7 @@ defmodule RadiatorWeb.OutlineLive.Index do Endpoint.subscribe(@topic) end - node = %Node{} + node = %Outline.Node{} changeset = Outline.change_node(node) socket @@ -23,49 +21,40 @@ defmodule RadiatorWeb.OutlineLive.Index do |> assign(:bookmarklet, get_bookmarklet(Endpoint.url() <> "/api/v1/outline", socket)) |> assign(:node, node) |> assign(:form, to_form(changeset)) - |> stream_configure(:nodes, dom_id: &"node-#{&1.uuid}") - |> stream(:nodes, Outline.list_nodes()) + |> push_event("insert", %{nodes: Outline.list_nodes()}) |> reply(:ok) end - @impl true - def handle_event("update", %{"node" => _params}, socket) do - socket - |> reply(:noreply) - end - @impl true def handle_event("next", %{"node" => params}, socket) do user = socket.assigns.current_user - Outline.create_node(params, user) + {:ok, node} = Outline.create_node(params, user) socket - # |> stream_insert(:nodes, node, at: 0) + |> push_event("insert", %{nodes: [node]}) |> reply(:noreply) end - @impl true - def handle_event("delete", %{"uuid" => uuid}, socket) do - node = Outline.get_node!(uuid) - Outline.delete_node(node) + # def handle_event("delete", %{"uuid" => uuid}, socket) do + # node = Outline.get_node!(uuid) + # Outline.delete_node(node) - socket - # |> stream_delete(:nodes, node) - |> reply(:noreply) - end + # socket + # |> reply(:noreply) + # end - @impl true - def handle_info({:insert, node}, socket) do - socket - |> stream_insert(:nodes, node, at: 0) - |> reply(:noreply) - end + # @impl true + # def handle_info({:insert, node}, socket) do + # socket + # |> push_event("insert", node) + # |> reply(:noreply) + # end - def handle_info({:delete, node}, socket) do - socket - |> stream_delete(:nodes, node) - |> reply(:noreply) - end + # def handle_info({:delete, node}, socket) do + # socket + # |> push_event("delete", node) + # |> reply(:noreply) + # end defp get_bookmarklet(api_uri, socket) do token = diff --git a/lib/radiator_web/live/outline_live/index.html.heex b/lib/radiator_web/live/outline_live/index.html.heex index e4d4ebb8..b2d185b2 100644 --- a/lib/radiator_web/live/outline_live/index.html.heex +++ b/lib/radiator_web/live/outline_live/index.html.heex @@ -16,29 +16,14 @@

Inbox

- <.focus_wrap id="input-form-wrap"> - <.form id="inbox-form" for={@form} phx-change="update" phx-submit="next"> - <.input type="text" field={@form[:content]} /> - <.input type="hidden" field={@form[:uuid]} /> - - + <.form id="inbox-form" for={@form} phx-submit="next"> + <.input + type="text" + field={@form[:content]} + placeholder="this input will be removed in the future" + /> + - +
    diff --git a/test/radiator_web/live/outline_live_test.exs b/test/radiator_web/live/outline_live_test.exs index e8fc6818..6c80cab0 100644 --- a/test/radiator_web/live/outline_live_test.exs +++ b/test/radiator_web/live/outline_live_test.exs @@ -29,11 +29,14 @@ defmodule RadiatorWeb.OutlineLiveTest do %{conn: log_in_user(conn, user), node: node} end - test "lists all nodes", %{conn: conn, node: node} do - {:ok, _live, html} = live(conn, ~p"/admin/outline") + test "lists all nodes", %{conn: conn, node: _node} do + {:ok, live, _html} = live(conn, ~p"/admin/outline") + + assert live |> element("h2", "Inbox") - assert html =~ "Inbox" - assert html =~ node.content + assert_push_event(live, "insert", %{nodes: [%{content: "some content"}]}) + + # assert html =~ node.content end test "save new node", %{conn: conn} do @@ -45,18 +48,19 @@ defmodule RadiatorWeb.OutlineLiveTest do assert live |> element("ul#inbox") - |> render() =~ "new node content" + + assert_push_event(live, "insert", %{nodes: [%{content: "new node content"}]}) end - test "delete existing node", %{conn: conn, node: node} do - {:ok, live, _html} = live(conn, ~p"/admin/outline") + # test "delete existing node", %{conn: conn, node: node} do + # {:ok, live, _html} = live(conn, ~p"/admin/outline") - assert live - |> element("#node-#{node.uuid} a", "Delete") - |> render_click() + # assert live + # |> element("#node-#{node.uuid} a", "Delete") + # |> render_click() - refute live - |> has_element?("#node-#{node.uuid}") - end + # refute live + # |> has_element?("#node-#{node.uuid}") + # end end end