├── 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 |
12 |

todos

13 | 14 |
15 | 16 |
17 | 18 | 19 | 20 |
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 | ![](screenshot.png) 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 | Creative Commons License
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 | 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 | --------------------------------------------------------------------------------