Implement note-form

This commit is contained in:
Adrian Gruntkowski 2024-11-15 00:17:20 +01:00
parent 5706314451
commit 9e41a54d65
5 changed files with 203 additions and 15 deletions

View file

@ -30,6 +30,18 @@
<div id="content">
<h1>Jenot</h1>
<note-form id="new-note">
<div class="note">
<p class="content">
</p>
<div class="toolbar">
<button class="tasklist-mode">☑️</button>
<button class="note-mode">📑</button>
<button class="remove">🗑️</button>
</div>
</div>
</note-form>
<div class="note">
<editable-area>example note</editable-area>
</div>

View file

@ -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, "<br>") + "<br>";
}
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`
<li draggable="true"><task-list-item></task-list-itm></li>
`;
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`<editable-area></editable-area>`;
this.content.replaceChildren(note);
note.value = this.note.content;
} else {
const taskList = html`<task-list><ul></ul></task-list>`;
this.content.replaceChildren(taskList);
taskList.value = this.note.content;
}
}
}
customElements.define("note-form", NoteForm);

19
js/dom.js Normal file
View file

@ -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, "<br>") + "<br>";
return stringToHTML(content, true);
}

View file

@ -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);
});
}

View file

@ -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;
}