Implement basics

This commit is contained in:
Adrian Gruntkowski 2024-11-10 21:58:02 +01:00
commit b678eb89a4
10 changed files with 466 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

BIN
img/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

BIN
img/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 B

BIN
img/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
img/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

55
index.html Normal file
View file

@ -0,0 +1,55 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Jenot</title>
<link rel="stylesheet" href="/style.css" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="/img/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="/img/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="/img/favicon-16x16.png"
/>
<link rel="manifest" href="/site.webmanifest" />
</head>
<body>
<div id="content">
<h1>Jenot</h1>
<div class="note">
<editable-area>example note</editable-area>
</div>
<div class="note">
<task-list>
<ul>
<li><task-list-item checked>one</task-list-item></li>
<li><task-list-item>two</task-list-item></li>
</ul>
</task-list>
</div>
</div>
<template id="task-list-item">
<div class="handle"><button>||</button></div>
<div class="checkbox"><input type="checkbox" /></div>
<editable-area></editable-area>
<div class="remove"><button>X</button></div>
</template>
<script src="/js/jenot.js"></script>
</body>
</html>

260
js/jenot.js Normal file
View file

@ -0,0 +1,260 @@
// editable-area component
class EditableArea extends HTMLElement {
static observedAttributes = ["readonly"];
constructor() {
super();
}
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.inputElement.addEventListener("input", () => this.#sync());
this.#updateReadonly();
this.#sync();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "readonly") {
this.#updateReadonly();
}
}
get value() {
return this.inputElement.value;
}
set value(text) {
this.inputElement.value = text;
this.#sync();
}
focusStart() {
this.inputElement.focus();
this.inputElement.selectionEnd = 0;
}
focusEnd() {
this.inputElement.focus();
this.inputElement.selectionStart = this.inputElement.value.length;
}
cutTextAfterCursor() {
const cursorPos = this.inputElement.selectionStart;
const text = this.inputElement.value;
this.inputElement.value = text.slice(0, cursorPos);
this.#sync();
return text.slice(cursorPos);
}
append(text) {
const cursorStartPos = this.inputElement.selectionStart;
const cursorEndPos = this.inputElement.selectionEnd;
this.inputElement.value += text;
this.inputElement.selectionStart = cursorStartPos;
this.inputElement.selectionEnd = cursorEndPos;
this.#sync();
}
cursorAtStart() {
return this.inputElement.selectionEnd === 0;
}
cursorAtEnd() {
return this.inputElement.selectionStart === this.inputElement.value.length;
}
#updateReadonly() {
if (this.inputElement) {
const readonly = ["", "true"].includes(this.attributes.readonly?.value);
this.inputElement.disabled = readonly;
}
}
#sync() {
this.displayElement.innerHTML = render(this.inputElement.value);
this.inputElement.style.height = this.displayElement.scrollHeight + "px";
this.inputElement.style.width = this.displayElement.scrollWidth + "px";
}
}
function render(text) {
return text.replace(/(?:\r\n|\r|\n)/g, "<br>") + "<br>";
}
customElements.define("editable-area", EditableArea);
// task-list component
class TaskListItem extends HTMLElement {
static observedAttributes = ["checked"];
constructor() {
super();
}
connectedCallback() {
const text = this.textContent;
this.textContent = "";
const template = document.querySelector("#task-list-item").content;
Array.from(template.children).forEach((child) => {
this.appendChild(child.cloneNode(true));
});
this.handleElement = this.querySelector(".handle");
this.checkboxElement = this.querySelector(".checkbox input");
this.contentElement = this.querySelector("editable-area");
this.contentElement.value = text;
this.removeButton = this.querySelector(".remove button");
this.removeButton.addEventListener("click", (e) => {
this.dispatchEvent(new Event("removeTaskWithButton", { bubbles: true }));
e.preventDefault();
});
this.contentElement.addEventListener("keydown", (e) => {
if (e.key === "Backspace" && this.contentElement.cursorAtStart()) {
this.dispatchEvent(
new CustomEvent("removeTask", {
bubbles: true,
detail: this.contentElement.cutTextAfterCursor(),
}),
);
e.preventDefault();
} else if (e.key === "Enter") {
const textAfterCursor = this.contentElement.cutTextAfterCursor();
this.dispatchEvent(
new CustomEvent("addTask", {
bubbles: true,
detail: textAfterCursor,
}),
);
e.preventDefault();
} else if (e.key === "ArrowUp" && this.contentElement.cursorAtStart()) {
this.dispatchEvent(new Event("moveToPrevTask", { bubbles: true }));
e.preventDefault();
} else if (e.key === "ArrowDown" && this.contentElement.cursorAtEnd()) {
this.dispatchEvent(new Event("moveToNextTask", { bubbles: true }));
e.preventDefault();
}
});
this.#updateChecked();
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === "checked") {
this.#updateChecked();
}
}
append(text) {
this.contentElement.append(text);
}
get value() {
return {
content: this.contentElement.value,
checked: !!this.checkboxElement.checked,
};
}
focusStart() {
this.contentElement.focusStart();
}
focusEnd() {
this.contentElement.focusEnd();
}
#updateChecked() {
if (this.checkboxElement) {
const checked = ["", "true"].includes(this.attributes.checked?.value);
this.checkboxElement.checked = checked;
}
}
}
customElements.define("task-list-item", TaskListItem);
class TaskList extends HTMLElement {
constructor() {
super();
}
connectedCallback() {
this.addEventListener("removeTaskWithButton", (e) => {
const tasksCount = this.querySelectorAll("task-list-item").length;
if (tasksCount === 1) {
const newLI = document.createElement("li");
const newItem = document.createElement("task-list-item");
newLI.appendChild(newItem);
this.querySelector("ul").appendChild(newLI);
newItem.focusStart();
}
const currentLI = e.target.parentNode;
currentLI.remove();
});
this.addEventListener("removeTask", (e) => {
const textToAppend = e.detail || "";
const currentLI = e.target.parentNode;
const previousItem =
currentLI.previousElementSibling?.querySelector("task-list-item");
if (previousItem) {
previousItem.focusEnd();
previousItem.append(textToAppend);
currentLI.remove();
}
});
this.addEventListener("addTask", (e) => {
const text = e.detail || "";
const currentLI = e.target.parentNode;
const newLI = document.createElement("li");
const newItem = document.createElement("task-list-item");
newItem.textContent = text;
newLI.appendChild(newItem);
currentLI.after(newLI);
newItem.focusStart();
});
this.addEventListener("moveToNextTask", (e) => {
const currentLI = e.target.parentNode;
const nextItem =
currentLI.nextElementSibling?.querySelector("task-list-item");
if (nextItem) {
nextItem.focusStart();
}
});
this.addEventListener("moveToPrevTask", (e) => {
const currentLI = e.target.parentNode;
const prevItem =
currentLI.previousElementSibling?.querySelector("task-list-item");
if (prevItem) {
prevItem.focusEnd();
}
});
}
}
customElements.define("task-list", TaskList);

