mirror of
https://github.com/zoldar/jenot.git
synced 2026-01-03 14:32:54 +00:00
Implement basics
This commit is contained in:
commit
b678eb89a4
10 changed files with 466 additions and 0 deletions
BIN
img/android-chrome-192x192.png
Normal file
BIN
img/android-chrome-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.6 KiB |
BIN
img/android-chrome-512x512.png
Normal file
BIN
img/android-chrome-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
img/apple-touch-icon.png
Normal file
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
BIN
img/favicon-16x16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 577 B |
BIN
img/favicon-32x32.png
Normal file
BIN
img/favicon-32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/favicon.ico
Normal file
BIN
img/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
55
index.html
Normal file
55
index.html
Normal 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
260
js/jenot.js
Normal 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
1
site.webmanifest
Normal 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
150
style.css
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue