diff --git a/assets/js/hooks/events/handler.ts b/assets/js/hooks/events/handler.ts index 7a1e140b..cccd08b7 100644 --- a/assets/js/hooks/events/handler.ts +++ b/assets/js/hooks/events/handler.ts @@ -1,16 +1,15 @@ import { Node } from "../types"; import { - getItemByNode, + changeItemContent, + cleanItem, createItem, - updateItem, deleteItem, focusItem, - getItemById, - setItemDirty, + moveItem, } from "../item"; export function handleList({ nodes }: { nodes: Node[] }) { - const container: HTMLOListElement = this.el; + const container: HTMLDivElement = this.el.querySelector(".children"); if (nodes.length == 0) { const node: Node = { @@ -29,33 +28,24 @@ export function handleList({ nodes }: { nodes: Node[] }) { // sort & indent all items nodes.forEach((node) => { - updateItem(node, container); + moveItem(node, container, true); }); // focus last item - const lastItem = container.lastElementChild as HTMLLIElement; + const lastItem = container.lastElementChild as HTMLDivElement; focusItem(lastItem); } export function handleInsert({ node }: { node: Node }) { - const container: HTMLOListElement = this.el; + const container: HTMLDivElement = this.el.querySelector(".children"); const item = createItem(node); container.append(item); - updateItem(node, container); + moveItem(node, container); } export function handleContentChange({ node }: { node: Node }) { - const item = getItemById(node.uuid); - if (!item) { - console.error("item not found"); - return; - } - - const input = item.firstChild!; - input.textContent = node.content || ""; - - setItemDirty(item, false); + changeItemContent(node); } export function handleDelete({ node }: { node: Node }) { @@ -63,6 +53,5 @@ export function handleDelete({ node }: { node: Node }) { } export function handleClean({ node }: { node: Node }) { - const item = getItemById(node.uuid) - item && setItemDirty(item, false); + cleanItem(node); } diff --git a/assets/js/hooks/events/listener.ts b/assets/js/hooks/events/listener.ts index ebb46ba1..d2eaf413 100644 --- a/assets/js/hooks/events/listener.ts +++ b/assets/js/hooks/events/listener.ts @@ -1,10 +1,11 @@ import { Node } from "../types"; import { + changeItemContent, createItem, - updateItem, deleteItem, - getItemByNode, focusItem, + getItemByNode, + moveItem, setItemDirty, } from "../item"; import { getNodeByEvent, getNodeByItem } from "../node"; @@ -30,7 +31,7 @@ export function input(event: Event) { } export function keydown(event: KeyboardEvent) { - const container: HTMLOListElement = this.el; + const container: HTMLDivElement = this.el.querySelector(".children"); const selection = window.getSelection(); // const range = selection?.getRangeAt(0) @@ -38,8 +39,8 @@ export function keydown(event: KeyboardEvent) { const node = getNodeByEvent(event); const item = getItemByNode(node)!; - const prevItem = item.previousSibling as HTMLLIElement | null; - const nextItem = item.nextSibling as HTMLLIElement | null; + const prevItem = item.previousSibling as HTMLDivElement | null; + const nextItem = item.nextSibling as HTMLDivElement | null; const prevNode = prevItem && getNodeByItem(prevItem); const nextNode = nextItem && getNodeByItem(nextItem); @@ -76,7 +77,7 @@ export function keydown(event: KeyboardEvent) { node.content = content?.substring(0, splitPos); node.dirty = true; - updateItem(node, container); + changeItemContent(node); this.pushEvent("update_node_content", node); const newNode: Node = { @@ -86,9 +87,7 @@ export function keydown(event: KeyboardEvent) { prev_id: node.uuid, dirty: true, }; - this.pushEvent("create_node", newNode); - // this.pushEvent("create_node", newNode, (node: Node, _ref: number) => { }); const newItem = createItem(newNode); item.after(newItem); @@ -103,7 +102,7 @@ export function keydown(event: KeyboardEvent) { prevNode.content += node.content || ""; prevNode.dirty = true; - updateItem(prevNode, container); + changeItemContent(prevNode); focusItem(prevItem); this.pushEvent("update_node_content", prevNode); @@ -119,7 +118,7 @@ export function keydown(event: KeyboardEvent) { node.content += nextNode.content || ""; node.dirty = true; - updateItem(node, container); + changeItemContent(node); focusItem(item); this.pushEvent("update_node_content", node); @@ -135,9 +134,9 @@ export function keydown(event: KeyboardEvent) { if (node.parent_id) { node.prev_id = node.parent_id; node.parent_id = prevNode?.parent_id; - updateItem(node, container); + moveItem(node, container); - // focusItem(item); + focusItem(item); this.pushEvent("move_node", node); } } else { @@ -145,9 +144,9 @@ export function keydown(event: KeyboardEvent) { if (node.prev_id) { 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); + moveItem(node, container); - // focusItem(item); + focusItem(item); this.pushEvent("move_node", node); } } diff --git a/assets/js/hooks/index.ts b/assets/js/hooks/index.ts index 6a50c8f6..0a0549a9 100644 --- a/assets/js/hooks/index.ts +++ b/assets/js/hooks/index.ts @@ -10,7 +10,7 @@ import { export const Hooks = { outline: { mounted() { - const container: HTMLOListElement = this.el; + const container: HTMLDivElement = this.el.querySelector(".children"); container.addEventListener("focusin", focusin.bind(this)); container.addEventListener("focusout", focusout.bind(this)); diff --git a/assets/js/hooks/item.ts b/assets/js/hooks/item.ts index 8ef66ce4..38489e36 100644 --- a/assets/js/hooks/item.ts +++ b/assets/js/hooks/item.ts @@ -1,51 +1,97 @@ import { Node } from "./types"; +import { getNodeByItem } from "./node"; export function createItem({ uuid, content, parent_id, prev_id, dirty }: Node) { - const input = document.createElement("div"); - input.textContent = content || ""; - input.contentEditable = "true"; // firefox does not support "plaintext-only" + //
+ // + // + // + // + //
+ // Node Content + //
+ //
+ //
+ + const item = document.createElement("div"); + item.id = `outline-node-${uuid}`; + item.className = "my-1 bg-gray-100 data-[dirty=true]:bg-red-100"; + item.setAttribute("data-parent", parent_id || ""); + item.setAttribute("data-prev", prev_id || ""); - const ol = document.createElement("ol"); - ol.className = "list-disc"; + const link = document.createElement("a"); + link.className = "block float-left my-0.5 bg-gray-200 rounded-full"; + link.href = `#${uuid}`; + link.innerHTML = + ''; + item.appendChild(link); - const item = document.createElement("li"); - item.id = "outline-node-" + uuid; - item.className = "my-1 ml-4 data-[dirty=true]:bg-red-100"; + const contentWrap = document.createElement("div"); + contentWrap.className = "ml-5 bg-gray-300 content"; + contentWrap.contentEditable = "true"; + item.appendChild(contentWrap); - item.setAttribute("data-parent", parent_id || ""); - item.setAttribute("data-prev", prev_id || ""); + const span = document.createElement("span"); + span.className = "bg-gray-400 innerContent"; + span.textContent = content || " "; + contentWrap.appendChild(span); + + const childContainer = document.createElement("div"); + childContainer.className = "ml-5 children"; + item.appendChild(childContainer); + + setItemDirty(item, dirty); + + return item; +} + +export function changeItemContent({ uuid, content, dirty }: Node) { + const item = getItemById(uuid); + if (!item) return; - setItemDirty(item, dirty) + const newContent = content || ""; - item.appendChild(input); - item.appendChild(ol); + const span = item.querySelector(".innerContent") as HTMLSpanElement; + if (span.textContent != newContent) span.textContent = newContent; + + setItemDirty(item, dirty); return item; } -export function updateItem( - { uuid, content, parent_id, prev_id, dirty }: Node, - container: HTMLOListElement +export function moveItem( + { uuid, parent_id, prev_id, dirty }: Node, + container: HTMLDivElement, + force: boolean = false ) { const item = getItemById(uuid); if (!item) return; - const input = item.firstChild!; - input.textContent = content || ""; + const currentNode = getNodeByItem(item); + if ( + !force && + currentNode.parent_id == parent_id && + currentNode.prev_id == prev_id + ) + return; item.setAttribute("data-parent", parent_id || ""); item.setAttribute("data-prev", prev_id || ""); - setItemDirty(item, dirty) - const prevItem = getItemById(prev_id); const parentItem = getItemById(parent_id); if (prevItem) { prevItem.after(item); } else if (parentItem) { - parentItem.querySelector("ol")?.append(item); + parentItem.querySelector(".children")?.append(item); + } else { + container.prepend(item); } + + setItemDirty(item, dirty); + + return item; } export function deleteItem({ uuid }: Node) { @@ -55,30 +101,37 @@ export function deleteItem({ uuid }: Node) { item.parentNode!.removeChild(item); } +export function cleanItem({ uuid }: Node) { + const item = getItemById(uuid); + item && setItemDirty(item, false); + + return item; +} + export function getItemByNode({ uuid }: Node) { return getItemById(uuid); } -export function getItemById(uuid: string | undefined) { +function getItemById(uuid: string | undefined) { if (!uuid) return null; - return document.getElementById("outline-node-" + uuid) as HTMLLIElement; + return document.getElementById(`outline-node-${uuid}`) as HTMLDivElement; } -export function getItemByEvent(event: Event): HTMLLIElement { +export function getItemByEvent(event: Event): HTMLDivElement { const target = event.target; - const item = target.parentElement!; + const item = target.parentElement!; return item; } -export function focusItem(item: HTMLLIElement, toEnd: boolean = true) { - const input = item.firstChild as HTMLDivElement; - input.focus(); +export function focusItem(item: HTMLDivElement, toEnd: boolean = true) { + const contentWrap = item.querySelector(".innerContent") as HTMLDivElement; + contentWrap.focus(); if (toEnd) { const range = document.createRange(); - range.setStart(input, 0); + range.setStart(contentWrap, 0); range.collapse(true); const selection = window.getSelection(); @@ -87,14 +140,6 @@ export function focusItem(item: HTMLLIElement, toEnd: boolean = true) { } } -// export function indentNode(node: Node) { -// // const node = event.target.parentNode -// // const parentNode = event.target.parentNode.previousSibling -// } - -// export function outdentNode(node: Node) { -// } - -export function setItemDirty(item: HTMLLIElement, dirty: boolean) { +export function setItemDirty(item: HTMLDivElement, dirty: boolean) { item.setAttribute("data-dirty", dirty ? "true" : "false"); } diff --git a/assets/js/hooks/node.ts b/assets/js/hooks/node.ts index 41168f40..f19dcea5 100644 --- a/assets/js/hooks/node.ts +++ b/assets/js/hooks/node.ts @@ -1,19 +1,20 @@ -import { UUID, Node } from "./types" -import { getItemByEvent } from "./item" +import { UUID, Node } from "./types"; +import { getItemByEvent } from "./item"; export function getNodeByEvent(event: Event): Node { - const item = getItemByEvent(event) + const item = getItemByEvent(event); - return getNodeByItem(item) + 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 || "" +export function getNodeByItem(item: HTMLDivElement): Node { + const uuid = item.id.split("outline-node-")[1] as UUID; + const span = item.querySelector(".innerContent") as HTMLSpanElement; + const content = span.textContent || ""; - const parent_id = item.getAttribute("data-parent") as UUID || undefined - const prev_id = item.getAttribute("data-prev") as UUID || undefined + const parent_id = (item.getAttribute("data-parent") as UUID) || undefined; + const prev_id = (item.getAttribute("data-prev") as UUID) || undefined; + const dirty = item.getAttribute("data-dirty") == "true" ? true : false; - return { uuid, content, parent_id, prev_id } + return { uuid, content, parent_id, prev_id, dirty }; } diff --git a/lib/radiator_web/live/episode_live/index.html.heex b/lib/radiator_web/live/episode_live/index.html.heex index 0dd0099f..f365dc0c 100644 --- a/lib/radiator_web/live/episode_live/index.html.heex +++ b/lib/radiator_web/live/episode_live/index.html.heex @@ -13,7 +13,7 @@ :for={{episode, i} <- Enum.with_index(@episodes)} class={[episode.id == @selected_episode.id && "bg-[#f0f4f4]"]} > - <.link patch={~p"/admin/podcast/#{@show}/#{episode}"} class="flex gap-4 my-4"> + <.link navigate={~p"/admin/podcast/#{@show}/#{episode}"} class="flex gap-4 my-4"> <%= episode.number %> <%= episode.title %> @@ -44,6 +44,8 @@

<%= @selected_episode.number %> <%= @selected_episode.title %>

-
    +
    +
    +