Skip to content

Commit

Permalink
js-hooks key controls
Browse files Browse the repository at this point in the history
  • Loading branch information
sorax committed Jan 16, 2024
1 parent 787c2b1 commit 627649d
Show file tree
Hide file tree
Showing 7 changed files with 216 additions and 85 deletions.
146 changes: 116 additions & 30 deletions assets/js/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
import { createItem, updateItem, focusItem, getItemByEvent, getNodeByEvent } from "./item"
import { Node } from "./node"
import { createItem, updateItem, focusItem, getItemByEvent, getItemById, getNodeByItem, getNodeByEvent } from "./item"

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

container.addEventListener("focusin", (event: FocusEvent) => {
const node = getNodeByEvent(event)
const uuid = node.uuid
const { uuid } = getNodeByEvent(event)

this.pushEvent("set_focus", uuid)
})

container.addEventListener("focusout", (event: FocusEvent) => {
const node = getNodeByEvent(event)
const uuid = node.uuid
const { uuid } = getNodeByEvent(event)

this.pushEvent("remove_focus", uuid)
})
Expand All @@ -27,64 +26,151 @@ export const Hooks = {

container.addEventListener("keydown", (event: KeyboardEvent) => {
const selection = window.getSelection()
const range = selection?.getRangeAt(0)
// const range = selection?.getRangeAt(0)

const node = getNodeByEvent(event)

const item = getItemByEvent(event)
const prevItem = <HTMLLIElement>item.previousSibling
const nextItem = <HTMLLIElement>item.nextSibling

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

case "ArrowUp":
if (selection?.anchorOffset == 0) {
event.preventDefault()
}
if (!prevItem) return
// otherwise parentItem

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

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

if (!nextItem) return
// otherwise firstChildItem

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

case "Tab":
case "Enter":
event.preventDefault()

if (event.shiftKey) {
const splitPos = selection?.anchorOffset || 0

const content = node.content
node.content = content?.substring(0, splitPos)

updateItem(node)

const newNode: Node = {
temp_id: self.crypto.randomUUID(),
content: content?.substring(splitPos),
parent_id: node.parent_id,
prev_id: node.uuid
}

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

focusItem(newItem, false)

this.pushEvent("update_node", node)
this.pushEvent("create_node", newNode)
break

case "Backspace":
if (node.content.length == 0) {
const item = getItemByEvent(event)
item.parentNode!.removeChild(item)
if (selection?.anchorOffset != 0) return
event.preventDefault()

// focus next item
if (!prevItem) return

this.pushEvent("delete_node", node.uuid)
}
const prevNode = getNodeByItem(prevItem)
prevNode.content += node.content
updateItem(prevNode)

item.parentNode?.removeChild(item)

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

case "Delete":
if (node.content.length == 0) {
const item = getItemByEvent(event)
item.parentNode!.removeChild(item)
if (selection?.anchorOffset != node.content.length) return
event.preventDefault()

// focus next item
if (!nextItem) return

this.pushEvent("delete_node", node.uuid)
}
const nextNode = getNodeByItem(nextItem)
node.content += nextNode.content
updateItem(node)

nextItem.parentNode?.removeChild(nextItem)

focusItem(item)
this.pushEvent("delete_node", nextNode.uuid)
break

// case "Tab":
// event.preventDefault()

// if (event.shiftKey) {
// // outdentNode(node)
// // node.prev_id = node.parent_id
// } else {
// // indentNode(node)
// }
// break
}
})

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

this.handleEvent("list", ({ nodes }) => {
nodes.forEach(node => {
const item = createItem(node)
container.prepend(item)
container.append(item)
})

// sort all items
nodes.forEach(node => {
const item = getItemById(node.uuid)
const prevItem = getItemById(node.prev_id)
const parentItem = getItemById(node.parent_id)

if (prevItem) {
prevItem.after(item)
} else if (parentItem) {
parentItem.querySelector("ol")?.append(item)
} else {
container.append(item)
}
})

const lastItem = container.lastElementChild as HTMLLIElement
focusItem(lastItem)
})

// this.handleEvent("insert", (node: Node) => {
// const item = createItem(node)
// container.append(item)
// })

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

// this.handleEvent("delete", ({ uuid }: Node) => {
// const item = getItemById(uuid!)
// item.parentNode!.removeChild(item)
// })
}
}
}
51 changes: 26 additions & 25 deletions assets/js/hooks/item.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
interface Node {
uuid?: string
content: string
creator_id?: number
parent_id?: string
prev_id?: string
}
import { Node } from "./node"

