mirror of
https://github.com/zoldar/jenot.git
synced 2026-01-03 14:32:54 +00:00
Implement note-form
This commit is contained in:
parent
5706314451
commit
9e41a54d65
5 changed files with 203 additions and 15 deletions
12
index.html
12
index.html
|
|
@ -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>
|
||||
|
|
|
|||
159
js/components.js
159
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, "<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
19
js/dom.js
Normal 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);
|
||||
}
|
||||
12
js/jenot.js
12
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);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
16
style.css
16
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue