Skip to content

Commit

Permalink
Merge pull request #1 from m-ld/m-ldify
Browse files Browse the repository at this point in the history
m-ld-ification
  • Loading branch information
gsvarovsky authored Jul 7, 2023
2 parents 60de76a + a10960b commit 9b5b351
Show file tree
Hide file tree
Showing 10 changed files with 1,980 additions and 178 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
node_modules
.DS_Store
.DS_Store
.idea
5 changes: 5 additions & 0 deletions css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ body {
text-rendering: optimizeLegibility;
}

.todoapp a {
all: unset;
cursor: pointer;
}

.new-todo,
.edit {
position: relative;
Expand Down
18 changes: 10 additions & 8 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@
<body>
<section class="todoapp">
<header class="header">
<h1>todos</h1>
<h1><a href=".">todos</a></h1>
<input
placeholder="What needs to be done?"
autofocus
class="new-todo"
data-todo="new"
disabled
/>
</header>
<!-- This section should be hidden by default and shown when there are todos -->
Expand All @@ -38,13 +39,13 @@ <h1>todos</h1>
<!-- Remove this if you don't implement routing -->
<ul class="filters" data-todo="filters">
<li>
<a class="selected" href="#/">All</a>
<a class="selected" href="#//">All</a>
</li>
<li>
<a href="#/active">Active</a>
<a href="#//active">Active</a>
</li>
<li>
<a href="#/completed">Completed</a>
<a href="#//completed">Completed</a>
</li>
</ul>
<!-- Hidden if no completed items are left ↓ -->
Expand All @@ -54,12 +55,13 @@ <h1>todos</h1>
</footer>
</section>
<footer class="info">
<p>Double-click to edit a todo</p>
<p>Created by <a href="https://twitter.com/1Marc">Marc Grabanski</a></p>
<p>Double-click to edit a todo | Click the header to create a new todo list</p>
<p>Created by <a href="https://twitter.com/1Marc">Marc Grabanski</a> |
Collaboration enabled with <a href="https://m-ld.org/">m-ld</a></p>
<p>
Project on GitHub:
<a href="https://github.com/1Marc/todomvc-vanillajs-2022"
>Vanilla JS TodoMVC 2022</a
<a href="https://github.com/m-ld/m-ld-todomvc-vanillajs"
>Vanilla JS TodoMVC with m-ld</a
>
</p>
</footer>
Expand Down
138 changes: 99 additions & 39 deletions js/app.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { delegate, getURLHash, insertHTML, replaceHTML } from "./helpers.js";
import { TodoStore } from "./store.js";

const Todos = new TodoStore("todo-modern-vanillajs");
import {delegate, getURLHash, insertHTML, replaceHTML} from "./helpers.js";
import {TodoStore} from "./store.js";
import {uuid} from 'https://edge.js.m-ld.org/ext/index.mjs';

const App = {
$: {
input: document.querySelector('[data-todo="new"]'),
toggleAll: document.querySelector('[data-todo="toggle-all"]'),
clear: document.querySelector('[data-todo="clear-completed"]'),
list: document.querySelector('[data-todo="list"]'),
filters: document.querySelectorAll(`[data-todo="filters"] a`),
showMain(show) {
document.querySelector('[data-todo="main"]').style.display = show ? "block" : "none";
},
Expand All @@ -18,9 +18,15 @@ const App = {
showClear(show) {
App.$.clear.style.display = show ? "block" : "none";
},
updateFilterHashes() {
App.$.filters.forEach((el) => {
const {filter} = getURLHash(el.getAttribute('href'));
el.setAttribute('href', `#/${App.todos.id}/${filter}`);
});
},
setActiveFilter(filter) {
document.querySelectorAll(`[data-todo="filters"] a`).forEach((el) => {
if (el.matches(`[href="#/${filter}"]`)) {
App.$.filters.forEach((el) => {
if (el.matches(`[href="#/${App.todos.id}/${filter}"]`)) {
el.classList.add("selected");
} else {
el.classList.remove("selected");
Expand All @@ -36,65 +42,108 @@ const App = {
`
);
},
getTodo(liOrId) {
let $li = liOrId instanceof Element ? liOrId :
document.querySelector(`[data-id="${liOrId}"]`);
return $li && {
$li,
get $input() {
return $li.querySelector('[data-todo="edit"]')
},
get $label() {
return $li.querySelector('[data-todo="label"]')
}
};
},
getEditing() {
const $li = document.querySelector('.editing');
if ($li != null) {
const editingId = $li.dataset.id;
const $input = App.$.getTodo($li).$input;
const selection = [$input.selectionStart, $input.selectionEnd];
return {
id: editingId,
restore() { return App.$.setEditing(editingId, ...selection); }
}
}
},
setEditing(liOrId, selectionStart, selectionEnd) {
let $todo = App.$.getTodo(liOrId);
if ($todo != null) {
$todo.$li.classList.add("editing");
$todo.$input.focus();
$todo.$input.setSelectionRange(selectionStart, selectionEnd);
return $todo;
}
}
},
init() {
Todos.addEventListener("save", App.render);
App.filter = getURLHash();
window.addEventListener("hashchange", () => {
App.filter = getURLHash();
App.render();
});
function onHashChange() {
let {documentId, filter} = getURLHash(document.location.hash);
const isNew = !documentId;
if (isNew) {
documentId = uuid();
history.pushState(null, null, `#/${documentId}/${filter}`);
}
App.filter = filter;
if (App.todos == null || App.todos.id !== documentId) {
App.todos?.close();
App.todos = new TodoStore(documentId, isNew);
App.$.updateFilterHashes();
App.todos.addEventListener("save", App.render);
App.todos.addEventListener("error", App.error);
} else {
App.$.setActiveFilter(App.filter);
App.render();
}
}
window.addEventListener("hashchange", onHashChange);
onHashChange();
App.$.input.addEventListener("keyup", (e) => {
if (e.key === "Enter" && e.target.value.length) {
Todos.add({
title: e.target.value,
completed: false,
id: "id_" + Date.now(),
});
App.todos.add({ title: e.target.value });
App.$.input.value = "";
}
});
App.$.toggleAll.addEventListener("click", (e) => {
Todos.toggleAll();
App.$.toggleAll.addEventListener("click", () => {
App.todos.toggleAll();
});
App.$.clear.addEventListener("click", (e) => {
Todos.clearCompleted();
App.$.clear.addEventListener("click", () => {
App.todos.clearCompleted();
});
App.bindTodoEvents();
App.render();
},
todoEvent(event, selector, handler) {
delegate(App.$.list, selector, event, (e) => {
let $el = e.target.closest("[data-id]");
handler(Todos.get($el.dataset.id), $el, e);
handler(App.todos.get($el.dataset.id), $el, e);
});
},
bindTodoEvents() {
App.todoEvent("click", '[data-todo="destroy"]', (todo) => Todos.remove(todo));
App.todoEvent("click", '[data-todo="toggle"]', (todo) => Todos.toggle(todo));
App.todoEvent("click", '[data-todo="destroy"]', (todo) => App.todos.remove(todo));
App.todoEvent("click", '[data-todo="toggle"]', (todo) => App.todos.toggle(todo));
App.todoEvent("dblclick", '[data-todo="label"]', (_, $li) => {
$li.classList.add("editing");
$li.querySelector('[data-todo="edit"]').focus();
App.$.setEditing($li);
});
App.todoEvent("keyup", '[data-todo="edit"]', (todo, $li, e) => {
let $input = $li.querySelector('[data-todo="edit"]');
let $input = App.$.getTodo($li).$input;
if (e.key === "Enter" && $input.value) {
$li.classList.remove("editing");
Todos.update({ ...todo, title: $input.value });
App.todos.update({ ...todo, title: $input.value });
}
if (e.key === "Escape") {
document.activeElement.blur();
}
});
App.todoEvent("focusout", '[data-todo="edit"]', (todo, $li, e) => {
App.todoEvent("focusout", '[data-todo="edit"]', (todo, $li) => {
if ($li.classList.contains("editing")) {
App.render();
}
});
},
createTodoItem(todo) {
const li = document.createElement("li");
li.dataset.id = todo.id;
li.dataset.id = todo['@id'];
if (todo.completed) {
li.classList.add("completed");
}
Expand All @@ -109,20 +158,31 @@ const App = {
<input class="edit" data-todo="edit">
`
);
li.querySelector('[data-todo="label"]').textContent = todo.title;
li.querySelector('[data-todo="edit"]').value = todo.title;
App.$.getTodo(li).$label.textContent = todo.title;
App.$.getTodo(li).$input.value = todo.title;
return li;
},
render() {
const count = Todos.all().length;
render(saveEvent) {
const editing = saveEvent && App.$.getEditing();
const count = App.todos.all().length;
App.$.setActiveFilter(App.filter);
App.$.list.replaceChildren(...Todos.all(App.filter).map((todo) => App.createTodoItem(todo)));
App.$.list.replaceChildren(...App.todos.all(App.filter).map((todo) => App.createTodoItem(todo)));
App.$.showMain(count);
App.$.showFooter(count);
App.$.showClear(Todos.hasCompleted());
App.$.toggleAll.checked = Todos.isAllCompleted();
App.$.displayCount(Todos.all("active").length);
App.$.showClear(App.todos.hasCompleted());
App.$.toggleAll.checked = App.todos.isAllCompleted();
App.$.displayCount(App.todos.all("active").length);
App.$.input.disabled = false;
editing?.restore(); // TODO: What if no longer present (filtered out or removed)
},
error(errEvent) {
replaceHTML(document.querySelector('.todoapp'), `
<header class="header">
<h1><a href=".">todos</a></h1>
<p>${errEvent.error}</p>
</header>
`);
}
};

App.init();
7 changes: 5 additions & 2 deletions js/helpers.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
export const getURLHash = () => document.location.hash.replace(/^#\//, "");
export const getURLHash = (hash) => {
const [documentId, filter] = hash.split('/').slice(1); // Remove hash symbol
return {documentId: documentId ?? '', filter: filter ?? ''};
};

export const delegate = (el, selector, event, handler) => {
el.addEventListener(event, (e) => {
Expand All @@ -11,4 +14,4 @@ export const insertHTML = (el, html) => el.insertAdjacentHTML("afterbegin", html
export const replaceHTML = (el, html) => {
el.replaceChildren();
insertHTML(el, html);
};
};
Loading

0 comments on commit 9b5b351

Please sign in to comment.