diff --git a/assets/js/hooks/event_handler.ts b/assets/js/hooks/event_handler.ts new file mode 100644 index 00000000..5fcb9271 --- /dev/null +++ b/assets/js/hooks/event_handler.ts @@ -0,0 +1,43 @@ +import { Node } from "./types" +import { createItem, updateItem, deleteItem, focusItem } from "./item" + +export function handleList({ nodes }: { nodes: Node[] }) { + const container: HTMLOListElement = this.el + + if ((nodes.length) == 0) { + const node: Node = { temp_id: self.crypto.randomUUID(), content: "" } + nodes = [node] + } + + // add all items + nodes.forEach(node => { + const item = createItem(node) + container.append(item) + }) + + // sort & indent all items + nodes.forEach(node => { + updateItem(node, container) + }) + + // focus last item + const lastItem = container.lastElementChild as HTMLLIElement + focusItem(lastItem) +} + +export function handleInsert(node: Node) { + const container: HTMLOListElement = this.el + + const item = createItem(node) + container.append(item) +} + +export function handleUpdate(node: Node) { + const container: HTMLOListElement = this.el + + updateItem(node, container) +} + +export function handleDelete(node: Node) { + deleteItem(node) +} diff --git a/assets/js/hooks/event_listener.ts b/assets/js/hooks/event_listener.ts new file mode 100644 index 00000000..d02f006f --- /dev/null +++ b/assets/js/hooks/event_listener.ts @@ -0,0 +1,144 @@ +import { Node } from "./types" +import { createItem, updateItem, deleteItem, getItemByNode, focusItem } from "./item" +import { getNodeByEvent, getNodeByItem } from "./node" + +export function focusin(event: FocusEvent) { + const { uuid } = getNodeByEvent(event) + + this.pushEvent("set_focus", uuid) +} + +export function focusout(event: FocusEvent) { + const { uuid } = getNodeByEvent(event) + + this.pushEvent("remove_focus", uuid) +} + +export function input(event: Event) { + const node = getNodeByEvent(event) + + this.pushEvent("update_node", node) +} + +export function keydown(event: KeyboardEvent) { + const container: HTMLOListElement = this.el + + const selection = window.getSelection() + // const range = selection?.getRangeAt(0) + + const node = getNodeByEvent(event) + + 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 || !prevNode) return + // TODO: if no prevItem exists, try to select the parent item + + focusItem(prevItem) + this.pushEvent("set_focus", prevNode.uuid) + break + + case "ArrowDown": + if (selection?.anchorOffset != node.content.length) return + event.preventDefault() + + if (!nextItem || !nextNode) return + // TODO: if no nextItem exists, try to select the first child + + focusItem(nextItem) + this.pushEvent("set_focus", nextNode.uuid) + break + + case "Enter": + event.preventDefault() + + const splitPos = selection?.anchorOffset || 0 + + const content = node.content + node.content = content?.substring(0, splitPos) + + updateItem(node, container) + this.pushEvent("update_node", node) + + const newNode: Node = { + temp_id: self.crypto.randomUUID(), + content: content?.substring(splitPos), + parent_id: node.parent_id, + prev_id: node.uuid + } + + 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 || !prevNode) return + + prevNode.content += node.content + updateItem(prevNode, container) + 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 || !nextNode) return + + node.content += nextNode.content + updateItem(node, container) + 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) { + // 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 + } +} + + diff --git a/assets/js/hooks/index.ts b/assets/js/hooks/index.ts index 10428d20..b7d5f3db 100644 --- a/assets/js/hooks/index.ts +++ b/assets/js/hooks/index.ts @@ -1,187 +1,22 @@ -import { Node } from "./types" -import { createItem, updateItem, deleteItem, getItemByNode, focusItem } from "./item" -import { getNodeByEvent, getNodeByItem } from "./node" +import { focusin, focusout, input, keydown } from "./event_listener" +import { handleList, handleInsert, handleUpdate, handleDelete } from "./event_handler" export const Hooks = { outline: { mounted() { const container: HTMLOListElement = this.el - container.addEventListener("focusin", (event: FocusEvent) => { - const { uuid } = getNodeByEvent(event) + container.addEventListener("focusin", focusin.bind(this)) + container.addEventListener("focusout", focusout.bind(this)) + container.addEventListener("input", input.bind(this)) - this.pushEvent("set_focus", uuid) - }) + container.addEventListener("keydown", keydown.bind(this)) + // container.addEventListener("keyup", keyup.bind(this)) - container.addEventListener("focusout", (event: FocusEvent) => { - const { uuid } = getNodeByEvent(event) - - this.pushEvent("remove_focus", uuid) - }) - - container.addEventListener("input", (event: Event) => { - const node = getNodeByEvent(event) - - this.pushEvent("update_node", node) - }) - - container.addEventListener("keydown", (event: KeyboardEvent) => { - const selection = window.getSelection() - // const range = selection?.getRangeAt(0) - - const node = getNodeByEvent(event) - - 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 || !prevNode) return - // TODO: if no prevItem exists, try to select the parent item - - focusItem(prevItem) - this.pushEvent("set_focus", prevNode.uuid) - break - - case "ArrowDown": - if (selection?.anchorOffset != node.content.length) return - event.preventDefault() - - if (!nextItem || !nextNode) return - // TODO: if no nextItem exists, try to select the first child - - focusItem(nextItem) - this.pushEvent("set_focus", nextNode.uuid) - break - - case "Enter": - event.preventDefault() - - const splitPos = selection?.anchorOffset || 0 - - const content = node.content - node.content = content?.substring(0, splitPos) - - updateItem(node, container) - this.pushEvent("update_node", node) - - const newNode: Node = { - temp_id: self.crypto.randomUUID(), - content: content?.substring(splitPos), - parent_id: node.parent_id, - prev_id: node.uuid - } - - 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 || !prevNode) return - - prevNode.content += node.content - updateItem(prevNode, container) - 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 || !nextNode) return - - node.content += nextNode.content - updateItem(node, container) - 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) { - // 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 }: { nodes: Node[] }) => { - if ((nodes.length) == 0) { - const node: Node = { temp_id: self.crypto.randomUUID(), content: "" } - nodes = [node] - } - - // add all items - nodes.forEach(node => { - const item = createItem(node) - container.append(item) - }) - - // sort & indent all items - nodes.forEach(node => { - updateItem(node, container) - }) - - // focus last 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) => { - updateItem(node, container) - }) - - this.handleEvent("delete", (node: Node) => { - deleteItem(node) - }) + this.handleEvent("list", handleList.bind(this)) + this.handleEvent("insert", handleInsert.bind(this)) + this.handleEvent("update", handleUpdate.bind(this)) + this.handleEvent("delete", handleDelete.bind(this)) } } }