DOM-powered Vim-style kanban
by mcsf
HTML
<h1>Kanban<em>-ish</em></h1>
<div class="tasks">
<ul aria-label="Today">
<li>One</li>
<li>Two</li>
<li>Three</li>
</ul>
<ul aria-label="Tomorrow">
<li>Four</li>
<li>Five</li>
<li>Six</li>
</ul>
</div>
<aside aria-label="Help">
<p>
Use Vim-style keys to manage a kanban.
</p>
<p>
Two focus modes: cell and column. Switch to <em>column</em> with <kbd>V</kbd>.
</p>
<p>
Move: <kbd>j</kbd>, <kbd>K</kbd>, <kbd>l</kbd>, <kbd>H</kbd>, <kbd>g</kbd>, <kbd>G</kbd>
<br>
Insert and edit: <kbd>o</kbd>, <kbd>O</kbd>, <kbd>c</kbd>
<br>
Copy and paste: <kbd>d</kbd>, <kbd>y</kbd>, <kbd>p</kbd>, <kbd>P</kbd>
<br>
Save to a base-64 hash: <kbd>w</kbd>
</p>
</aside>
CSS
.tasks {
display: flex;
flex-flow: row;
}
.tasks ul:focus {
outline: solid;
}
.tasks ul::before {
content: attr(aria-label);
font-weight: bold;
}
.tasks li:focus {
text-decoration: underline;
}
aside {
border-left: 10px solid #ffde05;
}
aside p {
margin: 0.5em 1em;
}
aside kbd {
border-bottom: 1px dashed;
}
JavaScript
location.hash = 'CiAgPHVsIGFyaWEtbGFiZWw9IkRvbmUiIHRhYmluZGV4PSIwIj4KICAgIDxsaT5Eb25lICMxPC9saT4KICAgIDxsaT5Eb25lICMyPC9saT4KICAgIDxsaT5Eb25lICMzPC9saT4KICA8L3VsPgogIDx1bCB0YWJpbmRleD0iMCIgYXJpYS1sYWJlbD0iSW4gcmV2aWV3Ij48bGk+SW4gcmV2aWV3ICMxPC9saT48L3VsPjx1bCBhcmlhLWxhYmVsPSJEb2luZyIgdGFiaW5kZXg9IjAiPgogICAgPGxpPkRvaW5nICM0PC9saT4KICAgIDxsaT5Eb2luZyAjNTwvbGk+CiAgICA8bGk+RG9pbmcgIzY8L2xpPgogIDwvdWw+Cjx1bCB0YWJpbmRleD0iMCIgYXJpYS1sYWJlbD0iRG9uZSI+PGxpPkRvbmUgIzE8L2xpPjwvdWw+'
document.querySelector('.tasks').innerHTML = atob(location.hash.substr(1))
Array.from(document.querySelectorAll('.tasks ul, .tasks li'))
.forEach(li => li.setAttribute('tabIndex', 0))
document.querySelector('.tasks li').focus()
const liTrash = []
const ulTrash = []
document.addEventListener('keydown', event => {
if (false !== commonHandler(event)) return
switch (event.target.nodeName) {
case 'LI': return liHandler(event)
case 'UL': return ulHandler(event)
}
})
adj = (el, reverse) => el[
(reverse ? 'previous' : 'next') + 'ElementSibling']
movAdj = (el, reverse) =>
el.parentNode.insertBefore(
reverse ? el : adj(el),
reverse ? adj(el, true) : el)
insAdj = (el, base, reverse) =>
base.parentNode.insertBefore(el,
reverse ? base : adj(base))
read = el => el.nodeName === 'LI' ?
el.innerText : el.getAttribute('aria-label')
write = (el, text) => el.nodeName === 'LI' ?
(el.innerText = text) : el.setAttribute('aria-label', text)
trash = el => el.nodeName === 'LI' ? liTrash : ulTrash
makeAlike = (el, text = '(untitled)') => {
const newEl = document.createElement(el.nodeName)
newEl.setAttribute('tabIndex', 0)
if (el.nodeName === 'UL')
newEl.appendChild(makeAlike(document.createElement('li')))
write(newEl, text)
return newEl
}
function commonHandler(event) {
if (event.target.isContentEditable) return false
const {key, target} = event
switch (key) {
case 'k': //upKey(target):
case 'j': //downKey(target):
adj(target, key ===...