Skip to content

Commit

Permalink
refactor: outline-hook
Browse files Browse the repository at this point in the history
  • Loading branch information
sorax committed Feb 15, 2024
1 parent 2e39001 commit 2294c2a
Show file tree
Hide file tree
Showing 14 changed files with 177 additions and 141 deletions.
123 changes: 60 additions & 63 deletions assets/js/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { createItem, updateItem, focusItem, getItemByEvent, getItemById, getNodeByItem, getNodeByEvent } from "./item"
import { Node } from "./types"
import { createItem, updateItem, deleteItem, getItemByNode, focusItem } from "./item"
import { getNodeByEvent, getNodeByItem } from "./node"

export const Hooks = {
outline: {
mounted() {
const container: HTMLElement = this.el
const container: HTMLOListElement = this.el

container.addEventListener("focusin", (event: FocusEvent) => {
const { uuid } = getNodeByEvent(event)
Expand All @@ -30,31 +31,34 @@ export const Hooks = {

const node = getNodeByEvent(event)

const item = getItemByEvent(event)
const prevItem = <HTMLLIElement>item.previousSibling
const nextItem = <HTMLLIElement>item.nextSibling
const item = getItemByNode(node)!
const prevItem = item.previousSibling as HTMLLIElement | null
const nextItem = item.nextSibling as HTMLLIElement | null

const prevNode = prevItem && getNodeByItem(prevItem)
const nextNode = nextItem && getNodeByItem(nextItem)

switch (event.key) {
case "ArrowUp":
if (selection?.anchorOffset != 0) return
event.preventDefault()

if (!prevItem) return
// otherwise parentItem
if (!prevItem || !prevNode) return
// TODO: if no prevItem exists, try to select the parent item

focusItem(prevItem)
this.pushEvent("set_focus", node.uuid)
this.pushEvent("set_focus", prevNode.uuid)
break

case "ArrowDown":
if (selection?.anchorOffset != node.content.length) return
event.preventDefault()

if (!nextItem) return
// otherwise firstChildItem
if (!nextItem || !nextNode) return
// TODO: if no nextItem exists, try to select the first child

focusItem(nextItem)
this.pushEvent("set_focus", node.uuid)
this.pushEvent("set_focus", nextNode.uuid)
break

case "Enter":
Expand All @@ -66,6 +70,7 @@ export const Hooks = {
node.content = content?.substring(0, splitPos)

updateItem(node, container)
this.pushEvent("update_node", node)

const newNode: Node = {
temp_id: self.crypto.randomUUID(),
Expand All @@ -74,85 +79,79 @@ export const Hooks = {
prev_id: node.uuid
}

const newItem = createItem(newNode)
item.after(newItem)

focusItem(newItem, false)

this.pushEvent("update_node", node)
this.pushEvent("create_node", newNode)
this.pushEvent("create_node", newNode, (node: Node, _ref: Number) => {
const newItem = createItem(node)
item.after(newItem)
focusItem(newItem, false)
})
break

case "Backspace":
if (selection?.anchorOffset != 0) return
event.preventDefault()

if (!prevItem) return
if (!prevItem || !prevNode) return

const prevNode = getNodeByItem(prevItem)
prevNode.content += node.content
updateItem(prevNode, container)

item.parentNode?.removeChild(item)

focusItem(prevItem)
this.pushEvent("update_node", node)

deleteItem(node)
this.pushEvent("delete_node", node.uuid)
break

case "Delete":
if (selection?.anchorOffset != node.content.length) return
event.preventDefault()

if (!nextItem) return
if (!nextItem || !nextNode) return

const nextNode = getNodeByItem(nextItem)
node.content += nextNode.content
updateItem(node, container)

nextItem.parentNode?.removeChild(nextItem)

focusItem(item)
this.pushEvent("update_node", node)

deleteItem(nextNode)
this.pushEvent("delete_node", nextNode.uuid)
break

case "Tab":
event.preventDefault()

if (event.shiftKey) {
if (node.parent_id) {
node.prev_id = node.parent_id
node.parent_id = undefined

updateItem(node, container)
focusItem(item)

this.pushEvent("update_node", node)
}
} else {
if (node.prev_id) {
node.parent_id = node.prev_id
node.prev_id = undefined

updateItem(node, container)
focusItem(item)

this.pushEvent("update_node", node)
}
}
break
// case "Tab":
// event.preventDefault()

// if (event.shiftKey) {
// if (node.parent_id) {
// // outdent
// node.prev_id = node.parent_id
// node.parent_id = undefined
// updateItem(node, container)

// focusItem(item)
// this.pushEvent("update_node", node)
// }
// } else {
// if (node.prev_id) {
// // indent
// node.parent_id = node.prev_id
// node.prev_id = undefined // TODO: prev_id should be the id of the last child of the parent node
// updateItem(node, container)

// focusItem(item)
// this.pushEvent("update_node", node)
// }
// }
// break
}
})

// container.addEventListener("keyup", (event) => {
// console.log("keyup", event)
// })

this.handleEvent("list", ({ nodes }) => {
this.handleEvent("list", ({ nodes }: { nodes: Node[] }) => {
if ((nodes.length) == 0) {
nodes = [{
temp_id: self.crypto.randomUUID(),
content: "",
}]
const node: Node = { temp_id: self.crypto.randomUUID(), content: "" }
nodes = [node]
}

// add all items
Expand All @@ -177,13 +176,11 @@ export const Hooks = {
// })

// this.handleEvent("update", (node: Node) => {
// // console.log(node)
// // updateItem(node)
// updateItem(node, container)
// })

// this.handleEvent("delete", ({ uuid }: Node) => {
// const item = getItemById(uuid!)
// item.parentNode!.removeChild(item)
// this.handleEvent("delete", (node: Node) => {
// deleteItem(node)
// })
}
}
Expand Down
37 changes: 15 additions & 22 deletions assets/js/hooks/item.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ import { Node } from "./types"
export function createItem({ uuid, temp_id, content, parent_id, prev_id }: Node) {
const input = document.createElement("div")
input.textContent = content
// input.contentEditable = "plaintext-only"
input.contentEditable = "true"
input.contentEditable = "true" // firefox does not support "plaintext-only"

const ol = document.createElement("ol")
ol.className = "list-disc"
Expand All @@ -24,8 +23,8 @@ export function createItem({ uuid, temp_id, content, parent_id, prev_id }: Node)
return item
}

export function updateItem({ uuid, temp_id, content, parent_id, prev_id }: Node, container: HTMLElement) {
const item = getItemById(temp_id || uuid!)
export function updateItem({ uuid, temp_id, content, parent_id, prev_id }: Node, container: HTMLOListElement) {
const item = getItemById(temp_id || uuid)
if (!item) return

temp_id && uuid && (item.id = "outline-node-" + uuid)
Expand All @@ -48,36 +47,30 @@ export function updateItem({ uuid, temp_id, content, parent_id, prev_id }: Node,
}
}

export function getItemById(uuid: string | undefined) {
if (!uuid) return null
export function deleteItem({ uuid }: Node) {
const item = getItemById(uuid)
if (!item) return

return document.getElementById("outline-node-" + uuid)
item.parentNode!.removeChild(item)
}

export function getNodeByEvent(event: Event): Node {
const item = getItemByEvent(event)
export function getItemByNode({ uuid, temp_id }: Node) {
return getItemById(temp_id || uuid)
}

function getItemById(uuid: string | undefined) {
if (!uuid) return null

return getNodeByItem(item)
return document.getElementById("outline-node-" + uuid) as HTMLLIElement
}

export function getItemByEvent(event: Event): HTMLLIElement {
const target = <HTMLElement>event.target
const target = <HTMLDivElement>event.target
const item = <HTMLLIElement>target.parentElement!

return item
}

export function getNodeByItem(item: HTMLLIElement): Node {
const uuid = item.id.split("outline-node-")[1]
const input = item.firstChild as HTMLDivElement
const content = input.textContent!

const parent_id = item.getAttribute("data-parent") || undefined
const prev_id = item.getAttribute("data-prev") || undefined

return { uuid, content, parent_id, prev_id }
}

export function focusItem(item: HTMLLIElement, toEnd: boolean = true) {
const input = item.firstChild as HTMLDivElement
input.focus()
Expand Down
19 changes: 19 additions & 0 deletions assets/js/hooks/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { UUID, Node } from "./types"
import { getItemByEvent } from "./item"

export function getNodeByEvent(event: Event): Node {
const item = getItemByEvent(event)

return getNodeByItem(item)
}

export function getNodeByItem(item: HTMLLIElement): Node {
const uuid = item.id.split("outline-node-")[1] as UUID
const input = item.firstChild as HTMLDivElement
const content = input.textContent || ""

const parent_id = item.getAttribute("data-parent") as UUID || undefined
const prev_id = item.getAttribute("data-prev") as UUID || undefined

return { uuid, content, parent_id, prev_id }
}
10 changes: 6 additions & 4 deletions assets/js/hooks/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
export type UUID = `${string}-${string}-${string}-${string}-${string}`

export interface Node {
uuid?: string
temp_id?: string
uuid?: UUID
temp_id?: UUID
content: string
creator_id?: number
parent_id?: string
prev_id?: string
parent_id?: UUID
prev_id?: UUID
}
1 change: 1 addition & 0 deletions lib/extension/phoenix/socket.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ defmodule Extension.Phoenix.Socket do
"""

def reply(socket, reply) when is_atom(reply), do: {reply, socket}
def reply(socket, reply, data) when is_atom(reply) and is_map(data), do: {reply, data, socket}
end
25 changes: 17 additions & 8 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ defmodule Radiator.Outline do
alias Radiator.Outline.Node
alias Radiator.Repo

@topic "outline"
@topic "outline-node"

@doc """
Returns the list of nodes.
Expand All @@ -24,6 +24,22 @@ defmodule Radiator.Outline do
|> Repo.all()
end

@doc """
Returns the list of nodes for an episode.
## Examples
iex> list_nodes(123)
[%Node{}, ...]
"""

def list_nodes_by_episode(episode_id) do
Node
|> where([p], p.episode_id == ^episode_id)
|> Repo.all()
end

@doc """
Gets a single node.
Expand Down Expand Up @@ -81,13 +97,6 @@ defmodule Radiator.Outline do
|> broadcast_node_action(:insert)
end

def create_node(attrs, %{id: id}) do
%Node{creator_id: id}
|> Node.changeset(attrs)
|> Repo.insert()
|> broadcast_node_action(:insert)
end

@doc """
Updates a node.
Expand Down
2 changes: 1 addition & 1 deletion lib/radiator/outline/node.ex
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,11 @@ defmodule Radiator.Outline.Node do
end

@required_fields [
:content,
:episode_id
]

@optional_fields [
:content,
:creator_id,
:parent_id,
:prev_id
Expand Down
9 changes: 7 additions & 2 deletions lib/radiator_web/controllers/api/outline_controller.ex
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,13 @@ defmodule RadiatorWeb.Api.OutlineController do
defp create_node(nil, _, _), do: {:error, :user}
defp create_node(_, _, nil), do: {:error, :episode}

defp create_node(user, content, episode_id),
do: Outline.create_node(%{"content" => content, "episode_id" => episode_id}, user)
defp create_node(user, content, episode_id) do
Outline.create_node(%{
"content" => content,
"creator_id" => user.id,
"episode_id" => episode_id
})
end

defp get_response({:ok, node}), do: {200, %{uuid: node.uuid}}
defp get_response({:error, _}), do: {400, %{error: "params"}}
Expand Down
Loading

0 comments on commit 2294c2a

Please sign in to comment.