Created
August 14, 2019 15:25
-
-
Save loveyunk/cd2b2859361a6e2da1d8f01ffd64f773 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> | |
<title>Vue Todo</title> | |
<style> | |
html, | |
body { | |
margin: 0; | |
padding: 0; | |
} | |
button { | |
margin: 0; | |
padding: 0; | |
border: 0; | |
background: none; | |
font-size: 100%; | |
vertical-align: baseline; | |
font-family: inherit; | |
font-weight: inherit; | |
color: inherit; | |
-webkit-appearance: none; | |
appearance: none; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
body { | |
font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; | |
line-height: 1.4em; | |
background: #f5f5f5; | |
color: #4d4d4d; | |
min-width: 230px; | |
max-width: 550px; | |
margin: 0 auto; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
font-weight: 300; | |
} | |
:focus { | |
outline: 0; | |
} | |
.hidden { | |
display: none; | |
} | |
.todoapp { | |
background: #fff; | |
margin: 130px 0 40px 0; | |
position: relative; | |
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); | |
} | |
.todoapp input::-webkit-input-placeholder { | |
font-style: italic; | |
font-weight: 300; | |
color: #e6e6e6; | |
} | |
.todoapp input::-moz-placeholder { | |
font-style: italic; | |
font-weight: 300; | |
color: #e6e6e6; | |
} | |
.todoapp input::input-placeholder { | |
font-style: italic; | |
font-weight: 300; | |
color: #e6e6e6; | |
} | |
.todoapp h1 { | |
position: absolute; | |
top: -155px; | |
width: 100%; | |
font-size: 100px; | |
font-weight: 100; | |
text-align: center; | |
color: rgba(175, 47, 47, 0.15); | |
-webkit-text-rendering: optimizeLegibility; | |
-moz-text-rendering: optimizeLegibility; | |
text-rendering: optimizeLegibility; | |
} | |
.new-todo, | |
.edit { | |
position: relative; | |
margin: 0; | |
width: 100%; | |
font-size: 24px; | |
font-family: inherit; | |
font-weight: inherit; | |
line-height: 1.4em; | |
border: 0; | |
color: inherit; | |
padding: 6px; | |
border: 1px solid #999; | |
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); | |
box-sizing: border-box; | |
-webkit-font-smoothing: antialiased; | |
-moz-osx-font-smoothing: grayscale; | |
} | |
.new-todo { | |
padding: 16px 16px 16px 60px; | |
border: none; | |
background: rgba(0, 0, 0, 0.003); | |
box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); | |
} | |
.main { | |
position: relative; | |
z-index: 2; | |
border-top: 1px solid #e6e6e6; | |
} | |
.toggle-all { | |
width: 1px; | |
height: 1px; | |
border: none; /* Mobile Safari */ | |
opacity: 0; | |
position: absolute; | |
right: 100%; | |
bottom: 100%; | |
} | |
.toggle-all + label { | |
width: 60px; | |
height: 34px; | |
font-size: 0; | |
position: absolute; | |
top: -52px; | |
left: -13px; | |
-webkit-transform: rotate(90deg); | |
transform: rotate(90deg); | |
} | |
.toggle-all + label:before { | |
content: '❯'; | |
font-size: 22px; | |
color: #e6e6e6; | |
padding: 10px 27px 10px 27px; | |
} | |
.toggle-all:checked + label:before { | |
color: #737373; | |
} | |
.todo-list { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
} | |
.todo-list li { | |
position: relative; | |
font-size: 24px; | |
border-bottom: 1px solid #ededed; | |
} | |
.todo-list li:last-child { | |
border-bottom: none; | |
} | |
.todo-list li.editing { | |
border-bottom: none; | |
padding: 0; | |
} | |
.todo-list li.editing .edit { | |
display: block; | |
width: calc(100% - 43px); | |
padding: 12px 16px; | |
margin: 0 0 0 43px; | |
} | |
.todo-list li.editing .view { | |
display: none; | |
} | |
.todo-list li .toggle { | |
text-align: center; | |
width: 40px; | |
/* auto, since non-WebKit browsers doesn't support input styling */ | |
height: auto; | |
position: absolute; | |
top: 0; | |
bottom: 0; | |
margin: auto 0; | |
border: none; /* Mobile Safari */ | |
-webkit-appearance: none; | |
appearance: none; | |
} | |
.todo-list li .toggle { | |
opacity: 0; | |
} | |
.todo-list li .toggle + label { | |
/* | |
Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 | |
IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ | |
*/ | |
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); | |
background-repeat: no-repeat; | |
background-position: center left; | |
} | |
.todo-list li .toggle:checked + label { | |
background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); | |
} | |
.todo-list li label { | |
word-break: break-all; | |
padding: 15px 15px 15px 60px; | |
display: block; | |
line-height: 1.2; | |
transition: color 0.4s; | |
} | |
.todo-list li.completed label { | |
color: #d9d9d9; | |
text-decoration: line-through; | |
} | |
.todo-list li .destroy { | |
display: none; | |
position: absolute; | |
top: 0; | |
right: 10px; | |
bottom: 0; | |
width: 40px; | |
height: 40px; | |
margin: auto 0; | |
font-size: 30px; | |
color: #cc9a9a; | |
margin-bottom: 11px; | |
transition: color 0.2s ease-out; | |
} | |
.todo-list li .destroy:hover { | |
color: #af5b5e; | |
} | |
.todo-list li .destroy:after { | |
content: '×'; | |
} | |
.todo-list li:hover .destroy { | |
display: block; | |
} | |
.todo-list li .edit { | |
display: none; | |
} | |
.todo-list li.editing:last-child { | |
margin-bottom: -1px; | |
} | |
.footer { | |
color: #777; | |
padding: 10px 15px; | |
height: 20px; | |
text-align: center; | |
border-top: 1px solid #e6e6e6; | |
} | |
.footer:before { | |
content: ''; | |
position: absolute; | |
right: 0; | |
bottom: 0; | |
left: 0; | |
height: 50px; | |
overflow: hidden; | |
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 0 17px 2px -6px rgba(0, 0, 0, 0.2); | |
} | |
.todo-count { | |
float: left; | |
text-align: left; | |
} | |
.todo-count strong { | |
font-weight: 300; | |
} | |
.filters { | |
margin: 0; | |
padding: 0; | |
list-style: none; | |
position: absolute; | |
right: 0; | |
left: 0; | |
} | |
.filters li { | |
display: inline; | |
} | |
.filters li a { | |
color: inherit; | |
margin: 3px; | |
padding: 3px 7px; | |
text-decoration: none; | |
border: 1px solid transparent; | |
border-radius: 3px; | |
} | |
.filters li a:hover { | |
border-color: rgba(175, 47, 47, 0.1); | |
} | |
.filters li a.selected { | |
border-color: rgba(175, 47, 47, 0.2); | |
} | |
.clear-completed, | |
html .clear-completed:active { | |
float: right; | |
position: relative; | |
line-height: 20px; | |
text-decoration: none; | |
cursor: pointer; | |
} | |
.clear-completed:hover { | |
text-decoration: underline; | |
} | |
.info { | |
margin: 65px auto 0; | |
color: #bfbfbf; | |
font-size: 10px; | |
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); | |
text-align: center; | |
} | |
.info p { | |
line-height: 1; | |
} | |
.info a { | |
color: inherit; | |
text-decoration: none; | |
font-weight: 400; | |
} | |
.info a:hover { | |
text-decoration: underline; | |
} | |
/* | |
Hack to remove background from Mobile Safari. | |
Can't use it globally since it destroys checkboxes in Firefox | |
*/ | |
@media screen and (-webkit-min-device-pixel-ratio: 0) { | |
.toggle-all, | |
.todo-list li .toggle { | |
background: none; | |
} | |
.todo-list li .toggle { | |
height: 40px; | |
} | |
} | |
@media (max-width: 430px) { | |
.footer { | |
height: 50px; | |
} | |
.filters { | |
bottom: 10px; | |
} | |
} | |
[v-cloak] { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<section class="todoapp"> | |
<header class="header"> | |
<h1>todos</h1> | |
<input class="new-todo" autofocus autocomplete="off" placeholder="What needs to be done?" v-model="newTodo" @keyup.enter="addTodo" /> | |
</header> | |
<section class="main" v-show="todos.length" v-cloak> | |
<input id="toggle-all" class="toggle-all" type="checkbox" v-model="allDone" /> | |
<label for="toggle-all"></label> | |
<ul class="todo-list"> | |
<li v-for="todo in filteredTodos" class="todo" :key="todo.id" :class="{ completed: todo.completed, editing: todo == editedTodo }"> | |
<div class="view"> | |
<input class="toggle" type="checkbox" v-model="todo.completed" /> | |
<label @dblclick="editTodo(todo)">{{ todo.title }}</label> | |
<button class="destroy" @click="removeTodo(todo)"></button> | |
</div> | |
<input | |
class="edit" | |
type="text" | |
v-model="todo.title" | |
v-todo-focus="todo == editedTodo" | |
@blur="doneEdit(todo)" | |
@keyup.enter="doneEdit(todo)" | |
@keyup.esc="cancelEdit(todo)" | |
/> | |
</li> | |
</ul> | |
</section> | |
<footer class="footer" v-show="todos.length" v-cloak> | |
<span class="todo-count"> | |
<strong>{{ remaining }}</strong> {{ remaining | pluralize }} left | |
</span> | |
<ul class="filters"> | |
<li><a href="#/all" :class="{ selected: visibility == 'all' }">All</a></li> | |
<li><a href="#/active" :class="{ selected: visibility == 'active' }">Active</a></li> | |
<li><a href="#/completed" :class="{ selected: visibility == 'completed' }">Completed</a></li> | |
</ul> | |
<button class="clear-completed" @click="removeCompleted" v-show="todos.length > remaining"> | |
Clear completed | |
</button> | |
</footer> | |
</section> | |
<footer class="info"> | |
<p>Double-click to edit a todo</p> | |
<p>Written by <a href="http://evanyou.me">Evan You</a></p> | |
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p> | |
</footer> | |
<script src="https://cdn.jsdelivr.net/npm/vue"></script> | |
<script> | |
// Full spec-compliant TodoMVC with localStorage persistence | |
// and hash-based routing in ~120 effective lines of JavaScript. | |
// localStorage persistence | |
var STORAGE_KEY = 'todos-vuejs-2.0'; | |
var todoStorage = { | |
fetch: function() { | |
var todos = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); | |
todos.forEach(function(todo, index) { | |
todo.id = index; | |
}); | |
todoStorage.uid = todos.length; | |
return todos; | |
}, | |
save: function(todos) { | |
localStorage.setItem(STORAGE_KEY, JSON.stringify(todos)); | |
} | |
}; | |
// visibility filters | |
var filters = { | |
all: function(todos) { | |
return todos; | |
}, | |
active: function(todos) { | |
return todos.filter(function(todo) { | |
return !todo.completed; | |
}); | |
}, | |
completed: function(todos) { | |
return todos.filter(function(todo) { | |
return todo.completed; | |
}); | |
} | |
}; | |
// app Vue instance | |
var app = new Vue({ | |
// app initial state | |
data: { | |
todos: todoStorage.fetch(), | |
newTodo: '', | |
editedTodo: null, | |
visibility: 'all' | |
}, | |
// watch todos change for localStorage persistence | |
watch: { | |
todos: { | |
handler: function(todos) { | |
todoStorage.save(todos); | |
}, | |
deep: true | |
} | |
}, | |
// computed properties | |
// http://vuejs.org/guide/computed.html | |
computed: { | |
filteredTodos: function() { | |
return filters[this.visibility](this.todos); | |
}, | |
remaining: function() { | |
return filters.active(this.todos).length; | |
}, | |
allDone: { | |
get: function() { | |
return this.remaining === 0; | |
}, | |
set: function(value) { | |
this.todos.forEach(function(todo) { | |
todo.completed = value; | |
}); | |
} | |
} | |
}, | |
filters: { | |
pluralize: function(n) { | |
return n === 1 ? 'item' : 'items'; | |
} | |
}, | |
// methods that implement data logic. | |
// note there's no DOM manipulation here at all. | |
methods: { | |
addTodo: function() { | |
var value = this.newTodo && this.newTodo.trim(); | |
if (!value) { | |
return; | |
} | |
this.todos.push({ | |
id: todoStorage.uid++, | |
title: value, | |
completed: false | |
}); | |
this.newTodo = ''; | |
}, | |
removeTodo: function(todo) { | |
this.todos.splice(this.todos.indexOf(todo), 1); | |
}, | |
editTodo: function(todo) { | |
this.beforeEditCache = todo.title; | |
this.editedTodo = todo; | |
}, | |
doneEdit: function(todo) { | |
if (!this.editedTodo) { | |
return; | |
} | |
this.editedTodo = null; | |
todo.title = todo.title.trim(); | |
if (!todo.title) { | |
this.removeTodo(todo); | |
} | |
}, | |
cancelEdit: function(todo) { | |
this.editedTodo = null; | |
todo.title = this.beforeEditCache; | |
}, | |
removeCompleted: function() { | |
this.todos = filters.active(this.todos); | |
} | |
}, | |
// a custom directive to wait for the DOM to be updated | |
// before focusing on the input field. | |
// http://vuejs.org/guide/custom-directive.html | |
directives: { | |
'todo-focus': function(el, binding) { | |
if (binding.value) { | |
el.focus(); | |
} | |
} | |
} | |
}); | |
// handle routing | |
function onHashChange() { | |
var visibility = window.location.hash.replace(/#\/?/, ''); | |
if (filters[visibility]) { | |
app.visibility = visibility; | |
} else { | |
window.location.hash = ''; | |
app.visibility = 'all'; | |
} | |
} | |
window.addEventListener('hashchange', onHashChange); | |
onHashChange(); | |
// mount | |
app.$mount('.todoapp'); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment