From 9e41a54d652cb958261694373ca96e11e0b761ac Mon Sep 17 00:00:00 2001 From: Adrian Gruntkowski Date: Fri, 15 Nov 2024 00:17:20 +0100 Subject: [PATCH] Implement note-form --- index.html | 12 ++++ js/components.js | 159 ++++++++++++++++++++++++++++++++++++++++++++--- js/dom.js | 19 ++++++ js/jenot.js | 12 ++-- style.css | 16 +++++ 5 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 js/dom.js diff --git a/index.html b/index.html index 92fb1b4..37ad826 100644 --- a/index.html +++ b/index.html @@ -30,6 +30,18 @@

Jenot

+ +
+

+

+
+ + + +
+
+
+
example note
diff --git a/js/components.js b/js/components.js index 041fae4..bd68660 100644 --- a/js/components.js +++ b/js/components.js @@ -1,3 +1,4 @@ +import { renderText, html } from "./dom.js"; // editable-area component @@ -10,15 +11,16 @@ class EditableArea extends HTMLElement { connectedCallback() { const text = this.textContent; - this.textContent = ""; this.displayElement = document.createElement("p", { class: "display" }); this.inputElement = document.createElement("textarea"); this.inputElement.value = text; - this.appendChild(this.displayElement); - this.appendChild(this.inputElement); + this.replaceChildren(this.displayElement, this.inputElement); - this.inputElement.addEventListener("input", () => this.#sync()); + this.inputElement.addEventListener("input", (e) => { + this.dispatchEvent(new Event("contentChange", { bubbles: true })); + this.#sync(); + }); this.#updateReadonly(); this.#sync(); @@ -83,19 +85,15 @@ class EditableArea extends HTMLElement { } #sync() { - this.displayElement.innerHTML = renderText(this.inputElement.value); + this.displayElement.replaceChildren(...renderText(this.inputElement.value)); this.inputElement.style.height = this.displayElement.scrollHeight + "px"; this.inputElement.style.width = this.displayElement.scrollWidth + "px"; } } -export function renderText(text) { - return text.replace(/(?:\r\n|\r|\n)/g, "
") + "
"; -} - customElements.define("editable-area", EditableArea); -// task-list component +// task-list-item component class TaskListItem extends HTMLElement { static observedAttributes = ["checked"]; @@ -121,6 +119,8 @@ class TaskListItem extends HTMLElement { this.contentElement.value = text; this.removeButton = this.querySelector(".remove button"); + this.handleElement.addEventListener("click", (e) => e.preventDefault()); + this.removeButton.addEventListener("click", (e) => { this.dispatchEvent(new Event("removeTaskWithButton", { bubbles: true })); e.preventDefault(); @@ -153,6 +153,10 @@ class TaskListItem extends HTMLElement { } }); + this.checkboxElement.addEventListener("change", () => + this.dispatchEvent(new Event("contentChange", { bubbles: true })), + ); + // drag and drop events this.parentNode.addEventListener("dragstart", (e) => { @@ -161,6 +165,7 @@ class TaskListItem extends HTMLElement { this.parentNode.addEventListener("dragend", () => { this.parentNode.classList.remove("dragging"); + this.dispatchEvent(new Event("contentChange", { bubbles: true })); }); this.parentNode.addEventListener("touchstart", (e) => { @@ -226,6 +231,7 @@ class TaskListItem extends HTMLElement { }); this.#updateChecked(); + this.dispatchEvent(new Event("contentChange", { bubbles: true })); } disconnectedCallback() { @@ -250,6 +256,11 @@ class TaskListItem extends HTMLElement { }; } + set value(item) { + this.checkboxElement.checked = item.checked; + this.contentElement.value = item.content; + } + focusStart() { this.contentElement.focusStart(); } @@ -268,6 +279,8 @@ class TaskListItem extends HTMLElement { customElements.define("task-list-item", TaskListItem); +// task-list component + class TaskList extends HTMLElement { dragPlaceholder = null; dragActiveElement = null; @@ -293,6 +306,8 @@ class TaskList extends HTMLElement { const currentLI = e.target.parentNode; currentLI.remove(); + + this.dispatchEvent(new Event("contentChange", { bubbles: true })); }); this.addEventListener("removeTask", (e) => { @@ -305,6 +320,7 @@ class TaskList extends HTMLElement { previousItem.focusEnd(); previousItem.append(textToAppend); currentLI.remove(); + this.dispatchEvent(new Event("contentChange", { bubbles: true })); } }); @@ -319,6 +335,7 @@ class TaskList extends HTMLElement { newLI.appendChild(newItem); currentLI.after(newLI); newItem.focusStart(); + this.dispatchEvent(new Event("contentChange", { bubbles: true })); }); this.addEventListener("moveToNextTask", (e) => { @@ -355,6 +372,26 @@ class TaskList extends HTMLElement { } }); } + + get value() { + return Array.from(this.querySelectorAll("task-list-item")).map( + (item) => item.value, + ); + } + + set value(tasks) { + this.listElement.replaceChildren(); + + tasks.forEach((task) => { + const item = html` +
  • + `; + this.listElement.appendChild(item); + item.querySelector("task-list-item").value = task; + }); + + this.dispatchEvent(new Event("contentChange", { bubbles: true })); + } } function getDragAfterElement(container, y) { @@ -378,3 +415,105 @@ function getDragAfterElement(container, y) { } customElements.define("task-list", TaskList); + +// note-form component + +function convertToTaskList(note) { + return note.split("\n").map((line) => { + return { checked: false, content: line }; + }); +} + +function convertToNote(tasks) { + const lines = tasks.map((task) => task.content); + return lines.join("\n"); +} + +const contentMap = { + note_string: (s) => s, + note_object: convertToNote, + tasklist_object: (s) => s, + tasklist_string: convertToTaskList, +}; + +class NoteForm extends HTMLElement { + constructor() { + super(); + + this.note = { + type: "note", + content: "", + }; + } + + connectedCallback() { + this.content = this.querySelector(".content"); + this.tasklistModeButton = this.querySelector(".tasklist-mode"); + this.noteModeButton = this.querySelector(".note-mode"); + this.removeButton = this.querySelector(".remove"); + + this.tasklistModeButton.addEventListener("click", (e) => { + e.preventDefault(); + this.note.type = "tasklist"; + this.#setContent(); + this.#updateUI(); + }); + + this.noteModeButton.addEventListener("click", (e) => { + e.preventDefault(); + this.note.type = "note"; + this.#setContent(); + this.#updateUI(); + }); + + this.removeButton.addEventListener("click", (e) => { + e.preventDefault(); + this.#reset(); + }); + + this.addEventListener("contentChange", () => { + this.note.content = this.content.firstChild.value; + }); + + this.#updateUI(); + this.#setContent(); + } + + #reset() { + this.note = { + type: "note", + content: "", + }; + + this.#updateUI(); + this.#setContent(); + } + + #updateUI() { + if (this.note.type === "note") { + this.tasklistModeButton.classList.remove("hidden"); + this.noteModeButton.classList.add("hidden"); + } else { + this.tasklistModeButton.classList.add("hidden"); + this.noteModeButton.classList.remove("hidden"); + } + } + + #setContent() { + const contentFun = + contentMap[`${this.note.type}_${typeof this.note.content}`]; + this.note.content = contentFun(this.note.content); + + if (this.note.type === "note") { + const note = html``; + this.content.replaceChildren(note); + note.value = this.note.content; + } else { + const taskList = html`
      `; + this.content.replaceChildren(taskList); + taskList.value = this.note.content; + } + } +} + +customElements.define("note-form", NoteForm); diff --git a/js/dom.js b/js/dom.js new file mode 100644 index 0000000..64fc95c --- /dev/null +++ b/js/dom.js @@ -0,0 +1,19 @@ +export function stringToHTML(str, allChildren) { + var parser = new DOMParser(); + var doc = parser.parseFromString(str, "text/html"); + + if (allChildren) { + return Array.from(doc.body.childNodes); + } else { + return doc.body.firstChild; + } +} + +export function html(strings, ...values) { + return stringToHTML(String.raw({ raw: strings })); +} + +export function renderText(text) { + const content = text.replace(/(?:\r\n|\r|\n)/g, "
      ") + "
      "; + return stringToHTML(content, true); +} diff --git a/js/jenot.js b/js/jenot.js index b1c5ce3..d5ca8fe 100644 --- a/js/jenot.js +++ b/js/jenot.js @@ -1,8 +1,11 @@ -import { renderText } from "./components.js"; +import { renderText } from "./dom.js"; import { NoteStore } from "./store.js"; +import "./components.js" const Notes = new NoteStore("jenot-app"); +const newNote = document.querySelector("#new-note"); + Notes.addEventListener("save", render.bind(this)); Notes.reset(); @@ -26,7 +29,6 @@ Notes.saveStorage(); function render() { const notes = Notes.all(); const notesContainer = document.querySelector("#notes"); - notesContainer.textContent = ""; notes.forEach((note) => { const container = document.createElement("div"); @@ -35,7 +37,7 @@ function render() { container.classList.add("readonly"); if (note.type === "note") { - container.innerHTML = renderText(note.content); + container.replaceChildren(...renderText(note.content)); } else if (note.type === "tasklist") { const list = document.createElement("ul"); @@ -45,7 +47,7 @@ function render() { check.textContent = task.checked ? "☑" : "☐"; item.appendChild(check); const itemContent = document.createElement("p"); - itemContent.innerHTML = renderText(task.content); + itemContent.replaceChildren(...renderText(task.content)); item.appendChild(itemContent); list.append(item); }); @@ -53,6 +55,6 @@ function render() { container.appendChild(list); } - notesContainer.appendChild(container); + notesContainer.replaceChildren(container); }); } diff --git a/style.css b/style.css index aec4c26..8d91f6d 100644 --- a/style.css +++ b/style.css @@ -147,6 +147,18 @@ task-list ul { touch-action: none; } +/* Note form */ + +note-form .content { + min-height: 2.5em; +} + +note-form .note { + display: flex; + flex-direction: column; + gap: 8px; +} + /* Styles */ * { @@ -181,3 +193,7 @@ div#content { align-items: baseline; margin-bottom: 4px; } + +.hidden { + display: none; +}