export function createItem({ uuid, content, parent_id, prev_id }: Node) {
export function createItem({ uuid, temp_id, content, parent_id, prev_id }: Node) {
const input = document.createElement("div")
input.textContent = content
input.contentEditable = "plaintext-only"

// const ol = document.createElement("ol")
const ol = document.createElement("ol")

const item = document.createElement("li")
temp_id && (item.id = "outline-node-" + temp_id)
uuid && (item.id = "outline-node-" + uuid)

item.className = "my-2 ml-2"
Expand All @@ -22,21 +17,22 @@ export function createItem({ uuid, content, parent_id, prev_id }: Node) {
item.setAttribute("data-prev", prev_id || "")

item.appendChild(input)
// item.appendChild(ol)
item.appendChild(ol)

return item
}

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

if (item) {
const input = item.firstChild!
input.textContent = content
temp_id && uuid && (item.id = "outline-node-" + uuid)

item.setAttribute("data-parent", parent_id || "")
item.setAttribute("data-prev", prev_id || "")
}
const input = item.firstChild!
input.textContent = content

item.setAttribute("data-parent", parent_id || "")
item.setAttribute("data-prev", prev_id || "")
}

export function getItemById(uuid: string) {
Expand All @@ -63,26 +59,31 @@ export function getNodeByItem(item: HTMLLIElement): Node {
const input = item.firstChild as HTMLDivElement
const content = input.textContent!

const parent_id = item.getAttribute("data-parent")!
const prev_id = item.getAttribute("data-prev")!
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 uuid = item.id.split("outline-node-")[1]
const input = item.firstChild as HTMLDivElement
input.focus()

if (toEnd) {
const range = document.createRange()
range.selectNodeContents(input)
range.collapse(false)
range.setStart(input, 1)
range.collapse(true)

const selection = window.getSelection()
selection?.removeAllRanges()
selection?.addRange(range)
}

this.pushEvent("set_focus", uuid)
}

// export function indentNode(node: Node) {
// // const node = event.target.parentNode
// // const parentNode = event.target.parentNode.previousSibling
// }

// export function outdentNode(node: Node) {
// }
8 changes: 8 additions & 0 deletions assets/js/hooks/node.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Node {
uuid?: string
temp_id?: string
content: string
creator_id?: number
parent_id?: string
prev_id?: string
}
2 changes: 2 additions & 0 deletions config/dev.exs
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,5 @@ config :phoenix_live_view, :debug_heex_annotations, true

# Disable swoosh api client as it is only required for production adapters.
config :swoosh, :api_client, false

config :mix_test_watch, clear: true
19 changes: 0 additions & 19 deletions lib/radiator/outline.ex
Original file line number Diff line number Diff line change
Expand Up @@ -69,25 +69,6 @@ defmodule Radiator.Outline do
|> broadcast_node_action(:insert)
end

@doc """
Upsert a node.
## Examples
iex> upsert_node(%{field: new_value})
{:ok, %Node{}}
iex> upsert_node(%{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def upsert_node(attrs) do
%Node{}
|> Node.changeset(attrs)
|> Repo.insert_or_update()
|> broadcast_node_action(:update)
end

@doc """
Updates a node.
Expand Down
Loading

0 comments on commit 627649d

Please sign in to comment.