2017-03-21 03:00:45 +01:00
|
|
|
var mutate = require('xtend/mutable')
|
|
|
|
|
var expose = require('choo-expose')
|
|
|
|
|
var css = require('sheetify')
|
|
|
|
|
var html = require('bel')
|
|
|
|
|
var choo = require('../')
|
|
|
|
|
|
|
|
|
|
css('todomvc-common/base.css')
|
|
|
|
|
css('todomvc-app-css/index.css')
|
|
|
|
|
|
|
|
|
|
// we export this so tests can run
|
|
|
|
|
if (module.parent) {
|
|
|
|
|
exports.todoStore = todoStore
|
|
|
|
|
} else {
|
|
|
|
|
var app = choo()
|
|
|
|
|
|
|
|
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
|
|
|
var persist = require('choo-persist')
|
|
|
|
|
var logger = require('choo-log')
|
|
|
|
|
app.use(persist())
|
|
|
|
|
app.use(logger())
|
|
|
|
|
}
|
|
|
|
|
app.use(expose())
|
|
|
|
|
app.use(todoStore)
|
|
|
|
|
|
|
|
|
|
app.route('/', mainView)
|
|
|
|
|
app.route('#active', mainView)
|
|
|
|
|
app.route('#completed', mainView)
|
|
|
|
|
app.mount('body')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function mainView (state, emit) {
|
|
|
|
|
emit('log:debug', 'Rendering main view')
|
|
|
|
|
return html`
|
|
|
|
|
<body>
|
|
|
|
|
<section class="todoapp">
|
|
|
|
|
${Header(state, emit)}
|
|
|
|
|
${TodoList(state, emit)}
|
|
|
|
|
${Footer(state, emit)}
|
|
|
|
|
</section>
|
|
|
|
|
<footer class="info">
|
|
|
|
|
<p>Double-click to edit a todo</p>
|
|
|
|
|
<p>choo by <a href="https://yoshuawuyts.com/">Yoshua Wuyts</a></p>
|
|
|
|
|
<p>Created by <a href="http://shuheikagawa.com">Shuhei Kagawa</a></p>
|
|
|
|
|
</footer>
|
|
|
|
|
</body>
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function todoStore (state, emitter) {
|
|
|
|
|
if (!state.todos) {
|
|
|
|
|
state.todos = {}
|
|
|
|
|
|
|
|
|
|
state.todos.active = []
|
|
|
|
|
state.todos.done = []
|
|
|
|
|
state.todos.all = []
|
2017-04-01 17:51:27 +02:00
|
|
|
state.todos.input = ''
|
2017-03-21 03:00:45 +01:00
|
|
|
|
|
|
|
|
state.todos.idCounter = 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emitter.on('DOMContentLoaded', function () {
|
|
|
|
|
emitter.emit('log:debug', 'Loading todos model')
|
|
|
|
|
|
|
|
|
|
// CRUD
|
|
|
|
|
emitter.on('todos:create', create)
|
|
|
|
|
emitter.on('todos:update', update)
|
|
|
|
|
emitter.on('todos:delete', del)
|
|
|
|
|
|
2017-04-01 17:51:27 +02:00
|
|
|
// Special
|
|
|
|
|
emitter.on('todos:input', oninput)
|
|
|
|
|
|
2017-03-21 03:00:45 +01:00
|
|
|
// Shorthand
|
|
|
|
|
emitter.on('todos:edit', edit)
|
|
|
|
|
emitter.on('todos:unedit', unedit)
|
|
|
|
|
emitter.on('todos:toggle', toggle)
|
|
|
|
|
emitter.on('todos:toggleAll', toggleAll)
|
|
|
|
|
emitter.on('todos:deleteCompleted', deleteCompleted)
|
|
|
|
|
})
|
|
|
|
|
|
2017-04-01 17:51:27 +02:00
|
|
|
function oninput (text) {
|
|
|
|
|
state.todos.input = text
|
|
|
|
|
}
|
|
|
|
|
|
2017-03-21 03:00:45 +01:00
|
|
|
function create (name) {
|
|
|
|
|
var item = {
|
|
|
|
|
id: state.todos.idCounter,
|
|
|
|
|
editing: false,
|
|
|
|
|
done: false,
|
|
|
|
|
name: name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
state.todos.idCounter += 1
|
|
|
|
|
state.todos.active.push(item)
|
|
|
|
|
state.todos.all.push(item)
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function edit (id) {
|
|
|
|
|
state.todos.all.forEach(function (todo) {
|
|
|
|
|
if (todo.id === id) todo.editing = true
|
|
|
|
|
})
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function unedit (id) {
|
|
|
|
|
state.todos.all.forEach(function (todo) {
|
|
|
|
|
if (todo.id === id) todo.editing = false
|
|
|
|
|
})
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function update (newTodo) {
|
|
|
|
|
var todo = state.todos.all.filter(function (todo) {
|
|
|
|
|
return todo.id === newTodo.id
|
|
|
|
|
})[0]
|
|
|
|
|
|
|
|
|
|
if (newTodo.done && todo.done === false) {
|
|
|
|
|
state.todos.active.splice(state.todos.active.indexOf(todo), 1)
|
|
|
|
|
state.todos.done.push(todo)
|
|
|
|
|
} else if (newTodo.done === false && todo.done) {
|
|
|
|
|
state.todos.done.splice(state.todos.done.indexOf(todo), 1)
|
|
|
|
|
state.todos.active.push(todo)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mutate(todo, newTodo)
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function del (id) {
|
|
|
|
|
var i = null
|
|
|
|
|
var todo = null
|
|
|
|
|
state.todos.all.forEach(function (_todo, j) {
|
|
|
|
|
if (_todo.id === id) {
|
|
|
|
|
i = j
|
|
|
|
|
todo = _todo
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
state.todos.all.splice(i, 1)
|
|
|
|
|
|
|
|
|
|
if (todo.done) {
|
|
|
|
|
var done = state.todos.done
|
2017-03-21 19:49:21 -04:00
|
|
|
var doneIndex
|
|
|
|
|
done.forEach(function (_todo, j) {
|
|
|
|
|
if (_todo.id === id) {
|
|
|
|
|
doneIndex = j
|
|
|
|
|
}
|
|
|
|
|
})
|
2017-03-21 03:00:45 +01:00
|
|
|
done.splice(doneIndex, 1)
|
|
|
|
|
} else {
|
|
|
|
|
var active = state.todos.active
|
2017-03-21 19:49:21 -04:00
|
|
|
var activeIndex
|
|
|
|
|
active.forEach(function (_todo, j) {
|
|
|
|
|
if (_todo.id === id) {
|
|
|
|
|
activeIndex = j
|
|
|
|
|
}
|
|
|
|
|
})
|
2017-03-21 03:00:45 +01:00
|
|
|
active.splice(activeIndex, 1)
|
|
|
|
|
}
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteCompleted (data) {
|
|
|
|
|
var done = state.todos.done
|
|
|
|
|
done.forEach(function (todo) {
|
|
|
|
|
var index = state.todos.all.indexOf(todo)
|
|
|
|
|
state.todos.all.splice(index, 1)
|
|
|
|
|
})
|
|
|
|
|
state.todos.done = []
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggle (id) {
|
|
|
|
|
var todo = state.todos.all.filter(function (todo) {
|
|
|
|
|
return todo.id === id
|
|
|
|
|
})[0]
|
|
|
|
|
var done = todo.done
|
|
|
|
|
todo.done = !done
|
|
|
|
|
var arr = done ? state.todos.done : state.todos.active
|
|
|
|
|
var target = done ? state.todos.active : state.todos.done
|
|
|
|
|
var index = arr.indexOf(todo)
|
|
|
|
|
arr.splice(index, 1)
|
|
|
|
|
target.push(todo)
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function toggleAll (data) {
|
|
|
|
|
var todos = state.todos.all
|
|
|
|
|
var allDone = state.todos.all.length &&
|
|
|
|
|
state.todos.done.length === state.todos.all.length
|
|
|
|
|
|
|
|
|
|
todos.forEach(function (todo) {
|
|
|
|
|
todo.done = !allDone
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (allDone) {
|
|
|
|
|
state.todos.done = []
|
|
|
|
|
state.todos.active = state.todos.all
|
|
|
|
|
} else {
|
|
|
|
|
state.todos.done = state.todos.all
|
|
|
|
|
state.todos.active = []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emitter.emit('render')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function Footer (state, emit) {
|
|
|
|
|
var filter = window.location.hash.replace(/^#/, '')
|
|
|
|
|
var activeCount = state.todos.active.length
|
|
|
|
|
var hasDone = state.todos.done.length
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<footer class="footer">
|
|
|
|
|
<span class="todo-count">
|
|
|
|
|
<strong>${activeCount}</strong>
|
|
|
|
|
item${state.todos.all === 1 ? '' : 's'} left
|
|
|
|
|
</span>
|
|
|
|
|
<ul class="filters">
|
|
|
|
|
${filterButton('All', '', filter, emit)}
|
|
|
|
|
${filterButton('Active', 'active', filter, emit)}
|
|
|
|
|
${filterButton('Completed', 'completed', filter, emit)}
|
|
|
|
|
</ul>
|
|
|
|
|
${hasDone ? deleteCompleted(emit) : ''}
|
|
|
|
|
</footer>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
function filterButton (name, filter, currentFilter, emit) {
|
|
|
|
|
var filterClass = filter === currentFilter
|
|
|
|
|
? 'selected'
|
|
|
|
|
: ''
|
|
|
|
|
|
|
|
|
|
var uri = '#' + name.toLowerCase()
|
|
|
|
|
if (uri === '#all') uri = '/'
|
|
|
|
|
return html`
|
|
|
|
|
<li>
|
|
|
|
|
<a href=${uri} class=${filterClass}>
|
|
|
|
|
${name}
|
|
|
|
|
</a>
|
|
|
|
|
</li>
|
|
|
|
|
`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function deleteCompleted (emit) {
|
|
|
|
|
return html`
|
|
|
|
|
<button class="clear-completed" onclick=${deleteAllCompleted}>
|
|
|
|
|
Clear completed
|
|
|
|
|
</button>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
function deleteAllCompleted () {
|
|
|
|
|
emit('todos:deleteCompleted')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-04-01 17:51:27 +02:00
|
|
|
function Header (state, emit) {
|
2017-03-21 03:00:45 +01:00
|
|
|
return html`
|
|
|
|
|
<header class="header">
|
|
|
|
|
<h1>todos</h1>
|
|
|
|
|
<input class="new-todo"
|
2017-04-01 17:51:27 +02:00
|
|
|
value=${state.todos.input}
|
2017-03-21 03:00:45 +01:00
|
|
|
autofocus
|
|
|
|
|
placeholder="What needs to be done?"
|
|
|
|
|
onkeydown=${createTodo} />
|
|
|
|
|
</header>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
function createTodo (e) {
|
2017-04-01 17:51:27 +02:00
|
|
|
var value = e.target.value
|
|
|
|
|
if (!value) return
|
2017-03-21 03:00:45 +01:00
|
|
|
if (e.keyCode === 13) {
|
2017-04-01 17:51:27 +02:00
|
|
|
emit('todos:input', '')
|
|
|
|
|
emit('todos:create', value)
|
|
|
|
|
} else {
|
|
|
|
|
emit('todos:input', value)
|
2017-03-21 03:00:45 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TodoItem (todo, emit) {
|
|
|
|
|
return html`
|
|
|
|
|
<li class=${classList({ completed: todo.done, editing: todo.editing })}>
|
|
|
|
|
<div class="view">
|
|
|
|
|
<input
|
|
|
|
|
type="checkbox"
|
|
|
|
|
class="toggle"
|
|
|
|
|
checked="${todo.done}"
|
|
|
|
|
onchange=${toggle} />
|
|
|
|
|
<label ondblclick=${edit}>${todo.name}</label>
|
|
|
|
|
<button
|
|
|
|
|
class="destroy"
|
|
|
|
|
onclick=${destroy}
|
|
|
|
|
></button>
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
|
|
|
|
class="edit"
|
|
|
|
|
value=${todo.name}
|
|
|
|
|
onkeydown=${handleEditKeydown}
|
|
|
|
|
onblur=${update} />
|
|
|
|
|
</li>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
function toggle (e) {
|
|
|
|
|
emit('todos:toggle', todo.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function edit (e) {
|
|
|
|
|
emit('todos:edit', todo.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function destroy (e) {
|
|
|
|
|
emit('todos:delete', todo.id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function update (e) {
|
|
|
|
|
emit('todos:update', {
|
|
|
|
|
id: todo.id,
|
|
|
|
|
editing: false,
|
|
|
|
|
name: e.target.value
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEditKeydown (e) {
|
|
|
|
|
if (e.keyCode === 13) update(e) // Enter
|
|
|
|
|
else if (e.code === 27) emit('todos:unedit') // Escape
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function classList (classes) {
|
|
|
|
|
var str = ''
|
|
|
|
|
var keys = Object.keys(classes)
|
|
|
|
|
for (var i = 0, len = keys.length; i < len; i++) {
|
|
|
|
|
var key = keys[i]
|
|
|
|
|
var val = classes[key]
|
|
|
|
|
if (val) str += (key + ' ')
|
|
|
|
|
}
|
|
|
|
|
return str
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function TodoList (state, emit) {
|
|
|
|
|
var filter = window.location.hash.replace(/^#/, '')
|
|
|
|
|
var items = filter === 'completed'
|
|
|
|
|
? state.todos.done
|
|
|
|
|
: filter === 'active'
|
|
|
|
|
? state.todos.active
|
|
|
|
|
: state.todos.all
|
|
|
|
|
|
|
|
|
|
var allDone = state.todos.done.length === state.todos.all.length
|
|
|
|
|
|
|
|
|
|
var nodes = items.map(function (todo) {
|
|
|
|
|
return TodoItem(todo, emit)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
return html`
|
|
|
|
|
<section class="main">
|
|
|
|
|
<input
|
|
|
|
|
class="toggle-all"
|
|
|
|
|
type="checkbox"
|
|
|
|
|
checked=${allDone}
|
|
|
|
|
onchange=${toggleAll}/>
|
|
|
|
|
<label for="toggle-all" style="display: none;">
|
|
|
|
|
Mark all as done
|
|
|
|
|
</label>
|
|
|
|
|
<ul class="todo-list">
|
|
|
|
|
${nodes}
|
|
|
|
|
</ul>
|
|
|
|
|
</section>
|
|
|
|
|
`
|
|
|
|
|
|
|
|
|
|
function toggleAll () {
|
|
|
|
|
emit('todos:toggleAll')
|
|
|
|
|
}
|
|
|
|
|
}
|