├── .gitignore
├── server
├── README.md
├── index.html
├── js
├── stores.js
├── helpers.js
└── app.js
└── css
└── style.css
/.gitignore:
--------------------------------------------------------------------------------
1 | gh-pages
--------------------------------------------------------------------------------
/server:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | browser-sync start --server --files="index.html, css/*, js/*"
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # A simple to do list in a pure vanilla javascript
2 |
3 | [DEMO PAGE HERE, I hope you like :)](http://expalmer.github.io/todo-list-vanilla-js/)
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Todo List
6 |
7 |
8 |
9 | Todo List
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | 0 items left
18 |
19 |
26 |
27 |
28 |
29 |
30 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/js/stores.js:
--------------------------------------------------------------------------------
1 | ;(function(context) {
2 |
3 | 'use strict';
4 |
5 | var store = localStorage;
6 |
7 | function Stores( key ) {
8 | this.key = key;
9 | if( !store[key] ) {
10 | store[key] = JSON.stringify([]);
11 | }
12 | }
13 |
14 | Stores.fn = Stores.prototype;
15 |
16 | Stores.fn.find = function( id, cb ) {
17 |
18 | var items = JSON.parse(store[this.key]);
19 | var item = items
20 | .filter(function(item) {
21 | return id === item.id;
22 | });
23 | cb.call(this, item[0] || {} );
24 | };
25 |
26 | Stores.fn.findAll = function( cb ) {
27 | cb.call(this, JSON.parse( store[this.key] ));
28 | };
29 |
30 | Stores.fn.save = function( item, cb, options ) {
31 |
32 | var items = JSON.parse(store[this.key]);
33 |
34 | // Implementar Update Multiple
35 | // if ( options && options.multi ) {
36 | // }
37 |
38 | // Update
39 | if (item.id) {
40 | items = items
41 | .map(function( x ) {
42 | if( x.id === item.id ) {
43 | for (var prop in item ) {
44 | x[prop] = item[prop];
45 | }
46 | }
47 | return x;
48 | });
49 | // Insert
50 | } else {
51 | item.id = new Date().getTime();
52 | items.push(item);
53 | }
54 |
55 | store[this.key] = JSON.stringify(items);
56 |
57 | cb.call(this, item);
58 | // this.findAll(cb);
59 |
60 | };
61 |
62 | Stores.fn.destroy = function( id, cb ) {
63 |
64 | var items = JSON.parse(store[this.key]);
65 | items = items
66 | .filter(function( x ) {
67 | return x.id !== id;
68 | });
69 |
70 | store[this.key] = JSON.stringify(items);
71 |
72 | cb.call(this, true);
73 |
74 | };
75 |
76 |
77 | Stores.fn.drop = function( cb ) {
78 | store[this.key] = JSON.stringify([]);
79 | this.findAll(cb);
80 | };
81 |
82 | context.Stores = Stores;
83 |
84 | })( this );
--------------------------------------------------------------------------------
/js/helpers.js:
--------------------------------------------------------------------------------
1 | ;(function(context) {
2 |
3 | 'use strict';
4 |
5 | function $( selector, scope ) {
6 | return $.qsa( selector, scope, true );
7 | }
8 |
9 | $['qsa'] = function( selector, scope, first ) {
10 | var e = ( scope || document).querySelectorAll( selector );
11 | return first ? e[0] : e;
12 | };
13 |
14 | $['noop'] = function() {};
15 |
16 |
17 | $['each'] = function( array, cb ) {
18 | var len = array.length;
19 | var idx = -1;
20 | while( ++idx < len ) {
21 | cb.call(array, array[idx], idx, array);
22 | }
23 | };
24 |
25 | $['pluralization'] = function( value ) {
26 | return +value === 1 ? "" : "s";
27 | };
28 |
29 | $['on'] = function (target, type, callback, useCapture) {
30 | target.addEventListener(type, callback, !!useCapture);
31 | };
32 |
33 | $['delegate'] = function (target, selector, type, handler) {
34 | function dispatchEvent(event) {
35 | var targetElement = event.target;
36 | var potentialElements = $.qsa(selector, target);
37 | var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
38 | if (hasMatch) {
39 | handler.call(targetElement, event);
40 | }
41 | }
42 |
43 | // https://developer.mozilla.org/en-US/docs/Web/Events/blur
44 | var useCapture = type === 'blur' || type === 'focus';
45 |
46 | $.on(target, type, dispatchEvent, useCapture);
47 | };
48 |
49 | $['parent'] = function (element, tagName) {
50 | if (!element.parentNode) {
51 | return;
52 | }
53 | if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
54 | return element.parentNode;
55 | }
56 | return $.parent(element.parentNode, tagName);
57 | };
58 |
59 | context.$ = $;
60 |
61 | })(this);
62 |
63 | // Prototype
64 | NodeList.prototype.each = function( fn ) {
65 | var len = this.length;
66 | var idx = -1;
67 | while( ++idx < len ) {
68 | fn.call(this, this[idx], idx, this);
69 | }
70 | }
71 |
72 | Array.prototype.some = function( fn ) {
73 |
74 | var len = this.length;
75 | var idx = -1;
76 | while( ++idx < len ) {
77 | if( fn( this[idx], idx, this ) ) {
78 | return true;
79 | }
80 | }
81 | return false;
82 |
83 | };
84 |
85 |
--------------------------------------------------------------------------------
/css/style.css:
--------------------------------------------------------------------------------
1 | html {
2 | box-sizing: border-box;
3 | }
4 |
5 | *, *:before, *:after {
6 | box-sizing: inherit;
7 | margin: 0;
8 | padding: 0;
9 | }
10 |
11 | body {
12 | margin: 0 auto;
13 | font-size: 100%;
14 | font-family: 'Roboto Slab', serif;
15 | color: #fff;
16 | background: linear-gradient(to right, #26232C 0%,#26232C 40%,#26232C 100%);
17 | }
18 |
19 | ::-webkit-input-placeholder {
20 | color: #3C3647;
21 | font-style: italic;
22 | }
23 |
24 | h1 {
25 | margin-top: 20px;
26 | text-align: center;
27 | color: #7DD180;
28 | text-shadow: 0px 2px #231C31;
29 | }
30 |
31 | a {
32 | text-decoration: none;
33 | }
34 |
35 | .limiter {
36 | margin: 20px auto;
37 | max-width: 600px;
38 | background: #8cbdc5;
39 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
40 | }
41 |
42 | .row:before,
43 | .row:after {
44 | content: "";
45 | display: table;
46 | }
47 | .row:after {
48 | clear: both;
49 | }
50 | .row {
51 | zoom: 1;
52 | }
53 |
54 | .col-1-2 {
55 | float: left;
56 | padding: 10px;
57 | width: 40%;
58 | }
59 |
60 | .col-1-4 {
61 | float: left;
62 | padding: 10px;
63 | width: 30%;
64 | }
65 |
66 | .insert {
67 | position: relative;
68 | padding: 30px;
69 | border-top: solid 1px #3B3841;
70 | overflow: hidden;
71 | background: #8cbdc5;
72 | }
73 |
74 | #js-insert {
75 | top: 0;
76 | left: 50px;
77 | width: 100%;
78 | height: 60px;
79 | position: absolute;
80 | border: none;
81 | outline: none;
82 | font-size: 23px;
83 | font-family: inherit;
84 | color:black;
85 | background: #8cbdc5;
86 | }
87 |
88 | #js-toggle-all {
89 | position: absolute;
90 | top: 20px;
91 | left: 20px;
92 | }
93 |
94 | .bar {
95 | color: #6C6777;
96 | background: #eee;
97 | border-top: solid 1px #ddd;
98 | border-bottom: solid 1px #ddd;
99 | box-shadow: inset 0 0 16px rgba(0,0,0,0.1);
100 | }
101 |
102 | .info {
103 | float: left;
104 | width: 33.333333333%;
105 | padding: 10px;
106 | font-size: 14px;
107 | }
108 |
109 | .info:last-child {
110 | text-align: right;
111 | }
112 |
113 | .total {
114 | display: inline-block;
115 | float: left;
116 | margin-top: 4px;
117 | }
118 |
119 | .filter {
120 | text-align: center;
121 | }
122 |
123 | .filter li {
124 | display: inline-block;
125 | }
126 |
127 | .button {
128 | display: inline-block;
129 | margin: 0 4px;
130 | padding: 2px 8px;
131 | font-size: 13px;
132 | color: #6C6777;
133 | border: solid 1px #6C6777;
134 | border-radius: 20px;
135 | cursor: pointer;
136 | outline: none;
137 | }
138 |
139 | .button.selected {
140 | color: #fff;
141 | background: #149f00;
142 | border: solid 1px #6C6777;
143 | }
144 |
145 | .button:active {
146 | /*transform: translate(0px,2px);*/
147 | /*border-bottom: 1px solid;*/
148 | }
149 |
150 | .button--clear {
151 | float: right;
152 | color: #fff;
153 | background: #EE1630;
154 | border: solid 1px #EE1630;
155 | }
156 |
157 | .list{
158 | background-color:#f8bf9b;
159 | position:relative;
160 | }
161 | .list:nth-child(2n){
162 | position: relative;
163 | background-color:#f37e9b;
164 | }
165 |
166 | .list:before {
167 | content: '';
168 | position: absolute;
169 | right: 0;
170 | bottom: 0;
171 | left: 0;
172 | height: 21px;
173 | overflow: hidden;
174 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #25222B, 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 17px 0 -6px #25222B, 0 17px 2px -6px rgba(0, 0, 0, 0.2);
175 | }
176 |
177 | .list li {
178 | position: relative;
179 | padding: 10px 20px;
180 | border-bottom: solid 1px #2C2933;
181 | list-style: none;
182 | overflow: hidden;
183 | color:black;
184 | }
185 |
186 | .list li:hover .destroy {
187 | display: inline-block;
188 | }
189 |
190 | .todo span {
191 | display: block;
192 | margin-left: 30px;
193 | font-size: 23px;
194 | transition: color 0.3s ease-out;
195 | }
196 |
197 | .completed .todo span {
198 | text-decoration: line-through;
199 | color:#a19a9ae0;
200 | }
201 |
202 | .toggle {
203 | position: absolute;
204 | top: 14px;
205 | left: 14px;
206 | -webkit-appearance: none;
207 | appearance: none;
208 | outline: none;
209 | }
210 |
211 | .toggle:after {
212 | content: "";
213 | position: absolute;
214 | transform: rotateZ(-42deg);
215 | animation: checkOut .0s ease-out forwards;
216 | }
217 |
218 | .toggle:checked:after {
219 | animation: checkIn .3s ease-out forwards;
220 | }
221 |
222 | @keyframes checkIn {
223 | 0% {
224 | width: 24px;
225 | height: 24px;
226 | background: #3D3747;
227 | border-radius: 20px;
228 | box-shadow: inset 0 0 3px #2B2831;
229 | }
230 | 100% {
231 | height: 14px;
232 | width: 20px;
233 | background: transparent;
234 | border-radius: 0;
235 | border-left: solid 4px #7ED180;
236 | border-bottom: solid 4px #7ED180;
237 | }
238 | }
239 |
240 | @keyframes checkOut {
241 | 0% {
242 | height: 14px;
243 | width: 20px;
244 | background: transparent;
245 | border-radius: 0;
246 | border-left: solid 4px #7ED180;
247 | border-bottom: solid 4px #7ED180;
248 | }
249 | 100% {
250 | width: 24px;
251 | height: 24px;
252 | background: #3D3747;
253 | border-radius: 20px;
254 | box-shadow: inset 0 0 3px #2B2831;
255 | }
256 | }
257 |
258 | .edit {
259 | display: none;
260 | position: absolute;
261 | top: 0;
262 | left: 0;
263 | padding-left: 50px;
264 | width: 0%;
265 | height: 50px;
266 | opacity: 0;
267 | margin: auto 0;
268 | font-size: 23px;
269 | font-family: inherit;
270 | color: #fff;
271 | background: #7ED180;
272 | margin-bottom: 11px;
273 | border: none;
274 | outline: none;
275 | box-shadow: inset 0 0 40px rgba(0,0,0,0.1);
276 | z-index: 9;
277 | }
278 |
279 | .editing .edit {
280 | animation: anime .3s ease-out forwards;
281 | }
282 |
283 | @keyframes anime {
284 | 0% {
285 | opacity: 0;
286 | width: 0%;
287 | }
288 | 100% {
289 | opacity: 1;
290 | width: 100%;
291 | }
292 | }
293 |
294 | .destroy {
295 | position: absolute;
296 | display: block;
297 | top: 0px;
298 | right: 0;
299 | border: none;
300 | width: 50px;
301 | height: 50px;
302 | background: transparent;
303 | z-index: 2;
304 | outline: none;
305 | }
306 |
307 | .destroy:before,
308 | .destroy:after {
309 | content: '';
310 | position: absolute;
311 | top: 12.5px;
312 | left: 24px;
313 | width: 1px;
314 | height: 25px;
315 | background: #EE1630;
316 | opacity: 0;
317 | box-shadow: 0 0 4px #24203D;
318 | transition: all .5s ease-out;
319 | transform: rotateZ(45deg);
320 | }
321 |
322 | .destroy:after {
323 | left: 25px;
324 | transform: rotateZ(-45deg);
325 | }
326 |
327 | .list li:hover .destroy:before,
328 | .list li:hover .destroy:after {
329 | opacity: 1;
330 | height: 25px;
331 | }
332 |
333 | .destroy:hover:before,
334 | .destroy:hover:after {
335 | box-shadow: 0 0px 8px #ee0000;
336 | }
337 |
338 |
339 | .editing .edit {
340 | display: block;
341 | }
342 |
343 | .editing .todo {
344 | visibility: hidden;
345 | }
346 |
--------------------------------------------------------------------------------
/js/app.js:
--------------------------------------------------------------------------------
1 | ;(function(context) {
2 |
3 | 'use strict';
4 |
5 | var ENTER_KEY = 13;
6 | var ESC_KEY = 27;
7 |
8 | function App( localStorageKey ) {
9 |
10 | this.stores = new Stores(localStorageKey);
11 | this.currentId = 0;
12 | this.$insert = $('#js-insert');
13 | this.$toggleAll = $('#js-toggle-all');
14 | this.$bar = $('#js-bar');
15 | this.$list = $('#js-list');
16 | this.$clearCompleted = $('#js-clear-completed');
17 | this.$total = $('#js-total');
18 | this.$filters = $('#js-filters');
19 | this.addEventListeners();
20 | this.render();
21 |
22 | }
23 |
24 | App.fn = App.prototype;
25 |
26 | App.fn.addEventListeners = function() {
27 |
28 | $.on(this.$insert, 'keypress', this.onInsert.bind(this));
29 |
30 | $.on(this.$toggleAll, 'click', this.onToggleAll.bind(this));
31 | $.delegate(this.$list, '.toggle', 'click', this.onToggle.bind(this));
32 |
33 | $.delegate(this.$list, '.destroy', 'click', this.onDestroy.bind(this) );
34 |
35 | $.on(this.$clearCompleted, 'click', this.onClearCompleted.bind(this));
36 |
37 |
38 | $.delegate(this.$filters, '.button', 'click', this.onFilter.bind(this));
39 |
40 | $.delegate(this.$list, 'span', 'dblclick', this.onStartEditing.bind(this));
41 | $.delegate(this.$list, '.edit', 'keyup', this.onEditingCancel.bind(this));
42 | $.delegate(this.$list, '.edit', 'keypress', this.onEditingDone.bind(this));
43 | $.delegate(this.$list, '.edit', 'blur', this.onEditingLeave.bind(this) );
44 | };
45 |
46 | App.fn.onStartEditing = function(event) {
47 | var li = $.parent(event.target, 'li');
48 | var element = $('.edit', li);
49 | this.currentId = parseInt(li.dataset.id, 10);
50 | li.className += ' editing';
51 | element.value = event.target.innerHTML;
52 | element.focus();
53 | };
54 |
55 | App.fn.onEditingCancel = function(event) {
56 | if( event.keyCode === ESC_KEY ) {
57 | console.log('onEditingCancel', event.target);
58 | event.target.dataset.isCanceled = true;
59 | event.target.blur();
60 | }
61 | };
62 |
63 | App.fn.onEditingDone = function(event) {
64 | if( event.keyCode === ENTER_KEY ) {
65 | event.target.blur();
66 | }
67 | };
68 |
69 | App.fn.onEditingLeave = function(event) {
70 | console.log('onEditingLeave');
71 | var input = event.target;
72 | var id = this.getItemId( input );
73 | var text = input.value.trim();
74 | var li = this.getElementByDataId( id );
75 | if( input.value.trim() ) {
76 | var item = {
77 | id: id,
78 | text: text
79 | };
80 | this.stores.save(item,this.endEditing.bind(this, li, text));
81 | } else {
82 | if( input.dataset.isCanceled ) {
83 | this.endEditing( li );
84 | } else {
85 | this.destroy( id );
86 | }
87 | }
88 | };
89 |
90 | App.fn.endEditing = function( li, text ) {
91 | li.className = li.className.replace('editing', '');
92 | $('.edit', li).removeAttribute('data-is-canceled');
93 | if( text ) {
94 | $('span', li).innerHTML = text;
95 | }
96 | };
97 |
98 | App.fn.getItemId = function( element ) {
99 | var li = $.parent(element, 'li');
100 | return parseInt(li.dataset.id, 10);
101 | };
102 |
103 | App.fn.getElementByDataId = function( id ) {
104 | return $('[data-id="' + id + '"]');
105 | };
106 |
107 | App.fn.onInsert = function( event ) {
108 | var element = event.target;
109 | var text = element.value.trim();
110 | if( text && event.keyCode === ENTER_KEY ) {
111 | this.insert(text);
112 | element.value = '';
113 | }
114 | };
115 |
116 | App.fn.onToggleAll = function(event) {
117 | var checked = event.target.checked;
118 | var self = this;
119 |
120 | this.stores.findAll(function( items ) {
121 | $.each( items, function( item ) {
122 | item.completed = checked;
123 | self.stores.save( item, $.noop);
124 | });
125 | self.render();
126 | });
127 | };
128 |
129 | App.fn.onToggle = function(event) {
130 | var element = event.target;
131 | var id = this.getItemId( element );
132 | var item = {
133 | id: id,
134 | completed: element.checked
135 | };
136 | this.stores.save( item, function(item) {
137 | var li = this.getElementByDataId( item.id );
138 | li.className = item.completed ? 'completed' : '';
139 | $('.toggle', li).checked = item.completed;
140 | this.showControls();
141 | }.bind(this));
142 | };
143 |
144 | App.fn.onDestroy = function(event) {
145 | var id = this.getItemId( event.target );
146 | this.destroy( id );
147 | };
148 |
149 | App.fn.onClearCompleted = function(event) {
150 | var self = this;
151 | this.stores.findAll(function( items ) {
152 | items = items
153 | .filter(function( item ) {
154 | return item.completed;
155 | })
156 | .forEach(function( item ) {
157 | self.destroy( item.id );
158 | });
159 | });
160 | };
161 |
162 | App.fn.onFilter = function(event) {
163 | document.location.hash = event.target.getAttribute('href');
164 | this.render();
165 | };
166 |
167 | // Insert
168 | App.fn.insert = function( text ) {
169 | var item = {
170 | text: text,
171 | completed: false
172 | };
173 | this.stores.save(item, function( item ) {
174 | var element = this.nodeItem( item );
175 | this.$list.appendChild( element );
176 | this.showControls();
177 | }.bind(this));
178 | };
179 |
180 | // Destroy
181 | App.fn.destroy = function( id ) {
182 | this.stores.destroy( id, function() {
183 | var li = this.getElementByDataId( id );
184 | this.$list.removeChild(li);
185 | this.showControls();
186 | }.bind(this));
187 | };
188 |
189 | App.fn.filter = function() {
190 | var hash = document.location.hash;
191 | if( !hash ) return false;
192 | $.qsa( '.button', this.$filters )
193 | .each(function( button ) {
194 | if ( button.getAttribute('href') === hash ) {
195 | button.className = 'button selected';
196 | } else {
197 | button.className = button.className.replace('selected', '');
198 | }
199 | });
200 | hash = hash.split('#/')[1]
201 | return hash !== 'all' ? hash : false;
202 | };
203 |
204 | // Render
205 | App.fn.render = function() {
206 |
207 | var filter = this.filter();
208 |
209 | this.stores.findAll(function(items){
210 | if( filter ) {
211 | items = items
212 | .filter(function(item) {
213 | return item.completed === ( filter === 'completed' );
214 | });
215 | }
216 | var nodes = this.nodeItemMulti( items );
217 | this.$list.innerHTML = "";
218 | this.$list.appendChild(nodes);
219 | this.showControls();
220 |
221 | }.bind(this));
222 |
223 | };
224 |
225 |
226 | App.fn.showControls = function () {
227 | this.stores.findAll(function(items){
228 | this.showBarAndToggleAll( items );
229 | this.showTotalTasksLeft( items );
230 | this.showClearCompleted( items );
231 | }.bind(this));
232 | };
233 |
234 | App.fn.showBarAndToggleAll = function( items ) {
235 | var total = items.length;
236 | var completed = items.filter(function(item){
237 | return item.completed;
238 | });
239 | var value = total ? 'block' : 'none';
240 | this.$toggleAll.style.display = value;
241 | this.$toggleAll.checked = total === completed.length;
242 | this.$bar.style.display = value;
243 | };
244 |
245 | App.fn.showTotalTasksLeft = function(items) {
246 | items = items
247 | .filter(function( item ) {
248 | return !item.completed;
249 | });
250 | var len = items.length;
251 | var text = [len,' item',$.pluralization( len ),' left'].join('');
252 | this.$total.innerHTML = text;
253 | };
254 |
255 | App.fn.showClearCompleted = function(items) {
256 | var some = items
257 | .some(function( item ) {
258 | return item.completed;
259 | });
260 | this.$clearCompleted.style.display = some ? 'inline-block' : 'none';
261 | };
262 |
263 | App.fn.nodeItemMulti = function ( items ) {
264 | var fragment = document.createDocumentFragment();
265 | $.each( items, function( item ) {
266 | fragment.appendChild( this.nodeItem(item) );
267 | }.bind(this));
268 | return fragment;
269 | };
270 |
271 | App.fn.nodeItem = function( item ) {
272 | var li = document.createElement('li');
273 | var div = document.createElement('div');
274 | var toggle = document.createElement('input');
275 | var span = document.createElement('span');
276 | var destroy = document.createElement('button');
277 | var edit = document.createElement('input');
278 |
279 | li.setAttribute('data-id', item.id );
280 |
281 | if ( item.completed ) {
282 | li.className = 'completed';
283 | }
284 |
285 | div.className = 'todo';
286 |
287 | toggle.setAttribute('type', 'checkbox');
288 | toggle.className = 'toggle';
289 | toggle.checked = item.completed;
290 |
291 | span.appendChild( document.createTextNode(item.text) );
292 |
293 | destroy.className = 'destroy';
294 | // destroy.appendChild( document.createTextNode('X') );
295 |
296 | edit.setAttribute('type', 'text');
297 | edit.className = 'edit';
298 |
299 |
300 | div.appendChild(toggle);
301 | div.appendChild(span);
302 | div.appendChild(destroy);
303 |
304 | li.appendChild(div);
305 | li.appendChild(edit);
306 |
307 | return li;
308 | }
309 |
310 | // Initialization on Dom Ready
311 | window.addEventListener('DOMContentLoaded', function() {
312 | var app = new App('todo');
313 | });
314 |
315 | })(this);
316 |
317 |
--------------------------------------------------------------------------------