mirror of
https://github.com/zoldar/jenot.git
synced 2026-01-05 07:02:55 +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">
|
<div id="content">
|
||||||
<h1>Jenot</h1>
|
<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">
|
<div class="note">
|
||||||
<editable-area>example note</editable-area>
|
<editable-area>example note</editable-area>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
159
js/components.js
159
js/components.js
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { renderText, html } from "./dom.js";
|
||||||
|
|
||||||
// editable-area component
|
// editable-area component
|
||||||
|
|
||||||
|
|
@ -10,15 +11,16 @@ class EditableArea extends HTMLElement {
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
const text = this.textContent;
|
const text = this.textContent;
|
||||||
this.textContent = "";
|
|
||||||
this.displayElement = document.createElement("p", { class: "display" });
|
this.displayElement = document.createElement("p", { class: "display" });
|
||||||
this.inputElement = document.createElement("textarea");
|
this.inputElement = document.createElement("textarea");
|
||||||
this.inputElement.value = text;
|
this.inputElement.value = text;
|
||||||
|
|
||||||
this.appendChild(this.displayElement);
|
this.replaceChildren(this.displayElement, this.inputElement);
|
||||||
this.appendChild(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.#updateReadonly();
|
||||||
this.#sync();
|
this.#sync();
|
||||||
|
|
@ -83,19 +85,15 @@ class EditableArea extends HTMLElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
#sync() {
|
#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.height = this.displayElement.scrollHeight + "px";
|
||||||
this.inputElement.style.width = this.displayElement.scrollWidth + "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);
|
customElements.define("editable-area", EditableArea);
|
||||||
|
|
||||||
// task-list component
|
// task-list-item component
|
||||||
|
|
||||||
class TaskListItem extends HTMLElement {
|
class TaskListItem extends HTMLElement {
|
||||||
static observedAttributes = ["checked"];
|
static observedAttributes = ["checked"];
|
||||||
|
|
@ -121,6 +119,8 @@ class TaskListItem extends HTMLElement {
|
||||||
this.contentElement.value = text;
|
this.contentElement.value = text;
|
||||||
this.removeButton = this.querySelector(".remove button");
|
this.removeButton = this.querySelector(".remove button");
|
||||||
|
|
||||||
|
this.handleElement.addEventListener("click", (e) => e.preventDefault());
|
||||||
|
|
||||||
this.removeButton.addEventListener("click", (e) => {
|
this.removeButton.addEventListener("click", (e) => {
|
||||||
this.dispatchEvent(new Event("removeTaskWithButton", { bubbles: true }));
|
this.dispatchEvent(new Event("removeTaskWithButton", { bubbles: true }));
|
||||||
e.preventDefault();
|
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
|
// drag and drop events
|
||||||
|
|
||||||
this.parentNode.addEventListener("dragstart", (e) => {
|
this.parentNode.addEventListener("dragstart", (e) => {
|
||||||
|
|
@ -161,6 +165,7 @@ class TaskListItem extends HTMLElement {
|
||||||
|
|
||||||
this.parentNode.addEventListener("dragend", () => {
|
this.parentNode.addEventListener("dragend", () => {
|
||||||
this.parentNode.classList.remove("dragging");
|
this.parentNode.classList.remove("dragging");
|
||||||
|
this.dispatchEvent(new Event("contentChange", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.parentNode.addEventListener("touchstart", (e) => {
|
this.parentNode.addEventListener("touchstart", (e) => {
|
||||||
|
|
@ -226,6 +231,7 @@ class TaskListItem extends HTMLElement {
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#updateChecked();
|
this.#updateChecked();
|
||||||
|
this.dispatchEvent(new Event("contentChange", { bubbles: true }));
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
|
|
@ -250,6 +256,11 @@ class TaskListItem extends HTMLElement {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
set value(item) {
|
||||||
|
this.checkboxElement.checked = item.checked;
|
||||||
|
this.contentElement.value = item.content;
|
||||||
|
}
|
||||||
|
|
||||||
focusStart() {
|
focusStart() {
|
||||||
this.contentElement.focusStart();
|
this.contentElement.focusStart();
|
||||||
}
|
}
|
||||||
|
|
@ -268,6 +279,8 @@ class TaskListItem extends HTMLElement {
|
||||||
|
|
||||||
customElements.define("task-list-item", TaskListItem);
|
customElements.define("task-list-item", TaskListItem);
|
||||||
|
|
||||||
|
// task-list component
|
||||||
|
|
||||||
class TaskList extends HTMLElement {
|
class TaskList extends HTMLElement {
|
||||||
dragPlaceholder = null;
|
dragPlaceholder = null;
|
||||||
dragActiveElement = null;
|
dragActiveElement = null;
|
||||||
|
|
@ -293,6 +306,8 @@ class TaskList extends HTMLElement {
|
||||||
|
|
||||||
const currentLI = e.target.parentNode;
|
const currentLI = e.target.parentNode;
|
||||||
currentLI.remove();
|
currentLI.remove();
|
||||||
|
|
||||||
|
this.dispatchEvent(new Event("contentChange", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addEventListener("removeTask", (e) => {
|
this.addEventListener("removeTask", (e) => {
|
||||||
|
|
@ -305,6 +320,7 @@ class TaskList extends HTMLElement {
|
||||||
previousItem.focusEnd();
|
previousItem.focusEnd();
|
||||||
previousItem.append(textToAppend);
|
previousItem.append(textToAppend);
|
||||||
currentLI.remove();
|
currentLI.remove();
|
||||||
|
this.dispatchEvent(new Event("contentChange", { bubbles: true }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -319,6 +335,7 @@ class TaskList extends HTMLElement {
|
||||||
newLI.appendChild(newItem);
|
newLI.appendChild(newItem);
|
||||||
currentLI.after(newLI);
|
currentLI.after(newLI);
|
||||||
newItem.focusStart();
|
newItem.focusStart();
|
||||||
|
this.dispatchEvent(new Event("contentChange", { bubbles: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
this.addEventListener("moveToNextTask", (e) => {
|
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) {
|
function getDragAfterElement(container, y) {
|
||||||
|
|
@ -378,3 +415,105 @@ function getDragAfterElement(container, y) {
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("task-list", TaskList);
|
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 { NoteStore } from "./store.js";
|
||||||
|
import "./components.js"
|
||||||
|
|
||||||
const Notes = new NoteStore("jenot-app");
|
const Notes = new NoteStore("jenot-app");
|
||||||
|
|
||||||
|
const newNote = document.querySelector("#new-note");
|
||||||
|
|
||||||
Notes.addEventListener("save", render.bind(this));
|
Notes.addEventListener("save", render.bind(this));
|
||||||
|
|
||||||
Notes.reset();
|
Notes.reset();
|
||||||
|
|
@ -26,7 +29,6 @@ Notes.saveStorage();
|
||||||
function render() {
|
function render() {
|
||||||
const notes = Notes.all();
|
const notes = Notes.all();
|
||||||
const notesContainer = document.querySelector("#notes");
|
const notesContainer = document.querySelector("#notes");
|
||||||
notesContainer.textContent = "";
|
|
||||||
|
|
||||||
notes.forEach((note) => {
|
notes.forEach((note) => {
|
||||||
const container = document.createElement("div");
|
const container = document.createElement("div");
|
||||||
|
|
@ -35,7 +37,7 @@ function render() {
|
||||||
container.classList.add("readonly");
|
container.classList.add("readonly");
|
||||||
|
|
||||||
if (note.type === "note") {
|
if (note.type === "note") {
|
||||||
container.innerHTML = renderText(note.content);
|
container.replaceChildren(...renderText(note.content));
|
||||||
} else if (note.type === "tasklist") {
|
} else if (note.type === "tasklist") {
|
||||||
const list = document.createElement("ul");
|
const list = document.createElement("ul");
|
||||||
|
|
||||||
|
|
@ -45,7 +47,7 @@ function render() {
|
||||||
check.textContent = task.checked ? "☑" : "☐";
|
check.textContent = task.checked ? "☑" : "☐";
|
||||||
item.appendChild(check);
|
item.appendChild(check);
|
||||||
const itemContent = document.createElement("p");
|
const itemContent = document.createElement("p");
|
||||||
itemContent.innerHTML = renderText(task.content);
|
itemContent.replaceChildren(...renderText(task.content));
|
||||||
item.appendChild(itemContent);
|
item.appendChild(itemContent);
|
||||||
list.append(item);
|
list.append(item);
|
||||||
});
|
});
|
||||||
|
|
@ -53,6 +55,6 @@ function render() {
|
||||||
container.appendChild(list);
|
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;
|
touch-action: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Note form */
|
||||||
|
|
||||||
|
note-form .content {
|
||||||
|
min-height: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
note-form .note {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Styles */
|
/* Styles */
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
|
@ -181,3 +193,7 @@ div#content {
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue