<div id="todo" class="container">
<section class="todo">
<header class="todo__header">
<h1 @keydown.enter.prevent="updateTitle" @keyup="updateTitle" @blur="updateTitle" @paste="updateTitle" @delete="updateTitle" contentEditable class="todo__title">{{ title }}</h1>
</header>
<transition-group name="todo__item" tag="ul" class="todo__list">
<li v-for="(todo, index) in todos" v-if="todo.active" :key="todo.id" :class="{ 'is-done': todo.done }" class="todo__item">
<label class="checkbox todo__checkbox">
<input type="checkbox" :checked="todo.done" v-model="todo.done" class="checkbox__input">
<span class="checkbox__icon"></span>
</label>
<span @keydown.enter.prevent="editItem(todo, $event)" @keyup="editItem(todo, $event)" @blur="editItem(todo, $event)" @paste="editItem(todo, $event)" @delete="editItem(todo, $event)" contentEditable class="todo__text">{{ todo.text }}</span>
<button @click="deactivateItem(todo)" class="todo__delete">x</button>
</li>
</transition-group>
<form @submit.prevent="addItem" class="todo__form">
<input type="text" name="text" id="text" autocomplete="off" placeholder="New Entry" class="todo__input">
</form>
</section>
<section class="recycle-bin">
<header class="recycle-bin__header">
<h2>Recycle Bin</h2> <button @click="resetList">Delete List</button>
</header>
<transition-group name="todo__item" tag="ul" class="todo__list">
<li v-for="(todo, index) in todos" v-if="!todo.active" :key="todo.id" :class="{ 'is-done': todo.done }" class="todo__item">
<label class="checkbox todo__checkbox">
<input type="checkbox" :checked="todo.done" v-model="todo.done" class="checkbox__input">
<span class="checkbox__icon"></span>
</label>
<span @keydown.enter.prevent="editItem(todo, $event)" contentEditable class="todo__text">{{ todo.text }}</span>
<div class="todo__button-group">
<button @click="activateItem(todo)">^</button>
<button @click="deleteItem(index)">X</button>
</div>
</li>
</transition-group>
</section>
</div>
@function svg-url($svg){
$encoded: '';
$slice: 2000;
$index: 0;
$loops: ceil(str-length($svg)/$slice);
@for $i from 1 through $loops {
$chunk: str-slice($svg, $index, $index + $slice - 1);
$chunk: str-replace($chunk,'"','\'');
$chunk: str-replace($chunk,'<','%3C');
$chunk: str-replace($chunk,'>','%3E');
$chunk: str-replace($chunk,'&','%26');
$chunk: str-replace($chunk,'#','%23');
$encoded: #{$encoded}#{$chunk};
$index: $index + $slice;
}
@return url("data:image/svg+xml;charset=utf8,#{$encoded}");
}
@function str-replace($string, $search, $replace: '') {
$index: str-index($string, $search);
@if $index {
@return str-slice($string, 1, $index - 1) + $replace +
str-replace(str-slice($string, $index +
str-length($search)), $search, $replace);
}
@return $string;
}
@mixin sr-only {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
$color-text: #333;
$color-text-disabled: rgba($color-text, 0.4);
*, *::before, *::after {
box-sizing: border-box;
}
body {
padding: 1em;
font-family: 'Roboto', sans-serif;
font-size: 1em;
color: $color-text;
}
h1, h2 {
margin: 0;
font-weight: 300;
}
h1 {
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
}
button {
padding: 0.25em 0.5em;
font-family: 'Roboto', sans-serif;
font-weight: 300;
font-size: 0.9em;
text-transform: uppercase;
line-height: 1;
color: #fff;
background-color: rgba(0, 0, 0, 0.4);
border: 0.0625rem solid rgba(0, 0, 0, 0.5);
cursor: pointer;
&:hover,
&:focus {
background-color: rgba(0, 0, 0, 0.6);
outline: none;
}
}
.checkbox {
position: absolute;
left: 1em;
z-index: 1;
}
.checkbox__input {
@include sr-only;
}
.checkbox__icon {
display: block;
width: 0.9em;
height: 0.9em;
cursor: pointer;
.checkbox__input + & {
background: svg-url('<svg fill="#{$color-text}" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z"/></svg>') left / 100% no-repeat;
}
.checkbox__input:checked + & {
background-image: svg-url('<svg fill="#{$color-text}" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
}
.checkbox__input:focus + & {
box-shadow: 0 0 0 0.0625rem rgba(0, 0, 0, 0.5);
border-radius: 0.1em;
}
}
.container {
max-width: 25em;
}
.todo {
margin-bottom: 1em;
background-color: #fffb93;
}
.todo__header {
padding: 1em;
}
.todo__title {
&:focus {
outline: none;
}
}
.todo__list {
list-style: none;
margin: 0;
padding: 0;
font-family: 'Roboto Slab', serif;
}
.todo__item {
display: flex;
align-items: center;
position: relative;
height: 2em;
transition: opacity 0.2s, height 0.2s;
}
.todo__item-enter, .todo__item-leave-active {
height: 0;
opacity: 0;
}
.todo__checkbox {
transition: opacity 0.2s;
.is-done & {
opacity: 0.4;
}
}
.todo__text {
display: flex;
flex-grow: 1;
align-items: center;
height: 100%;
padding-left: 2.5em;
transition: opacity 0.2s;
&:focus {
outline: none;
border-top: 0.0625rem solid rgba(0, 0, 0, 0.2);
border-bottom: 0.0625rem solid rgba(0, 0, 0, 0.2);
}
.is-done & {
color: $color-text-disabled;
}
}
.todo__delete {
position: absolute;
right: 1em;
width: 1.25em;
height: 1.25em;
padding: 0;
text-transform: none;
border-radius: 50%;
opacity: 0;
transition: opacity 0.2s;
&:focus,
.todo__item:hover & {
opacity: 1;
}
}
.todo__form {
display: flex;
flex-wrap: wrap;
padding-bottom: 1em;
}
.todo__input {
flex-grow: 1;
height: 2em;
margin-right: -0.0625rem;
padding: 0 0 0 2.5em;
font-family: 'Roboto Slab', serif;
background: transparent svg-url('<svg fill="#{$color-text-disabled}" height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>') left 0.9em center / 1em no-repeat;
border: none;
&:focus {
border-top: 0.0625rem solid rgba(0, 0, 0, 0.2);
border-bottom: 0.0625rem solid rgba(0, 0, 0, 0.2);
outline: none;
}
&::placeholder {
opacity: 1;
color: $color-text-disabled;
}
}
.todo__delete-list {
margin-bottom: 1em;
}
.todo__button-group {
position: absolute;
right: 1em;
}
.recycle-bin {
background-color: #ffbaba;
padding-bottom: 1em;
}
.recycle-bin__header {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 1em;
}
View Compiled
'use strict';
let createUUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};
let updateLocalStorage = function(key, value) {
localStorage.setItem(key, JSON.stringify(value));
};
let getLocalStorage = function(key) {
return JSON.parse(localStorage.getItem(key));
};
let todo = new Vue({
el: '#todo',
data: {
title: 'Simple Todo List',
todos: [
{
id: createUUID(),
active: true,
done: true,
text: 'Create todo list with Vue.js'
},
{
id: createUUID(),
active: true,
done: false,
text: 'Make todo list more awesome'
},
{
id: createUUID(),
active: false,
done: false,
text: 'Secret hidden todo list entry'
}
]
},
created: function() {
let todoLocal = getLocalStorage('todos');
let titleLocal = getLocalStorage('title');
if (todoLocal && todoLocal.length > 0) {
this.todos = todoLocal;
}
if (titleLocal) {
this.title = titleLocal;
}
},
methods: {
updateTitle: _.debounce(function(event) {
let title = event.target.innerText;
if (title) {
this.title = title;
updateLocalStorage('title', title);
}
}, 500),
addItem: function(event) {
let formData = new FormData(event.target);
let text = formData.get('text');
if (text) {
this.todos.push({
id: createUUID(),
active: true,
done: false,
text: text
});
}
updateLocalStorage('todos', this.todos);
event.target.reset();
},
editItem: _.debounce(function(todo, event) {
let text = event.target.innerText;
if (text) {
todo.text = event.target.innerText;
updateLocalStorage('todos', this.todos);
}
}, 500),
deactivateItem: function(todo) {
todo.active = false;
updateLocalStorage('todos', this.todos);
},
activateItem: function(todo) {
todo.active = true;
updateLocalStorage('todos', this.todos);
},
deleteItem: function(index) {
this.todos.splice(index, 1);
},
resetList: function() {
if (confirm('This deletes the whole list including unfinished entries!')) {
this.title = 'Simple Todo List';
this.todos = [];
localStorage.removeItem('todos');
localStorage.removeItem('title');
}
}
}
});
View Compiled