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 ===...