Vanilla HTML+JS to-do list
by mcsf
HTML
<meta charset=utf8>
<link rel=stylesheet href=style.css>
<script defer src=script.js></script>
<dialog id=help onclick="help.close()">
<ul>
<li class=row><span>Add new task</span><kbd>Enter</kbd></li>
<li class=row><span>Toggle status</span><kbd>Meta + Enter</kbd></li>
<li class=row><span>Delete task, if empty</span><kbd>Backspace</kbd></li>
<li class=row><span>Move through tasks</span><span><kbd>↑</kbd> <kbd>↓</kbd></span></li>
</ul>
</dialog>
<button onclick="help.showModal()">Show keyboard shortcuts </button>
<form id=todos>
<ul>
<li draggable=true>
<input name="1:done" type=checkbox>
<input name="1:text" type=text value="Cross an item off">
</li>
<li draggable=true>
<input name="2:done" type=checkbox>
<input name="2:text" type=text value="Drag it around">
</li>
<li draggable=true>
<input name="3:done" type=checkbox>
<input name="3:text" type=text placeholder="Add a new task…">
</li>
</ul>
</form>
<template id=liTemplate>
<li draggable=true>
<input name="0:done" type=checkbox>
<input name="0:text" type=text placeholder="Add a new task…">
</li>
</template>
<pre id=out></pre>
CSS
@media (prefers-color-scheme: dark) {
body {
color: #f8f8f2;
background: #282a36;
}
}
ul {
padding-inline: 0;
}
li {
list-style: none;
}
kbd {
border: 1px solid #444;
border-radius: 3px;
padding-inline: 3px;
}
.row {
display: flex;
justify-content: space-between;
gap: 3rem;
margin-block: 0.5rem;
}
.transparent {
opacity: 0.4;
}
input[type="text"] {
border: none;
background: transparent;
color: inherit;
}
dialog:modal {
border-radius: 3px;
}
JavaScript
// LI element of a task as it is being dragged.
let dragTarget;
// Infer ID from DOM. Used to identify new tasks.
let lastId = Math.max.apply(null,
Array.from(document.querySelectorAll('[name]'))
.map(el => +el.name.split(':')[0]));
todos.addEventListener('input', (e) => display(e.currentTarget));
todos.addEventListener('submit', (e) => e.preventDefault());
todos.addEventListener('keydown', handleKeys);
function handleKeys(e) {
if (e.target.tagName !== 'INPUT' || e.target.type !== 'text') return;
switch (e.key) {
case 'Enter':
e.preventDefault();
if (e.metaKey) {
e.target.previousElementSibling.click();
} else {
const id = ++lastId;
const li = liTemplate.content
.firstElementChild.cloneNode(true);
for (const input of li.querySelectorAll('[name]')) {
input.name = input.name.replace('0', id);
}
e.target.closest('li').after(li);
li.lastElementChild.focus();
}
break;
case 'Backspace':
if (
e.target.value.length === 0
&& e.target.closest('ul').children.length > 1
) {
e.preventDefault();
const li = e.target.closest('li');
const prev = li.previousElementSibling;
li.remove();
prev.querySelector('[type="text"]').focus();
}
break;
case 'ArrowDown':
e.target.closest('li').nextElementSibling
?.querySelector('[type="text"]').focus();
break;
case 'ArrowUp':
e.target.closest('li').previousElementSibling
?.querySelector('[type="text"]').focus();
break;
}
}
todos.addEventListener('dragstart', (e) => {
dragTarget = e.target;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => dragTarget.classList.add('transparent'), 0);
});
todos.addEventListener('dragend', (e) => {
e.dataTransfer.effectAllowed = 'move';
e.preventDefault();
dragTarget.classList.remove('transparent');
dragTarget = null;
display(todos);
});
todos.addEventListener('dragover', (e) => {
e.dataTransfer.dropEffect = 'move';
const...