├── README.md
├── index.html
├── node_modules
├── todomvc-app-css
│ ├── index.css
│ ├── package.json
│ └── readme.md
└── todomvc-common
│ ├── base.css
│ ├── base.js
│ ├── package.json
│ └── readme.md
├── package.json
└── src
├── .jshintrc
├── app.js
├── controller.js
├── helpers.js
├── model.js
├── store.js
├── template.js
└── view.js
/README.md:
--------------------------------------------------------------------------------
1 | # Vanilla ES6 (ES2015) • [TodoMVC](http://todomvc.com)
2 |
3 | This is a rewrite of the incoming ES2015 app for TodoMVC that works out of the box in Chrome 49+ without the need for a transpiler.
4 |
5 | Full ES2015 support in a browser would allow Luke Edwards [original](https://github.com/tastejs/todomvc/pull/1515) ES2015 app to run without any modifications needed.
6 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | ES6 • TodoMVC
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
21 |
22 |
31 |
32 |
33 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/node_modules/todomvc-app-css/index.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | margin: 0;
4 | padding: 0;
5 | }
6 |
7 | button {
8 | margin: 0;
9 | padding: 0;
10 | border: 0;
11 | background: none;
12 | font-size: 100%;
13 | vertical-align: baseline;
14 | font-family: inherit;
15 | font-weight: inherit;
16 | color: inherit;
17 | -webkit-appearance: none;
18 | appearance: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-font-smoothing: antialiased;
21 | font-smoothing: antialiased;
22 | }
23 |
24 | body {
25 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
26 | line-height: 1.4em;
27 | background: #f5f5f5;
28 | color: #4d4d4d;
29 | min-width: 230px;
30 | max-width: 550px;
31 | margin: 0 auto;
32 | -webkit-font-smoothing: antialiased;
33 | -moz-font-smoothing: antialiased;
34 | font-smoothing: antialiased;
35 | font-weight: 300;
36 | }
37 |
38 | button,
39 | input[type="checkbox"] {
40 | outline: none;
41 | }
42 |
43 | .hidden {
44 | display: none;
45 | }
46 |
47 | .todoapp {
48 | background: #fff;
49 | margin: 130px 0 40px 0;
50 | position: relative;
51 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
52 | 0 25px 50px 0 rgba(0, 0, 0, 0.1);
53 | }
54 |
55 | .todoapp input::-webkit-input-placeholder {
56 | font-style: italic;
57 | font-weight: 300;
58 | color: #e6e6e6;
59 | }
60 |
61 | .todoapp input::-moz-placeholder {
62 | font-style: italic;
63 | font-weight: 300;
64 | color: #e6e6e6;
65 | }
66 |
67 | .todoapp input::input-placeholder {
68 | font-style: italic;
69 | font-weight: 300;
70 | color: #e6e6e6;
71 | }
72 |
73 | .todoapp h1 {
74 | position: absolute;
75 | top: -155px;
76 | width: 100%;
77 | font-size: 100px;
78 | font-weight: 100;
79 | text-align: center;
80 | color: rgba(175, 47, 47, 0.15);
81 | -webkit-text-rendering: optimizeLegibility;
82 | -moz-text-rendering: optimizeLegibility;
83 | text-rendering: optimizeLegibility;
84 | }
85 |
86 | .new-todo,
87 | .edit {
88 | position: relative;
89 | margin: 0;
90 | width: 100%;
91 | font-size: 24px;
92 | font-family: inherit;
93 | font-weight: inherit;
94 | line-height: 1.4em;
95 | border: 0;
96 | outline: none;
97 | color: inherit;
98 | padding: 6px;
99 | border: 1px solid #999;
100 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
101 | box-sizing: border-box;
102 | -webkit-font-smoothing: antialiased;
103 | -moz-font-smoothing: antialiased;
104 | font-smoothing: antialiased;
105 | }
106 |
107 | .new-todo {
108 | padding: 16px 16px 16px 60px;
109 | border: none;
110 | background: rgba(0, 0, 0, 0.003);
111 | box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
112 | }
113 |
114 | .main {
115 | position: relative;
116 | z-index: 2;
117 | border-top: 1px solid #e6e6e6;
118 | }
119 |
120 | label[for='toggle-all'] {
121 | display: none;
122 | }
123 |
124 | .toggle-all {
125 | position: absolute;
126 | top: -55px;
127 | left: -12px;
128 | width: 60px;
129 | height: 34px;
130 | text-align: center;
131 | border: none; /* Mobile Safari */
132 | }
133 |
134 | .toggle-all:before {
135 | content: '❯';
136 | font-size: 22px;
137 | color: #e6e6e6;
138 | padding: 10px 27px 10px 27px;
139 | }
140 |
141 | .toggle-all:checked:before {
142 | color: #737373;
143 | }
144 |
145 | .todo-list {
146 | margin: 0;
147 | padding: 0;
148 | list-style: none;
149 | }
150 |
151 | .todo-list li {
152 | position: relative;
153 | font-size: 24px;
154 | border-bottom: 1px solid #ededed;
155 | }
156 |
157 | .todo-list li:last-child {
158 | border-bottom: none;
159 | }
160 |
161 | .todo-list li.editing {
162 | border-bottom: none;
163 | padding: 0;
164 | }
165 |
166 | .todo-list li.editing .edit {
167 | display: block;
168 | width: 506px;
169 | padding: 13px 17px 12px 17px;
170 | margin: 0 0 0 43px;
171 | }
172 |
173 | .todo-list li.editing .view {
174 | display: none;
175 | }
176 |
177 | .todo-list li .toggle {
178 | text-align: center;
179 | width: 40px;
180 | /* auto, since non-WebKit browsers doesn't support input styling */
181 | height: auto;
182 | position: absolute;
183 | top: 0;
184 | bottom: 0;
185 | margin: auto 0;
186 | border: none; /* Mobile Safari */
187 | -webkit-appearance: none;
188 | appearance: none;
189 | }
190 |
191 | .todo-list li .toggle:after {
192 | content: url('data:image/svg+xml;utf8, ');
193 | }
194 |
195 | .todo-list li .toggle:checked:after {
196 | content: url('data:image/svg+xml;utf8, ');
197 | }
198 |
199 | .todo-list li label {
200 | white-space: pre;
201 | word-break: break-word;
202 | padding: 15px 60px 15px 15px;
203 | margin-left: 45px;
204 | display: block;
205 | line-height: 1.2;
206 | transition: color 0.4s;
207 | }
208 |
209 | .todo-list li.completed label {
210 | color: #d9d9d9;
211 | text-decoration: line-through;
212 | }
213 |
214 | .todo-list li .destroy {
215 | display: none;
216 | position: absolute;
217 | top: 0;
218 | right: 10px;
219 | bottom: 0;
220 | width: 40px;
221 | height: 40px;
222 | margin: auto 0;
223 | font-size: 30px;
224 | color: #cc9a9a;
225 | margin-bottom: 11px;
226 | transition: color 0.2s ease-out;
227 | }
228 |
229 | .todo-list li .destroy:hover {
230 | color: #af5b5e;
231 | }
232 |
233 | .todo-list li .destroy:after {
234 | content: '×';
235 | }
236 |
237 | .todo-list li:hover .destroy {
238 | display: block;
239 | }
240 |
241 | .todo-list li .edit {
242 | display: none;
243 | }
244 |
245 | .todo-list li.editing:last-child {
246 | margin-bottom: -1px;
247 | }
248 |
249 | .footer {
250 | color: #777;
251 | padding: 10px 15px;
252 | height: 20px;
253 | text-align: center;
254 | border-top: 1px solid #e6e6e6;
255 | }
256 |
257 | .footer:before {
258 | content: '';
259 | position: absolute;
260 | right: 0;
261 | bottom: 0;
262 | left: 0;
263 | height: 50px;
264 | overflow: hidden;
265 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
266 | 0 8px 0 -3px #f6f6f6,
267 | 0 9px 1px -3px rgba(0, 0, 0, 0.2),
268 | 0 16px 0 -6px #f6f6f6,
269 | 0 17px 2px -6px rgba(0, 0, 0, 0.2);
270 | }
271 |
272 | .todo-count {
273 | float: left;
274 | text-align: left;
275 | }
276 |
277 | .todo-count strong {
278 | font-weight: 300;
279 | }
280 |
281 | .filters {
282 | margin: 0;
283 | padding: 0;
284 | list-style: none;
285 | position: absolute;
286 | right: 0;
287 | left: 0;
288 | }
289 |
290 | .filters li {
291 | display: inline;
292 | }
293 |
294 | .filters li a {
295 | color: inherit;
296 | margin: 3px;
297 | padding: 3px 7px;
298 | text-decoration: none;
299 | border: 1px solid transparent;
300 | border-radius: 3px;
301 | }
302 |
303 | .filters li a.selected,
304 | .filters li a:hover {
305 | border-color: rgba(175, 47, 47, 0.1);
306 | }
307 |
308 | .filters li a.selected {
309 | border-color: rgba(175, 47, 47, 0.2);
310 | }
311 |
312 | .clear-completed,
313 | html .clear-completed:active {
314 | float: right;
315 | position: relative;
316 | line-height: 20px;
317 | text-decoration: none;
318 | cursor: pointer;
319 | }
320 |
321 | .clear-completed:hover {
322 | text-decoration: underline;
323 | }
324 |
325 | .info {
326 | margin: 65px auto 0;
327 | color: #bfbfbf;
328 | font-size: 10px;
329 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
330 | text-align: center;
331 | }
332 |
333 | .info p {
334 | line-height: 1;
335 | }
336 |
337 | .info a {
338 | color: inherit;
339 | text-decoration: none;
340 | font-weight: 400;
341 | }
342 |
343 | .info a:hover {
344 | text-decoration: underline;
345 | }
346 |
347 | /*
348 | Hack to remove background from Mobile Safari.
349 | Can't use it globally since it destroys checkboxes in Firefox
350 | */
351 | @media screen and (-webkit-min-device-pixel-ratio:0) {
352 | .toggle-all,
353 | .todo-list li .toggle {
354 | background: none;
355 | }
356 |
357 | .todo-list li .toggle {
358 | height: 40px;
359 | }
360 |
361 | .toggle-all {
362 | -webkit-transform: rotate(90deg);
363 | transform: rotate(90deg);
364 | -webkit-appearance: none;
365 | appearance: none;
366 | }
367 | }
368 |
369 | @media (max-width: 430px) {
370 | .footer {
371 | height: 50px;
372 | }
373 |
374 | .filters {
375 | bottom: 10px;
376 | }
377 | }
378 |
--------------------------------------------------------------------------------
/node_modules/todomvc-app-css/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todomvc-app-css",
3 | "style": "index.css",
4 | "version": "2.0.2",
5 | "description": "CSS for TodoMVC apps",
6 | "license": "CC-BY-4.0",
7 | "repository": {
8 | "type": "git",
9 | "url": "git+https://github.com/tastejs/todomvc-app-css.git"
10 | },
11 | "author": {
12 | "name": "Sindre Sorhus",
13 | "email": "sindresorhus@gmail.com",
14 | "url": "sindresorhus.com"
15 | },
16 | "files": [
17 | "index.css"
18 | ],
19 | "keywords": [
20 | "todomvc",
21 | "tastejs",
22 | "app",
23 | "todo",
24 | "template",
25 | "css",
26 | "style",
27 | "stylesheet"
28 | ],
29 | "gitHead": "fd1e83f8f53a0b85537415365f2a6301bbb4199f",
30 | "bugs": {
31 | "url": "https://github.com/tastejs/todomvc-app-css/issues"
32 | },
33 | "homepage": "https://github.com/tastejs/todomvc-app-css",
34 | "_id": "todomvc-app-css@2.0.2",
35 | "scripts": {},
36 | "_shasum": "31ff679dc3a409b260bc7e0a6eaca5f8757a89a1",
37 | "_from": "todomvc-app-css@>=2.0.1 <3.0.0",
38 | "_npmVersion": "2.14.7",
39 | "_nodeVersion": "4.2.1",
40 | "_npmUser": {
41 | "name": "sindresorhus",
42 | "email": "sindresorhus@gmail.com"
43 | },
44 | "dist": {
45 | "shasum": "31ff679dc3a409b260bc7e0a6eaca5f8757a89a1",
46 | "tarball": "http://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.2.tgz"
47 | },
48 | "maintainers": [
49 | {
50 | "name": "sindresorhus",
51 | "email": "sindresorhus@gmail.com"
52 | },
53 | {
54 | "name": "addyosmani",
55 | "email": "addyosmani@gmail.com"
56 | },
57 | {
58 | "name": "passy",
59 | "email": "phartig@rdrei.net"
60 | },
61 | {
62 | "name": "stephenplusplus",
63 | "email": "sawchuk@gmail.com"
64 | }
65 | ],
66 | "directories": {},
67 | "_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.0.2.tgz",
68 | "readme": "ERROR: No README data found!"
69 | }
70 |
--------------------------------------------------------------------------------
/node_modules/todomvc-app-css/readme.md:
--------------------------------------------------------------------------------
1 | # todomvc-app-css
2 |
3 | > CSS for TodoMVC apps
4 |
5 | 
6 |
7 |
8 | ## Install
9 |
10 |
11 | ```
12 | $ npm install --save todomvc-app-css
13 | ```
14 |
15 |
16 | ## Getting started
17 |
18 | ```html
19 |
20 | ```
21 |
22 | See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template).
23 |
24 |
25 |
26 | ## License
27 |
28 | This work by Sindre Sorhus is licensed under a Creative Commons Attribution 4.0 International License .
29 |
--------------------------------------------------------------------------------
/node_modules/todomvc-common/base.css:
--------------------------------------------------------------------------------
1 | hr {
2 | margin: 20px 0;
3 | border: 0;
4 | border-top: 1px dashed #c5c5c5;
5 | border-bottom: 1px dashed #f7f7f7;
6 | }
7 |
8 | .learn a {
9 | font-weight: normal;
10 | text-decoration: none;
11 | color: #b83f45;
12 | }
13 |
14 | .learn a:hover {
15 | text-decoration: underline;
16 | color: #787e7e;
17 | }
18 |
19 | .learn h3,
20 | .learn h4,
21 | .learn h5 {
22 | margin: 10px 0;
23 | font-weight: 500;
24 | line-height: 1.2;
25 | color: #000;
26 | }
27 |
28 | .learn h3 {
29 | font-size: 24px;
30 | }
31 |
32 | .learn h4 {
33 | font-size: 18px;
34 | }
35 |
36 | .learn h5 {
37 | margin-bottom: 0;
38 | font-size: 14px;
39 | }
40 |
41 | .learn ul {
42 | padding: 0;
43 | margin: 0 0 30px 25px;
44 | }
45 |
46 | .learn li {
47 | line-height: 20px;
48 | }
49 |
50 | .learn p {
51 | font-size: 15px;
52 | font-weight: 300;
53 | line-height: 1.3;
54 | margin-top: 0;
55 | margin-bottom: 0;
56 | }
57 |
58 | #issue-count {
59 | display: none;
60 | }
61 |
62 | .quote {
63 | border: none;
64 | margin: 20px 0 60px 0;
65 | }
66 |
67 | .quote p {
68 | font-style: italic;
69 | }
70 |
71 | .quote p:before {
72 | content: '“';
73 | font-size: 50px;
74 | opacity: .15;
75 | position: absolute;
76 | top: -20px;
77 | left: 3px;
78 | }
79 |
80 | .quote p:after {
81 | content: '”';
82 | font-size: 50px;
83 | opacity: .15;
84 | position: absolute;
85 | bottom: -42px;
86 | right: 3px;
87 | }
88 |
89 | .quote footer {
90 | position: absolute;
91 | bottom: -40px;
92 | right: 0;
93 | }
94 |
95 | .quote footer img {
96 | border-radius: 3px;
97 | }
98 |
99 | .quote footer a {
100 | margin-left: 5px;
101 | vertical-align: middle;
102 | }
103 |
104 | .speech-bubble {
105 | position: relative;
106 | padding: 10px;
107 | background: rgba(0, 0, 0, .04);
108 | border-radius: 5px;
109 | }
110 |
111 | .speech-bubble:after {
112 | content: '';
113 | position: absolute;
114 | top: 100%;
115 | right: 30px;
116 | border: 13px solid transparent;
117 | border-top-color: rgba(0, 0, 0, .04);
118 | }
119 |
120 | .learn-bar > .learn {
121 | position: absolute;
122 | width: 272px;
123 | top: 8px;
124 | left: -300px;
125 | padding: 10px;
126 | border-radius: 5px;
127 | background-color: rgba(255, 255, 255, .6);
128 | transition-property: left;
129 | transition-duration: 500ms;
130 | }
131 |
132 | @media (min-width: 899px) {
133 | .learn-bar {
134 | width: auto;
135 | padding-left: 300px;
136 | }
137 |
138 | .learn-bar > .learn {
139 | left: 8px;
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/node_modules/todomvc-common/base.js:
--------------------------------------------------------------------------------
1 | /* global _ */
2 | (function () {
3 | 'use strict';
4 |
5 | /* jshint ignore:start */
6 | // Underscore's Template Module
7 | // Courtesy of underscorejs.org
8 | var _ = (function (_) {
9 | _.defaults = function (object) {
10 | if (!object) {
11 | return object;
12 | }
13 | for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
14 | var iterable = arguments[argsIndex];
15 | if (iterable) {
16 | for (var key in iterable) {
17 | if (object[key] == null) {
18 | object[key] = iterable[key];
19 | }
20 | }
21 | }
22 | }
23 | return object;
24 | }
25 |
26 | // By default, Underscore uses ERB-style template delimiters, change the
27 | // following template settings to use alternative delimiters.
28 | _.templateSettings = {
29 | evaluate : /<%([\s\S]+?)%>/g,
30 | interpolate : /<%=([\s\S]+?)%>/g,
31 | escape : /<%-([\s\S]+?)%>/g
32 | };
33 |
34 | // When customizing `templateSettings`, if you don't want to define an
35 | // interpolation, evaluation or escaping regex, we need one that is
36 | // guaranteed not to match.
37 | var noMatch = /(.)^/;
38 |
39 | // Certain characters need to be escaped so that they can be put into a
40 | // string literal.
41 | var escapes = {
42 | "'": "'",
43 | '\\': '\\',
44 | '\r': 'r',
45 | '\n': 'n',
46 | '\t': 't',
47 | '\u2028': 'u2028',
48 | '\u2029': 'u2029'
49 | };
50 |
51 | var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
52 |
53 | // JavaScript micro-templating, similar to John Resig's implementation.
54 | // Underscore templating handles arbitrary delimiters, preserves whitespace,
55 | // and correctly escapes quotes within interpolated code.
56 | _.template = function(text, data, settings) {
57 | var render;
58 | settings = _.defaults({}, settings, _.templateSettings);
59 |
60 | // Combine delimiters into one regular expression via alternation.
61 | var matcher = new RegExp([
62 | (settings.escape || noMatch).source,
63 | (settings.interpolate || noMatch).source,
64 | (settings.evaluate || noMatch).source
65 | ].join('|') + '|$', 'g');
66 |
67 | // Compile the template source, escaping string literals appropriately.
68 | var index = 0;
69 | var source = "__p+='";
70 | text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
71 | source += text.slice(index, offset)
72 | .replace(escaper, function(match) { return '\\' + escapes[match]; });
73 |
74 | if (escape) {
75 | source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
76 | }
77 | if (interpolate) {
78 | source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
79 | }
80 | if (evaluate) {
81 | source += "';\n" + evaluate + "\n__p+='";
82 | }
83 | index = offset + match.length;
84 | return match;
85 | });
86 | source += "';\n";
87 |
88 | // If a variable is not specified, place data values in local scope.
89 | if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
90 |
91 | source = "var __t,__p='',__j=Array.prototype.join," +
92 | "print=function(){__p+=__j.call(arguments,'');};\n" +
93 | source + "return __p;\n";
94 |
95 | try {
96 | render = new Function(settings.variable || 'obj', '_', source);
97 | } catch (e) {
98 | e.source = source;
99 | throw e;
100 | }
101 |
102 | if (data) return render(data, _);
103 | var template = function(data) {
104 | return render.call(this, data, _);
105 | };
106 |
107 | // Provide the compiled function source as a convenience for precompilation.
108 | template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
109 |
110 | return template;
111 | };
112 |
113 | return _;
114 | })({});
115 |
116 | if (location.hostname === 'todomvc.com') {
117 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
118 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
119 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
120 | })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
121 | ga('create', 'UA-31081062-1', 'auto');
122 | ga('send', 'pageview');
123 | }
124 | /* jshint ignore:end */
125 |
126 | function redirect() {
127 | if (location.hostname === 'tastejs.github.io') {
128 | location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
129 | }
130 | }
131 |
132 | function findRoot() {
133 | var base = location.href.indexOf('examples/');
134 | return location.href.substr(0, base);
135 | }
136 |
137 | function getFile(file, callback) {
138 | if (!location.host) {
139 | return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
140 | }
141 |
142 | var xhr = new XMLHttpRequest();
143 |
144 | xhr.open('GET', findRoot() + file, true);
145 | xhr.send();
146 |
147 | xhr.onload = function () {
148 | if (xhr.status === 200 && callback) {
149 | callback(xhr.responseText);
150 | }
151 | };
152 | }
153 |
154 | function Learn(learnJSON, config) {
155 | if (!(this instanceof Learn)) {
156 | return new Learn(learnJSON, config);
157 | }
158 |
159 | var template, framework;
160 |
161 | if (typeof learnJSON !== 'object') {
162 | try {
163 | learnJSON = JSON.parse(learnJSON);
164 | } catch (e) {
165 | return;
166 | }
167 | }
168 |
169 | if (config) {
170 | template = config.template;
171 | framework = config.framework;
172 | }
173 |
174 | if (!template && learnJSON.templates) {
175 | template = learnJSON.templates.todomvc;
176 | }
177 |
178 | if (!framework && document.querySelector('[data-framework]')) {
179 | framework = document.querySelector('[data-framework]').dataset.framework;
180 | }
181 |
182 | this.template = template;
183 |
184 | if (learnJSON.backend) {
185 | this.frameworkJSON = learnJSON.backend;
186 | this.frameworkJSON.issueLabel = framework;
187 | this.append({
188 | backend: true
189 | });
190 | } else if (learnJSON[framework]) {
191 | this.frameworkJSON = learnJSON[framework];
192 | this.frameworkJSON.issueLabel = framework;
193 | this.append();
194 | }
195 |
196 | this.fetchIssueCount();
197 | }
198 |
199 | Learn.prototype.append = function (opts) {
200 | var aside = document.createElement('aside');
201 | aside.innerHTML = _.template(this.template, this.frameworkJSON);
202 | aside.className = 'learn';
203 |
204 | if (opts && opts.backend) {
205 | // Remove demo link
206 | var sourceLinks = aside.querySelector('.source-links');
207 | var heading = sourceLinks.firstElementChild;
208 | var sourceLink = sourceLinks.lastElementChild;
209 | // Correct link path
210 | var href = sourceLink.getAttribute('href');
211 | sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
212 | sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
213 | } else {
214 | // Localize demo links
215 | var demoLinks = aside.querySelectorAll('.demo-link');
216 | Array.prototype.forEach.call(demoLinks, function (demoLink) {
217 | if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
218 | demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
219 | }
220 | });
221 | }
222 |
223 | document.body.className = (document.body.className + ' learn-bar').trim();
224 | document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
225 | };
226 |
227 | Learn.prototype.fetchIssueCount = function () {
228 | var issueLink = document.getElementById('issue-count-link');
229 | if (issueLink) {
230 | var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
231 | var xhr = new XMLHttpRequest();
232 | xhr.open('GET', url, true);
233 | xhr.onload = function (e) {
234 | var parsedResponse = JSON.parse(e.target.responseText);
235 | if (parsedResponse instanceof Array) {
236 | var count = parsedResponse.length;
237 | if (count !== 0) {
238 | issueLink.innerHTML = 'This app has ' + count + ' open issues';
239 | document.getElementById('issue-count').style.display = 'inline';
240 | }
241 | }
242 | };
243 | xhr.send();
244 | }
245 | };
246 |
247 | redirect();
248 | getFile('learn.json', Learn);
249 | })();
250 |
--------------------------------------------------------------------------------
/node_modules/todomvc-common/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "todomvc-common",
3 | "version": "1.0.2",
4 | "description": "Common TodoMVC utilities used by our apps",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "git+https://github.com/tastejs/todomvc-common.git"
9 | },
10 | "author": {
11 | "name": "TasteJS"
12 | },
13 | "main": "base.js",
14 | "files": [
15 | "base.js",
16 | "base.css"
17 | ],
18 | "keywords": [
19 | "todomvc",
20 | "tastejs",
21 | "util",
22 | "utilities"
23 | ],
24 | "gitHead": "e82d0c79e01687ce7407df786cc784ad82166cb3",
25 | "bugs": {
26 | "url": "https://github.com/tastejs/todomvc-common/issues"
27 | },
28 | "homepage": "https://github.com/tastejs/todomvc-common",
29 | "_id": "todomvc-common@1.0.2",
30 | "scripts": {},
31 | "_shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
32 | "_from": "todomvc-common@>=1.0.2 <2.0.0",
33 | "_npmVersion": "2.7.4",
34 | "_nodeVersion": "0.12.2",
35 | "_npmUser": {
36 | "name": "sindresorhus",
37 | "email": "sindresorhus@gmail.com"
38 | },
39 | "dist": {
40 | "shasum": "eb3ab61281ac74809f5869c917c7b08bc84234e0",
41 | "tarball": "http://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz"
42 | },
43 | "maintainers": [
44 | {
45 | "name": "sindresorhus",
46 | "email": "sindresorhus@gmail.com"
47 | },
48 | {
49 | "name": "addyosmani",
50 | "email": "addyosmani@gmail.com"
51 | },
52 | {
53 | "name": "passy",
54 | "email": "phartig@rdrei.net"
55 | },
56 | {
57 | "name": "stephenplusplus",
58 | "email": "sawchuk@gmail.com"
59 | }
60 | ],
61 | "directories": {},
62 | "_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.2.tgz",
63 | "readme": "ERROR: No README data found!"
64 | }
65 |
--------------------------------------------------------------------------------
/node_modules/todomvc-common/readme.md:
--------------------------------------------------------------------------------
1 | # todomvc-common
2 |
3 | > Common TodoMVC utilities used by our apps
4 |
5 |
6 | ## Install
7 |
8 | ```
9 | $ npm install --save todomvc-common
10 | ```
11 |
12 |
13 | ## License
14 |
15 | MIT © [TasteJS](http://tastejs.com)
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "dependencies": {
4 | "todomvc-app-css": "^2.0.1",
5 | "todomvc-common": "^1.0.2"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/.jshintrc:
--------------------------------------------------------------------------------
1 | {
2 | "esnext": true
3 | }
4 |
--------------------------------------------------------------------------------
/src/app.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | let todo;
4 | const setView = () => todo.controller.setView(document.location.hash);
5 |
6 | class Todo {
7 | /**
8 | * Init new Todo List
9 | * @param {string} The name of your list
10 | */
11 | constructor(name) {
12 | this.storage = new Store(name);
13 | this.model = new Model(this.storage);
14 |
15 | this.template = new Template();
16 | this.view = new View(this.template);
17 |
18 | this.controller = new Controller(this.model, this.view);
19 | }
20 | }
21 |
22 | $on(window, 'load', () => {
23 | todo = new Todo('todos-vanillajs');
24 | setView();
25 | });
26 |
27 | $on(window, 'hashchange', setView);
28 |
--------------------------------------------------------------------------------
/src/controller.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class Controller {
4 | /**
5 | * Take a model & view, then act as controller between them
6 | * @param {object} model The model instance
7 | * @param {object} view The view instance
8 | */
9 | constructor(model, view) {
10 | this.model = model;
11 | this.view = view;
12 |
13 | this.view.bind('newTodo', title => this.addItem(title));
14 | this.view.bind('itemEdit', item => this.editItem(item.id));
15 | this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title));
16 | this.view.bind('itemEditCancel', item => this.editItemCancel(item.id));
17 | this.view.bind('itemRemove', item => this.removeItem(item.id));
18 | this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed));
19 | this.view.bind('removeCompleted', () => this.removeCompletedItems());
20 | this.view.bind('toggleAll', status => this.toggleAll(status.completed));
21 | }
22 |
23 | /**
24 | * Load & Initialize the view
25 | * @param {string} '' | 'active' | 'completed'
26 | */
27 | setView(hash){
28 | let route = hash.split('/')[1];
29 | let page = route || '';
30 | this._updateFilter(page);
31 | }
32 |
33 | /**
34 | * Event fires on load. Gets all items & displays them
35 | */
36 | showAll(){
37 | this.model.read(data => this.view.render('showEntries', data));
38 | }
39 |
40 | /**
41 | * Renders all active tasks
42 | */
43 | showActive(){
44 | this.model.read({completed: false}, data => this.view.render('showEntries', data));
45 | }
46 |
47 | /**
48 | * Renders all completed tasks
49 | */
50 | showCompleted(){
51 | this.model.read({completed: true}, data => this.view.render('showEntries', data));
52 | }
53 |
54 | /**
55 | * An event to fire whenever you want to add an item. Simply pass in the event
56 | * object and it'll handle the DOM insertion and saving of the new item.
57 | */
58 | addItem(title){
59 | if (title.trim() === '') {
60 | return;
61 | }
62 |
63 | this.model.create(title, () => {
64 | this.view.render('clearNewTodo');
65 | this._filter(true);
66 | });
67 | }
68 |
69 | /*
70 | * Triggers the item editing mode.
71 | */
72 | editItem(id){
73 | this.model.read(id, data => {
74 | let title = data[0].title;
75 | this.view.render('editItem', {id, title});
76 | });
77 | }
78 |
79 | /*
80 | * Finishes the item editing mode successfully.
81 | */
82 | editItemSave(id, title){
83 | title = title.trim();
84 |
85 | if (title.length !== 0) {
86 | this.model.update(id, {title}, () => {
87 | this.view.render('editItemDone', {id, title});
88 | });
89 | } else {
90 | this.removeItem(id);
91 | }
92 | }
93 |
94 | /*
95 | * Cancels the item editing mode.
96 | */
97 | editItemCancel(id){
98 | this.model.read(id, data => {
99 | let title = data[0].title;
100 | this.view.render('editItemDone', {id, title});
101 | });
102 | }
103 |
104 | /**
105 | * Find the DOM element with given ID,
106 | * Then remove it from DOM & Storage
107 | */
108 | removeItem(id){
109 | this.model.remove(id, () => this.view.render('removeItem', id));
110 | this._filter();
111 | }
112 |
113 | /**
114 | * Will remove all completed items from the DOM and storage.
115 | */
116 | removeCompletedItems(){
117 | this.model.read({completed: true}, data => {
118 | for (let item of data) {
119 | this.removeItem(item.id);
120 | }
121 | });
122 |
123 | this._filter();
124 | }
125 |
126 | /**
127 | * Give it an ID of a model and a checkbox and it will update the item
128 | * in storage based on the checkbox's state.
129 | *
130 | * @param {number} id The ID of the element to complete or uncomplete
131 | * @param {object} checkbox The checkbox to check the state of complete
132 | * or not
133 | * @param {boolean|undefined} silent Prevent re-filtering the todo items
134 | */
135 | toggleComplete(id, completed, silent){
136 | this.model.update(id, {completed}, () => {
137 | this.view.render('elementComplete', {id, completed});
138 | });
139 |
140 | if (!silent) {
141 | this._filter();
142 | }
143 | }
144 |
145 | /**
146 | * Will toggle ALL checkboxes' on/off state and completeness of models.
147 | * Just pass in the event object.
148 | */
149 | toggleAll(completed){
150 | this.model.read({completed: !completed}, data => {
151 | for (let item of data) {
152 | this.toggleComplete(item.id, completed, true);
153 | }
154 | });
155 |
156 | this._filter();
157 | }
158 |
159 | /**
160 | * Updates the pieces of the page which change depending on the remaining
161 | * number of todos.
162 | */
163 | _updateCount(){
164 | this.model.getCount(todos => {
165 | const completed = todos.completed;
166 | const visible = completed > 0;
167 | const checked = completed === todos.total;
168 |
169 | this.view.render('updateElementCount', todos.active);
170 | this.view.render('clearCompletedButton', {completed, visible});
171 |
172 | this.view.render('toggleAll', {checked});
173 | this.view.render('contentBlockVisibility', {visible: todos.total > 0});
174 | });
175 | }
176 |
177 | /**
178 | * Re-filters the todo items, based on the active route.
179 | * @param {boolean|undefined} force forces a re-painting of todo items.
180 | */
181 | _filter(force){
182 | let active = this._activeRoute;
183 | const activeRoute = active.charAt(0).toUpperCase() + active.substr(1);
184 |
185 | // Update the elements on the page, which change with each completed todo
186 | this._updateCount();
187 |
188 | // If the last active route isn't "All", or we're switching routes, we
189 | // re-create the todo item elements, calling:
190 | // this.show[All|Active|Completed]()
191 | if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
192 | this['show' + activeRoute]();
193 | }
194 |
195 | this._lastActiveRoute = activeRoute;
196 | }
197 |
198 | /**
199 | * Simply updates the filter nav's selected states
200 | */
201 | _updateFilter(currentPage){
202 | // Store a reference to the active route, allowing us to re-filter todo
203 | // items as they are marked complete or incomplete.
204 | this._activeRoute = currentPage;
205 |
206 | if (currentPage === '') {
207 | this._activeRoute = 'All';
208 | }
209 |
210 | this._filter();
211 |
212 | this.view.render('setFilter', currentPage);
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/src/helpers.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | // Allow for looping on nodes by chaining:
5 | // qsa('.foo').forEach(function () {})
6 | NodeList.prototype.forEach = Array.prototype.forEach;
7 |
8 | // Get element(s) by CSS selector:
9 | function qs(selector, scope) {
10 | return (scope || document).querySelector(selector);
11 | }
12 |
13 | function qsa(selector, scope) {
14 | return (scope || document).querySelectorAll(selector);
15 | }
16 |
17 | // addEventListener wrapper:
18 | function $on(target, type, callback, useCapture) {
19 | target.addEventListener(type, callback, !!useCapture);
20 | }
21 |
22 | // Attach a handler to event for all elements that match the selector,
23 | // now or in the future, based on a root element
24 | function $delegate(target, selector, type, handler) {
25 | let dispatchEvent = event => {
26 | const targetElement = event.target;
27 | const potentialElements = qsa(selector, target);
28 | const hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
29 |
30 | if (hasMatch) {
31 | handler.call(targetElement, event);
32 | }
33 | };
34 |
35 | // https://developer.mozilla.org/en-US/docs/Web/Events/blur
36 | const useCapture = type === 'blur' || type === 'focus';
37 |
38 | $on(target, type, dispatchEvent, useCapture);
39 | }
40 |
41 | // Find the element's parent with the given tag name:
42 | // $parent(qs('a'), 'div')
43 | function $parent(element, tagName) {
44 | if (!element.parentNode) {
45 | return;
46 | }
47 |
48 | if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
49 | return element.parentNode;
50 | }
51 |
52 | return $parent(element.parentNode, tagName);
53 | }
54 |
--------------------------------------------------------------------------------
/src/model.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | /**
5 | * Creates a new Model instance and hooks up the storage.
6 | * @constructor
7 | * @param {object} storage A reference to the client side storage class
8 | */
9 | class Model {
10 | constructor(storage) {
11 | this.storage = storage;
12 | }
13 |
14 | /**
15 | * Creates a new todo model
16 | *
17 | * @param {string} [title] The title of the task
18 | * @param {function} [callback] The callback to fire after the model is created
19 | */
20 | create(title, callback){
21 | title = title || '';
22 |
23 | let newItem = {
24 | title: title.trim(),
25 | completed: false
26 | };
27 |
28 | this.storage.save(newItem, callback);
29 | }
30 |
31 | /**
32 | * Finds and returns a model in storage. If no query is given it'll simply
33 | * return everything. If you pass in a string or number it'll look that up as
34 | * the ID of the model to find. Lastly, you can pass it an object to match
35 | * against.
36 | *
37 | * @param {string|number|object} [query] A query to match models against
38 | * @param {function} [callback] The callback to fire after the model is found
39 | *
40 | * @example
41 | * model.read(1, func) // Will find the model with an ID of 1
42 | * model.read('1') // Same as above
43 | * //Below will find a model with foo equalling bar and hello equalling world.
44 | * model.read({ foo: 'bar', hello: 'world' })
45 | */
46 | read(query, callback){
47 | const queryType = typeof query;
48 |
49 | if (queryType === 'function') {
50 | callback = query;
51 | this.storage.findAll(callback);
52 | } else if (queryType === 'string' || queryType === 'number') {
53 | query = parseInt(query, 10);
54 | this.storage.find({id: query}, callback);
55 | } else {
56 | this.storage.find(query, callback);
57 | }
58 | }
59 |
60 | /**
61 | * Updates a model by giving it an ID, data to update, and a callback to fire when
62 | * the update is complete.
63 | *
64 | * @param {number} id The id of the model to update
65 | * @param {object} data The properties to update and their new value
66 | * @param {function} callback The callback to fire when the update is complete.
67 | */
68 | update(id, data, callback){
69 | this.storage.save(data, callback, id);
70 | }
71 |
72 | /**
73 | * Removes a model from storage
74 | *
75 | * @param {number} id The ID of the model to remove
76 | * @param {function} callback The callback to fire when the removal is complete.
77 | */
78 | remove(id, callback){
79 | this.storage.remove(id, callback);
80 | }
81 |
82 | /**
83 | * WARNING: Will remove ALL data from storage.
84 | *
85 | * @param {function} callback The callback to fire when the storage is wiped.
86 | */
87 | removeAll(callback){
88 | this.storage.drop(callback);
89 | }
90 |
91 | /**
92 | * Returns a count of all todos
93 | */
94 | getCount(callback){
95 | let todos = {
96 | active: 0,
97 | completed: 0,
98 | total: 0
99 | };
100 |
101 | this.storage.findAll(data => {
102 | for (let todo of data) {
103 | if (todo.completed) {
104 | todos.completed++;
105 | } else {
106 | todos.active++;
107 | }
108 |
109 | todos.total++;
110 | }
111 |
112 | if (callback) {
113 | callback(todos);
114 | }
115 | });
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/store.js:
--------------------------------------------------------------------------------
1 | /*jshint eqeqeq:false */
2 | 'use strict';
3 |
4 | /**
5 | * Creates a new client side storage object and will create an empty
6 | * collection if no collection already exists.
7 | *
8 | * @param {string} name The name of our DB we want to use
9 | * @param {function} callback Our fake DB uses callbacks because in
10 | * real life you probably would be making AJAX calls
11 | */
12 | class Store {
13 | constructor(name, callback) {
14 | this._dbName = name;
15 |
16 | if (!localStorage[name]) {
17 | let data = {
18 | todos: []
19 | };
20 |
21 | localStorage[name] = JSON.stringify(data);
22 | }
23 |
24 | if (callback) {
25 | callback.call(this, JSON.parse(localStorage[name]));
26 | }
27 | }
28 |
29 | /**
30 | * Finds items based on a query given as a JS object
31 | *
32 | * @param {object} query The query to match against (i.e. {foo: 'bar'})
33 | * @param {function} callback The callback to fire when the query has
34 | * completed running
35 | *
36 | * @example
37 | * db.find({foo: 'bar', hello: 'world'}, function (data) {
38 | * // data will return any items that have foo: bar and
39 | * // hello: world in their properties
40 | * })
41 | */
42 | find(query, callback){
43 | if (!callback) {
44 | return;
45 | }
46 |
47 | let todos = JSON.parse(localStorage[this._dbName]).todos;
48 |
49 | callback.call(this, todos.filter(todo => {
50 | for (let q in query) {
51 | if (query[q] !== todo[q]) {
52 | return false;
53 | }
54 | }
55 | return true;
56 | }));
57 | }
58 |
59 | /**
60 | * Will retrieve all data from the collection
61 | *
62 | * @param {function} callback The callback to fire upon retrieving data
63 | */
64 | findAll(callback){
65 | if (callback) {
66 | callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
67 | }
68 | }
69 |
70 | /**
71 | * Will save the given data to the DB. If no item exists it will create a new
72 | * item, otherwise it'll simply update an existing item's properties
73 | *
74 | * @param {object} updateData The data to save back into the DB
75 | * @param {function} callback The callback to fire after saving
76 | * @param {number} id An optional param to enter an ID of an item to update
77 | */
78 | save(updateData, callback, id){
79 | const data = JSON.parse(localStorage[this._dbName]);
80 | let todos = data.todos;
81 | const len = todos.length;
82 |
83 | // If an ID was actually given, find the item and update each property
84 | if (id) {
85 | for (let i = 0; i < len; i++) {
86 | if (todos[i].id === id) {
87 | for (let key in updateData) {
88 | todos[i][key] = updateData[key];
89 | }
90 | break;
91 | }
92 | }
93 |
94 | localStorage[this._dbName] = JSON.stringify(data);
95 |
96 | if (callback) {
97 | callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
98 | }
99 | } else {
100 | // Generate an ID
101 | updateData.id = new Date().getTime();
102 |
103 | todos.push(updateData);
104 | localStorage[this._dbName] = JSON.stringify(data);
105 |
106 | if (callback) {
107 | callback.call(this, [updateData]);
108 | }
109 | }
110 | }
111 |
112 | /**
113 | * Will remove an item from the Store based on its ID
114 | *
115 | * @param {number} id The ID of the item you want to remove
116 | * @param {function} callback The callback to fire after saving
117 | */
118 | remove(id, callback){
119 | const data = JSON.parse(localStorage[this._dbName]);
120 | let todos = data.todos;
121 | const len = todos.length;
122 |
123 | for (let i = 0; i < todos.length; i++) {
124 | if (todos[i].id == id) {
125 | todos.splice(i, 1);
126 | break;
127 | }
128 | }
129 |
130 | localStorage[this._dbName] = JSON.stringify(data);
131 |
132 | if (callback) {
133 | callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
134 | }
135 | }
136 |
137 | /**
138 | * Will drop all storage and start fresh
139 | *
140 | * @param {function} callback The callback to fire after dropping the data
141 | */
142 | drop(callback){
143 | localStorage[this._dbName] = JSON.stringify({todos: []});
144 |
145 | if (callback) {
146 | callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
147 | }
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/template.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | const htmlEscapes = {
5 | '&': '&',
6 | '<': '<',
7 | '>': '>',
8 | '"': '"',
9 | '\'': ''',
10 | '`': '`'
11 | };
12 |
13 | const reUnescapedHtml = /[&<>"'`]/g;
14 | const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
15 |
16 | let escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
17 | let escapeHtmlChar = chr => htmlEscapes[chr];
18 |
19 | class Template {
20 | constructor() {
21 | this.defaultTemplate = `
22 |
23 |
24 |
25 | {{title}}
26 |
27 |
28 |
29 | `;
30 | }
31 |
32 | /**
33 | * Creates an HTML string and returns it for placement in your app.
34 | *
35 | * NOTE: In real life you should be using a templating engine such as Mustache
36 | * or Handlebars, however, this is a vanilla JS example.
37 | *
38 | * @param {object} data The object containing keys you want to find in the
39 | * template to replace.
40 | * @returns {string} HTML String of an element
41 | *
42 | * @example
43 | * view.show({
44 | * id: 1,
45 | * title: "Hello World",
46 | * completed: 0,
47 | * })
48 | */
49 | show(data){
50 | let i = 0;
51 | let view = '';
52 | const len = data.length;
53 |
54 | for (i; i < len; i++) {
55 | let completed = '';
56 | let checked = '';
57 | let template = this.defaultTemplate;
58 |
59 | if (data[i].completed) {
60 | completed = 'completed';
61 | checked = 'checked';
62 | }
63 |
64 | template = template.replace('{{id}}', data[i].id);
65 | template = template.replace('{{title}}', escape(data[i].title));
66 | template = template.replace('{{completed}}', completed);
67 | template = template.replace('{{checked}}', checked);
68 |
69 | view += template;
70 | }
71 |
72 | return view;
73 | }
74 |
75 | /**
76 | * Displays a counter of how many to dos are left to complete
77 | *
78 | * @param {number} activeTodos The number of active todos.
79 | * @returns {string} String containing the count
80 | */
81 | itemCounter(activeTodos){
82 | let plural = activeTodos === 1 ? '' : 's';
83 | return `${activeTodos} item${plural} left`;
84 | }
85 |
86 | /**
87 | * Updates the text within the "Clear completed" button
88 | *
89 | * @param {[type]} completedTodos The number of completed todos.
90 | * @returns {string} String containing the count
91 | */
92 | clearCompletedButton(completedTodos){
93 | return (completedTodos > 0) ? 'Clear completed' : '';
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/view.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | // import {qs, qsa, $on, $parent, $delegate} from './helpers';
5 |
6 | let _itemId = element => parseInt($parent(element, 'li').dataset.id, 10);
7 |
8 | let _setFilter = currentPage => {
9 | qs('.filters .selected').className = '';
10 | qs(`.filters [href="#/${currentPage}"]`).className = 'selected';
11 | };
12 |
13 | let _elementComplete = (id, completed) => {
14 | let listItem = qs(`[data-id="${id}"]`);
15 |
16 | if (!listItem) {
17 | return;
18 | }
19 |
20 | listItem.className = completed ? 'completed' : '';
21 |
22 | // In case it was toggled from an event and not by clicking the checkbox
23 | qs('input', listItem).checked = completed;
24 | };
25 |
26 | let _editItem = (id, title) => {
27 | let listItem = qs(`[data-id="${id}"]`);
28 |
29 | if (!listItem) {
30 | return;
31 | }
32 |
33 | listItem.className += ' editing';
34 |
35 | let input = document.createElement('input');
36 | input.className = 'edit';
37 |
38 | listItem.appendChild(input);
39 | input.focus();
40 | input.value = title;
41 | };
42 |
43 | /**
44 | * View that abstracts away the browser's DOM completely.
45 | * It has two simple entry points:
46 | *
47 | * - bind(eventName, handler)
48 | * Takes a todo application event and registers the handler
49 | * - render(command, parameterObject)
50 | * Renders the given command with the options
51 | */
52 | class View {
53 | constructor(template) {
54 | this.template = template;
55 |
56 | this.ENTER_KEY = 13;
57 | this.ESCAPE_KEY = 27;
58 |
59 | this.$todoList = qs('.todo-list');
60 | this.$todoItemCounter = qs('.todo-count');
61 | this.$clearCompleted = qs('.clear-completed');
62 | this.$main = qs('.main');
63 | this.$footer = qs('.footer');
64 | this.$toggleAll = qs('.toggle-all');
65 | this.$newTodo = qs('.new-todo');
66 |
67 | this.viewCommands = {
68 | showEntries: parameter => this.$todoList.innerHTML = this.template.show(parameter),
69 | removeItem: parameter => this._removeItem(parameter),
70 | updateElementCount: parameter => this.$todoItemCounter.innerHTML = this.template.itemCounter(parameter),
71 | clearCompletedButton: parameter => this._clearCompletedButton(parameter.completed, parameter.visible),
72 | contentBlockVisibility: parameter => this.$main.style.display = this.$footer.style.display = parameter.visible ? 'block' : 'none',
73 | toggleAll: parameter => this.$toggleAll.checked = parameter.checked,
74 | setFilter: parameter => _setFilter(parameter),
75 | clearNewTodo: parameter => this.$newTodo.value = '',
76 | elementComplete: parameter => _elementComplete(parameter.id, parameter.completed),
77 | editItem: parameter => _editItem(parameter.id, parameter.title),
78 | editItemDone: parameter => this._editItemDone(parameter.id, parameter.title),
79 | };
80 | }
81 |
82 | _removeItem(id) {
83 | let elem = qs(`[data-id="${id}"]`);
84 |
85 | if (elem) {
86 | this.$todoList.removeChild(elem);
87 | }
88 | }
89 |
90 | _clearCompletedButton(completedCount, visible) {
91 | this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
92 | this.$clearCompleted.style.display = visible ? 'block' : 'none';
93 | }
94 |
95 | _editItemDone(id, title) {
96 | let listItem = qs(`[data-id="${id}"]`);
97 |
98 | if (!listItem) {
99 | return;
100 | }
101 |
102 | let input = qs('input.edit', listItem);
103 | listItem.removeChild(input);
104 |
105 | listItem.className = listItem.className.replace(' editing', '');
106 |
107 | qsa('label', listItem).forEach(label => label.textContent = title);
108 | }
109 |
110 | render(viewCmd, parameter) {
111 | this.viewCommands[viewCmd](parameter);
112 | }
113 |
114 | _bindItemEditDone(handler) {
115 | let self = this;
116 |
117 | $delegate(self.$todoList, 'li .edit', 'blur', function () {
118 | if (!this.dataset.iscanceled) {
119 | handler({
120 | id: _itemId(this),
121 | title: this.value
122 | });
123 | }
124 | });
125 |
126 | // Remove the cursor from the input when you hit enter just like if it were a real form
127 | $delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
128 | if (event.keyCode === self.ENTER_KEY) {
129 | this.blur();
130 | }
131 | });
132 | }
133 |
134 | _bindItemEditCancel(handler) {
135 | let self = this;
136 |
137 | $delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
138 | if (event.keyCode === self.ESCAPE_KEY) {
139 | let id = _itemId(this);
140 | this.dataset.iscanceled = true;
141 | this.blur();
142 |
143 | handler({ id });
144 | }
145 | });
146 | }
147 |
148 | bind(event, handler) {
149 | if (event === 'newTodo') {
150 | $on(this.$newTodo, 'change', () => handler(this.$newTodo.value));
151 | } else if (event === 'removeCompleted') {
152 | $on(this.$clearCompleted, 'click', handler);
153 | } else if (event === 'toggleAll') {
154 | $on(this.$toggleAll, 'click', function(){
155 | handler({completed: this.checked});
156 | });
157 | } else if (event === 'itemEdit') {
158 | $delegate(this.$todoList, 'li label', 'dblclick', function(){
159 | handler({id: _itemId(this)});
160 | });
161 | } else if (event === 'itemRemove') {
162 | $delegate(this.$todoList, '.destroy', 'click', function(){
163 | handler({id: _itemId(this)});
164 | });
165 | } else if (event === 'itemToggle') {
166 | $delegate(this.$todoList, '.toggle', 'click', function(){
167 | handler({
168 | id: _itemId(this),
169 | completed: this.checked
170 | });
171 | });
172 | } else if (event === 'itemEditDone') {
173 | this._bindItemEditDone(handler);
174 | } else if (event === 'itemEditCancel') {
175 | this._bindItemEditCancel(handler);
176 | }
177 | }
178 | }
179 |
--------------------------------------------------------------------------------