commit b678eb89a4ab43b84fc06ec99b9fa7c29a942097 Author: Adrian Gruntkowski Date: Sun Nov 10 21:58:02 2024 +0100 Implement basics diff --git a/img/android-chrome-192x192.png b/img/android-chrome-192x192.png new file mode 100644 index 0000000..be0e061 Binary files /dev/null and b/img/android-chrome-192x192.png differ diff --git a/img/android-chrome-512x512.png b/img/android-chrome-512x512.png new file mode 100644 index 0000000..9484a39 Binary files /dev/null and b/img/android-chrome-512x512.png differ diff --git a/img/apple-touch-icon.png b/img/apple-touch-icon.png new file mode 100644 index 0000000..c4cd45a Binary files /dev/null and b/img/apple-touch-icon.png differ diff --git a/img/favicon-16x16.png b/img/favicon-16x16.png new file mode 100644 index 0000000..c7bb516 Binary files /dev/null and b/img/favicon-16x16.png differ diff --git a/img/favicon-32x32.png b/img/favicon-32x32.png new file mode 100644 index 0000000..d2397d4 Binary files /dev/null and b/img/favicon-32x32.png differ diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000..da4c60d Binary files /dev/null and b/img/favicon.ico differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..0987eb8 --- /dev/null +++ b/index.html @@ -0,0 +1,55 @@ + + + + + + Jenot + + + + + + + + +
+

Jenot

+ +
+ example note +
+ +
+ +
    +
  • one
  • +
  • two
  • +
+
+
+
+ + + + + + diff --git a/js/jenot.js b/js/jenot.js new file mode 100644 index 0000000..deaa16b --- /dev/null +++ b/js/jenot.js @@ -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, "
") + "
"; +} + +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); diff --git a/site.webmanifest b/site.webmanifest new file mode 100644 index 0000000..8efee0f --- /dev/null +++ b/site.webmanifest @@ -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"} diff --git a/style.css b/style.css new file mode 100644 index 0000000..175a0b0 --- /dev/null +++ b/style.css @@ -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; +}