1
site.webmanifest Normal file
View file

@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/img/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/img/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}

150
style.css Normal file
View file

@ -0,0 +1,150 @@
/* CSS reset */
*,
*::before,
*::after {
box-sizing: border-box;
}
* {
margin: 0;
}
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
img,
picture,
video,
canvas,
svg {
display: block;
max-width: 100%;
}
input,
button,
textarea,
select {
font: inherit;
}
p,
h1,
h2,
h3,
h4,
h5,
h6 {
overflow-wrap: break-word;
}
p {
text-wrap: pretty;
}
h1,
h2,
h3,
h4,
h5,
h6 {
text-wrap: balance;
}
#root,
#__next {
isolation: isolate;
}
/* Editable area component */
editable-area {
display: block;
position: relative;
font-size: inherit;
font-family: inherit;
line-height: inherit;
text-indent: inherit;
letter-spacing: inherit;
width: 100%;
}
editable-area .display {
font-size: inherit;
font-family: inherit;
line-height: inherit;
text-indent: inherit;
letter-spacing: inherit;
padding: 0;
margin: 0;
border-width: 0;
}
editable-area textarea {
padding: 0;
position: absolute;
top: 0;
left: 0;
-webkit-text-fill-color: transparent;
-webkit-tap-highlight-color: rgba(0,0,0,0);
-webkit-tap-highlight-color: transparent;
background: transparent;
border: none;
resize: none;
font-size: inherit;
font-family: inherit;
line-height: inherit;
text-indent: inherit;
letter-spacing: inherit;
padding: 0;
margin: 0;
border-width: 0;
}
editable-area textarea:focus {
outline: none;
}
/* Task list */
task-list ul {
wdith: 100%;
list-style-type: none;
margin: 0;
padding: 0;
}
task-list-item {
display: flex;
gap: 6px;
align-items: baseline;
margin-bottom: 4px;
}
task-list-item .checkbox input {
width: 1.1em;
height: 1.1em;
}
/* Styles */
* {
font-family: Arial, Helvetica, sans-serif;
color: #222;
}
div#content {
width: 270px;
max-width: 1200px;
margin: 10px auto 0 auto;
}
.note {
max-width: 240px;
background: beige;
box-shadow: 2px 2px 4px 0px rgb(0 0 0 / 0.8);
padding: 4px 8px;
margin-bottom: 20px;
}