index.html 49 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773
  1. <!doctype html>
  2. <html lang="en" data-framework="javascript">
  3. <head>
  4. <meta charset="utf-8">
  5. <title>VanillaJS • TodoMVC</title>
  6. <style>
  7. hr {
  8. margin: 20px 0;
  9. border: 0;
  10. border-top: 1px dashed #c5c5c5;
  11. border-bottom: 1px dashed #f7f7f7;
  12. }
  13. .learn a {
  14. font-weight: normal;
  15. text-decoration: none;
  16. color: #b83f45;
  17. }
  18. .learn a:hover {
  19. text-decoration: underline;
  20. color: #787e7e;
  21. }
  22. .learn h3,
  23. .learn h4,
  24. .learn h5 {
  25. margin: 10px 0;
  26. font-weight: 500;
  27. line-height: 1.2;
  28. color: #000;
  29. }
  30. .learn h3 {
  31. font-size: 24px;
  32. }
  33. .learn h4 {
  34. font-size: 18px;
  35. }
  36. .learn h5 {
  37. margin-bottom: 0;
  38. font-size: 14px;
  39. }
  40. .learn ul {
  41. padding: 0;
  42. margin: 0 0 30px 25px;
  43. }
  44. .learn li {
  45. line-height: 20px;
  46. }
  47. .learn p {
  48. font-size: 15px;
  49. font-weight: 300;
  50. line-height: 1.3;
  51. margin-top: 0;
  52. margin-bottom: 0;
  53. }
  54. #issue-count {
  55. display: none;
  56. }
  57. .quote {
  58. border: none;
  59. margin: 20px 0 60px 0;
  60. }
  61. .quote p {
  62. font-style: italic;
  63. }
  64. .quote p:before {
  65. content: '“';
  66. font-size: 50px;
  67. opacity: .15;
  68. position: absolute;
  69. top: -20px;
  70. left: 3px;
  71. }
  72. .quote p:after {
  73. content: '”';
  74. font-size: 50px;
  75. opacity: .15;
  76. position: absolute;
  77. bottom: -42px;
  78. right: 3px;
  79. }
  80. .quote footer {
  81. position: absolute;
  82. bottom: -40px;
  83. right: 0;
  84. }
  85. .quote footer img {
  86. border-radius: 3px;
  87. }
  88. .quote footer a {
  89. margin-left: 5px;
  90. vertical-align: middle;
  91. }
  92. .speech-bubble {
  93. position: relative;
  94. padding: 10px;
  95. background: rgba(0, 0, 0, .04);
  96. border-radius: 5px;
  97. }
  98. .speech-bubble:after {
  99. content: '';
  100. position: absolute;
  101. top: 100%;
  102. right: 30px;
  103. border: 13px solid transparent;
  104. border-top-color: rgba(0, 0, 0, .04);
  105. }
  106. .learn-bar > .learn {
  107. position: absolute;
  108. width: 272px;
  109. top: 8px;
  110. left: -300px;
  111. padding: 10px;
  112. border-radius: 5px;
  113. background-color: rgba(255, 255, 255, .6);
  114. transition-property: left;
  115. transition-duration: 500ms;
  116. }
  117. @media (min-width: 899px) {
  118. .learn-bar {
  119. width: auto;
  120. padding-left: 300px;
  121. }
  122. .learn-bar > .learn {
  123. left: 8px;
  124. }
  125. }
  126. </style>
  127. <style>
  128. html,
  129. body {
  130. margin: 0;
  131. padding: 0;
  132. }
  133. button {
  134. margin: 0;
  135. padding: 0;
  136. border: 0;
  137. background: none;
  138. font-size: 100%;
  139. vertical-align: baseline;
  140. font-family: inherit;
  141. font-weight: inherit;
  142. color: inherit;
  143. -webkit-appearance: none;
  144. appearance: none;
  145. -webkit-font-smoothing: antialiased;
  146. -moz-osx-font-smoothing: grayscale;
  147. }
  148. body {
  149. font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
  150. line-height: 1.4em;
  151. background: #f5f5f5;
  152. color: #4d4d4d;
  153. min-width: 230px;
  154. max-width: 550px;
  155. margin: 0 auto;
  156. -webkit-font-smoothing: antialiased;
  157. -moz-osx-font-smoothing: grayscale;
  158. font-weight: 300;
  159. }
  160. :focus {
  161. outline: 0;
  162. }
  163. .hidden {
  164. display: none;
  165. }
  166. .todoapp {
  167. background: #fff;
  168. margin: 130px 0 40px 0;
  169. position: relative;
  170. box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
  171. 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  172. }
  173. .todoapp input::-webkit-input-placeholder {
  174. font-style: italic;
  175. font-weight: 300;
  176. color: #e6e6e6;
  177. }
  178. .todoapp input::-moz-placeholder {
  179. font-style: italic;
  180. font-weight: 300;
  181. color: #e6e6e6;
  182. }
  183. .todoapp input::input-placeholder {
  184. font-style: italic;
  185. font-weight: 300;
  186. color: #e6e6e6;
  187. }
  188. .todoapp h1 {
  189. position: absolute;
  190. top: -155px;
  191. width: 100%;
  192. font-size: 100px;
  193. font-weight: 100;
  194. text-align: center;
  195. color: rgba(175, 47, 47, 0.15);
  196. -webkit-text-rendering: optimizeLegibility;
  197. -moz-text-rendering: optimizeLegibility;
  198. text-rendering: optimizeLegibility;
  199. }
  200. .new-todo,
  201. .edit {
  202. position: relative;
  203. margin: 0;
  204. width: 100%;
  205. font-size: 24px;
  206. font-family: inherit;
  207. font-weight: inherit;
  208. line-height: 1.4em;
  209. border: 0;
  210. color: inherit;
  211. padding: 6px;
  212. border: 1px solid #999;
  213. box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
  214. box-sizing: border-box;
  215. -webkit-font-smoothing: antialiased;
  216. -moz-osx-font-smoothing: grayscale;
  217. }
  218. .new-todo {
  219. padding: 16px 16px 16px 60px;
  220. border: none;
  221. background: rgba(0, 0, 0, 0.003);
  222. box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
  223. }
  224. .main {
  225. position: relative;
  226. z-index: 2;
  227. border-top: 1px solid #e6e6e6;
  228. }
  229. .toggle-all {
  230. width: 1px;
  231. height: 1px;
  232. border: none; /* Mobile Safari */
  233. opacity: 0;
  234. position: absolute;
  235. right: 100%;
  236. bottom: 100%;
  237. }
  238. .toggle-all + label {
  239. width: 60px;
  240. height: 34px;
  241. font-size: 0;
  242. position: absolute;
  243. top: -52px;
  244. left: -13px;
  245. -webkit-transform: rotate(90deg);
  246. transform: rotate(90deg);
  247. }
  248. .toggle-all + label:before {
  249. content: '❯';
  250. font-size: 22px;
  251. color: #e6e6e6;
  252. padding: 10px 27px 10px 27px;
  253. }
  254. .toggle-all:checked + label:before {
  255. color: #737373;
  256. }
  257. .todo-list {
  258. margin: 0;
  259. padding: 0;
  260. list-style: none;
  261. }
  262. .todo-list li {
  263. position: relative;
  264. font-size: 24px;
  265. border-bottom: 1px solid #ededed;
  266. }
  267. .todo-list li:last-child {
  268. border-bottom: none;
  269. }
  270. .todo-list li.editing {
  271. border-bottom: none;
  272. padding: 0;
  273. }
  274. .todo-list li.editing .edit {
  275. display: block;
  276. width: 506px;
  277. padding: 12px 16px;
  278. margin: 0 0 0 43px;
  279. }
  280. .todo-list li.editing .view {
  281. display: none;
  282. }
  283. .todo-list li .toggle {
  284. text-align: center;
  285. width: 40px;
  286. /* auto, since non-WebKit browsers doesn't support input styling */
  287. height: auto;
  288. position: absolute;
  289. top: 0;
  290. bottom: 0;
  291. margin: auto 0;
  292. border: none; /* Mobile Safari */
  293. -webkit-appearance: none;
  294. appearance: none;
  295. }
  296. .todo-list li .toggle {
  297. opacity: 0;
  298. }
  299. .todo-list li .toggle + label {
  300. /*
  301. Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
  302. IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
  303. */
  304. background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
  305. background-repeat: no-repeat;
  306. background-position: center left;
  307. }
  308. .todo-list li .toggle:checked + label {
  309. background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
  310. }
  311. .todo-list li label {
  312. word-break: break-all;
  313. padding: 15px 15px 15px 60px;
  314. display: block;
  315. line-height: 1.2;
  316. transition: color 0.4s;
  317. }
  318. .todo-list li.completed label {
  319. color: #d9d9d9;
  320. text-decoration: line-through;
  321. }
  322. .todo-list li .destroy {
  323. display: none;
  324. position: absolute;
  325. top: 0;
  326. right: 10px;
  327. bottom: 0;
  328. width: 40px;
  329. height: 40px;
  330. margin: auto 0;
  331. font-size: 30px;
  332. color: #cc9a9a;
  333. margin-bottom: 11px;
  334. transition: color 0.2s ease-out;
  335. }
  336. .todo-list li .destroy:hover {
  337. color: #af5b5e;
  338. }
  339. .todo-list li .destroy:after {
  340. content: '×';
  341. }
  342. .todo-list li:hover .destroy {
  343. display: block;
  344. }
  345. .todo-list li .edit {
  346. display: none;
  347. }
  348. .todo-list li.editing:last-child {
  349. margin-bottom: -1px;
  350. }
  351. .footer {
  352. color: #777;
  353. padding: 10px 15px;
  354. height: 20px;
  355. text-align: center;
  356. border-top: 1px solid #e6e6e6;
  357. }
  358. .footer:before {
  359. content: '';
  360. position: absolute;
  361. right: 0;
  362. bottom: 0;
  363. left: 0;
  364. height: 50px;
  365. overflow: hidden;
  366. box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
  367. 0 8px 0 -3px #f6f6f6,
  368. 0 9px 1px -3px rgba(0, 0, 0, 0.2),
  369. 0 16px 0 -6px #f6f6f6,
  370. 0 17px 2px -6px rgba(0, 0, 0, 0.2);
  371. }
  372. .todo-count {
  373. float: left;
  374. text-align: left;
  375. }
  376. .todo-count strong {
  377. font-weight: 300;
  378. }
  379. .filters {
  380. margin: 0;
  381. padding: 0;
  382. list-style: none;
  383. position: absolute;
  384. right: 0;
  385. left: 0;
  386. }
  387. .filters li {
  388. display: inline;
  389. }
  390. .filters li a {
  391. color: inherit;
  392. margin: 3px;
  393. padding: 3px 7px;
  394. text-decoration: none;
  395. border: 1px solid transparent;
  396. border-radius: 3px;
  397. }
  398. .filters li a:hover {
  399. border-color: rgba(175, 47, 47, 0.1);
  400. }
  401. .filters li a.selected {
  402. border-color: rgba(175, 47, 47, 0.2);
  403. }
  404. .clear-completed,
  405. html .clear-completed:active {
  406. float: right;
  407. position: relative;
  408. line-height: 20px;
  409. text-decoration: none;
  410. cursor: pointer;
  411. }
  412. .clear-completed:hover {
  413. text-decoration: underline;
  414. }
  415. .info {
  416. margin: 65px auto 0;
  417. color: #bfbfbf;
  418. font-size: 10px;
  419. text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
  420. text-align: center;
  421. }
  422. .info p {
  423. line-height: 1;
  424. }
  425. .info a {
  426. color: inherit;
  427. text-decoration: none;
  428. font-weight: 400;
  429. }
  430. .info a:hover {
  431. text-decoration: underline;
  432. }
  433. /*
  434. Hack to remove background from Mobile Safari.
  435. Can't use it globally since it destroys checkboxes in Firefox
  436. */
  437. @media screen and (-webkit-min-device-pixel-ratio:0) {
  438. .toggle-all,
  439. .todo-list li .toggle {
  440. background: none;
  441. }
  442. .todo-list li .toggle {
  443. height: 40px;
  444. }
  445. }
  446. @media (max-width: 430px) {
  447. .footer {
  448. height: 50px;
  449. }
  450. .filters {
  451. bottom: 10px;
  452. }
  453. }
  454. </style>
  455. </head>
  456. <body>
  457. <section class="todoapp">
  458. <header class="header">
  459. <h1>todos</h1>
  460. <input class="new-todo" placeholder="What needs to be done?" autofocus>
  461. </header>
  462. <section class="main">
  463. <input id="toggle-all" class="toggle-all" type="checkbox">
  464. <label for="toggle-all">Mark all as complete</label>
  465. <ul class="todo-list"></ul>
  466. </section>
  467. <footer class="footer">
  468. <span class="todo-count"></span>
  469. <ul class="filters">
  470. <li>
  471. <a href="#/" class="selected">All</a>
  472. </li>
  473. <li>
  474. <a href="#/active">Active</a>
  475. </li>
  476. <li>
  477. <a href="#/completed">Completed</a>
  478. </li>
  479. </ul>
  480. <button class="clear-completed">Clear completed</button>
  481. </footer>
  482. </section>
  483. <footer class="info">
  484. <p>Double-click to edit a todo</p>
  485. <p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</a></p>
  486. <p>Refactored by <a href="https://github.com/cburgmer">Christoph Burgmer</a></p>
  487. <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
  488. </footer>
  489. <script>
  490. /* global _ */
  491. (function () {
  492. 'use strict';
  493. /* jshint ignore:start */
  494. // Underscore's Template Module
  495. // Courtesy of underscorejs.org
  496. var _ = (function (_) {
  497. _.defaults = function (object) {
  498. if (!object) {
  499. return object;
  500. }
  501. for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
  502. var iterable = arguments[argsIndex];
  503. if (iterable) {
  504. for (var key in iterable) {
  505. if (object[key] == null) {
  506. object[key] = iterable[key];
  507. }
  508. }
  509. }
  510. }
  511. return object;
  512. };
  513. // By default, Underscore uses ERB-style template delimiters, change the
  514. // following template settings to use alternative delimiters.
  515. _.templateSettings = {
  516. evaluate : /<%([\s\S]+?)%>/g,
  517. interpolate : /<%=([\s\S]+?)%>/g,
  518. escape : /<%-([\s\S]+?)%>/g
  519. };
  520. // When customizing `templateSettings`, if you don't want to define an
  521. // interpolation, evaluation or escaping regex, we need one that is
  522. // guaranteed not to match.
  523. var noMatch = /(.)^/;
  524. // Certain characters need to be escaped so that they can be put into a
  525. // string literal.
  526. var escapes = {
  527. "'": "'",
  528. '\\': '\\',
  529. '\r': 'r',
  530. '\n': 'n',
  531. '\t': 't',
  532. '\u2028': 'u2028',
  533. '\u2029': 'u2029'
  534. };
  535. var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
  536. // JavaScript micro-templating, similar to John Resig's implementation.
  537. // Underscore templating handles arbitrary delimiters, preserves whitespace,
  538. // and correctly escapes quotes within interpolated code.
  539. _.template = function(text, data, settings) {
  540. var render;
  541. settings = _.defaults({}, settings, _.templateSettings);
  542. // Combine delimiters into one regular expression via alternation.
  543. var matcher = new RegExp([
  544. (settings.escape || noMatch).source,
  545. (settings.interpolate || noMatch).source,
  546. (settings.evaluate || noMatch).source
  547. ].join('|') + '|$', 'g');
  548. // Compile the template source, escaping string literals appropriately.
  549. var index = 0;
  550. var source = "__p+='";
  551. text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
  552. source += text.slice(index, offset)
  553. .replace(escaper, function(match) { return '\\' + escapes[match]; });
  554. if (escape) {
  555. source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
  556. }
  557. if (interpolate) {
  558. source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
  559. }
  560. if (evaluate) {
  561. source += "';\n" + evaluate + "\n__p+='";
  562. }
  563. index = offset + match.length;
  564. return match;
  565. });
  566. source += "';\n";
  567. // If a variable is not specified, place data values in local scope.
  568. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
  569. source = "var __t,__p='',__j=Array.prototype.join," +
  570. "print=function(){__p+=__j.call(arguments,'');};\n" +
  571. source + "return __p;\n";
  572. try {
  573. render = new Function(settings.variable || 'obj', '_', source);
  574. } catch (e) {
  575. e.source = source;
  576. throw e;
  577. }
  578. if (data) return render(data, _);
  579. var template = function(data) {
  580. return render.call(this, data, _);
  581. };
  582. // Provide the compiled function source as a convenience for precompilation.
  583. template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
  584. return template;
  585. };
  586. return _;
  587. })({});
  588. if (location.hostname === 'todomvc.com') {
  589. (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  590. (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  591. m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  592. })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
  593. ga('create', 'UA-31081062-1', 'auto');
  594. ga('send', 'pageview');
  595. }
  596. /* jshint ignore:end */
  597. function redirect() {
  598. if (location.hostname === 'tastejs.github.io') {
  599. location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
  600. }
  601. }
  602. function findRoot() {
  603. var base = location.href.indexOf('examples/');
  604. return location.href.substr(0, base);
  605. }
  606. function getFile(file, callback) {
  607. if (!location.host) {
  608. return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
  609. }
  610. var xhr = new XMLHttpRequest();
  611. xhr.open('GET', findRoot() + file, true);
  612. xhr.send();
  613. xhr.onload = function () {
  614. if (xhr.status === 200 && callback) {
  615. callback(xhr.responseText);
  616. }
  617. };
  618. }
  619. function Learn(learnJSON, config) {
  620. if (!(this instanceof Learn)) {
  621. return new Learn(learnJSON, config);
  622. }
  623. var template, framework;
  624. if (typeof learnJSON !== 'object') {
  625. try {
  626. learnJSON = JSON.parse(learnJSON);
  627. } catch (e) {
  628. return;
  629. }
  630. }
  631. if (config) {
  632. template = config.template;
  633. framework = config.framework;
  634. }
  635. if (!template && learnJSON.templates) {
  636. template = learnJSON.templates.todomvc;
  637. }
  638. if (!framework && document.querySelector('[data-framework]')) {
  639. framework = document.querySelector('[data-framework]').dataset.framework;
  640. }
  641. this.template = template;
  642. if (learnJSON.backend) {
  643. this.frameworkJSON = learnJSON.backend;
  644. this.frameworkJSON.issueLabel = framework;
  645. this.append({
  646. backend: true
  647. });
  648. } else if (learnJSON[framework]) {
  649. this.frameworkJSON = learnJSON[framework];
  650. this.frameworkJSON.issueLabel = framework;
  651. this.append();
  652. }
  653. this.fetchIssueCount();
  654. }
  655. Learn.prototype.append = function (opts) {
  656. var aside = document.createElement('aside');
  657. aside.innerHTML = _.template(this.template, this.frameworkJSON);
  658. aside.className = 'learn';
  659. if (opts && opts.backend) {
  660. // Remove demo link
  661. var sourceLinks = aside.querySelector('.source-links');
  662. var heading = sourceLinks.firstElementChild;
  663. var sourceLink = sourceLinks.lastElementChild;
  664. // Correct link path
  665. var href = sourceLink.getAttribute('href');
  666. sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
  667. sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
  668. } else {
  669. // Localize demo links
  670. var demoLinks = aside.querySelectorAll('.demo-link');
  671. Array.prototype.forEach.call(demoLinks, function (demoLink) {
  672. if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
  673. demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
  674. }
  675. });
  676. }
  677. document.body.className = (document.body.className + ' learn-bar').trim();
  678. document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
  679. };
  680. Learn.prototype.fetchIssueCount = function () {
  681. var issueLink = document.getElementById('issue-count-link');
  682. if (issueLink) {
  683. var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
  684. var xhr = new XMLHttpRequest();
  685. xhr.open('GET', url, true);
  686. xhr.onload = function (e) {
  687. var parsedResponse = JSON.parse(e.target.responseText);
  688. if (parsedResponse instanceof Array) {
  689. var count = parsedResponse.length;
  690. if (count !== 0) {
  691. issueLink.innerHTML = 'This app has ' + count + ' open issues';
  692. document.getElementById('issue-count').style.display = 'inline';
  693. }
  694. }
  695. };
  696. xhr.send();
  697. }
  698. };
  699. redirect();
  700. })();
  701. </script>
  702. <script>
  703. /*global NodeList */
  704. (function (window) {
  705. 'use strict';
  706. // Get element(s) by CSS selector:
  707. window.qs = function (selector, scope) {
  708. return (scope || document).querySelector(selector);
  709. };
  710. window.qsa = function (selector, scope) {
  711. return (scope || document).querySelectorAll(selector);
  712. };
  713. // addEventListener wrapper:
  714. window.$on = function (target, type, callback, useCapture) {
  715. target.addEventListener(type, callback, !!useCapture);
  716. };
  717. // Attach a handler to event for all elements that match the selector,
  718. // now or in the future, based on a root element
  719. window.$delegate = function (target, selector, type, handler) {
  720. function dispatchEvent(event) {
  721. var targetElement = event.target;
  722. var potentialElements = window.qsa(selector, target);
  723. var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
  724. if (hasMatch) {
  725. handler.call(targetElement, event);
  726. }
  727. }
  728. // https://developer.mozilla.org/en-US/docs/Web/Events/blur
  729. var useCapture = type === 'blur' || type === 'focus';
  730. window.$on(target, type, dispatchEvent, useCapture);
  731. };
  732. // Find the element's parent with the given tag name:
  733. // $parent(qs('a'), 'div');
  734. window.$parent = function (element, tagName) {
  735. if (!element.parentNode) {
  736. return;
  737. }
  738. if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
  739. return element.parentNode;
  740. }
  741. return window.$parent(element.parentNode, tagName);
  742. };
  743. // Allow for looping on nodes by chaining:
  744. // qsa('.foo').forEach(function () {})
  745. NodeList.prototype.forEach = Array.prototype.forEach;
  746. })(window);
  747. </script>
  748. <script>
  749. /*jshint eqeqeq:false */
  750. (function (window) {
  751. 'use strict';
  752. /**
  753. * Creates a new client side storage object and will create an empty
  754. * collection if no collection already exists.
  755. *
  756. * @param {string} name The name of our DB we want to use
  757. * @param {function} callback Our fake DB uses callbacks because in
  758. * real life you probably would be making AJAX calls
  759. */
  760. function Store(name, callback) {
  761. callback = callback || function () {};
  762. this._dbName = name;
  763. if (!localStorage.getItem(name)) {
  764. var todos = [];
  765. localStorage.setItem(name, JSON.stringify(todos));
  766. }
  767. callback.call(this, JSON.parse(localStorage.getItem(name)));
  768. }
  769. /**
  770. * Finds items based on a query given as a JS object
  771. *
  772. * @param {object} query The query to match against (i.e. {foo: 'bar'})
  773. * @param {function} callback The callback to fire when the query has
  774. * completed running
  775. *
  776. * @example
  777. * db.find({foo: 'bar', hello: 'world'}, function (data) {
  778. * // data will return any items that have foo: bar and
  779. * // hello: world in their properties
  780. * });
  781. */
  782. Store.prototype.find = function (query, callback) {
  783. if (!callback) {
  784. return;
  785. }
  786. var todos = JSON.parse(localStorage.getItem(this._dbName));
  787. callback.call(this, todos.filter(function (todo) {
  788. for (var q in query) {
  789. if (query[q] !== todo[q]) {
  790. return false;
  791. }
  792. }
  793. return true;
  794. }));
  795. };
  796. /**
  797. * Will retrieve all data from the collection
  798. *
  799. * @param {function} callback The callback to fire upon retrieving data
  800. */
  801. Store.prototype.findAll = function (callback) {
  802. callback = callback || function () {};
  803. callback.call(this, JSON.parse(localStorage.getItem(this._dbName)));
  804. };
  805. /**
  806. * Will save the given data to the DB. If no item exists it will create a new
  807. * item, otherwise it'll simply update an existing item's properties
  808. *
  809. * @param {object} updateData The data to save back into the DB
  810. * @param {function} callback The callback to fire after saving
  811. * @param {number} id An optional param to enter an ID of an item to update
  812. */
  813. Store.prototype.save = function (updateData, callback, id) {
  814. var todos = JSON.parse(localStorage.getItem(this._dbName));
  815. callback = callback || function() {};
  816. // If an ID was actually given, find the item and update each property
  817. if (id) {
  818. for (var i = 0; i < todos.length; i++) {
  819. if (todos[i].id === id) {
  820. for (var key in updateData) {
  821. todos[i][key] = updateData[key];
  822. }
  823. break;
  824. }
  825. }
  826. localStorage.setItem(this._dbName, JSON.stringify(todos));
  827. callback.call(this, todos);
  828. } else {
  829. // Generate an ID
  830. updateData.id = new Date().getTime();
  831. todos.push(updateData);
  832. localStorage.setItem(this._dbName, JSON.stringify(todos));
  833. callback.call(this, [updateData]);
  834. }
  835. };
  836. /**
  837. * Will remove an item from the Store based on its ID
  838. *
  839. * @param {number} id The ID of the item you want to remove
  840. * @param {function} callback The callback to fire after saving
  841. */
  842. Store.prototype.remove = function (id, callback) {
  843. var todos = JSON.parse(localStorage.getItem(this._dbName));
  844. for (var i = 0; i < todos.length; i++) {
  845. if (todos[i].id == id) {
  846. todos.splice(i, 1);
  847. break;
  848. }
  849. }
  850. localStorage.setItem(this._dbName, JSON.stringify(todos));
  851. callback.call(this, todos);
  852. };
  853. /**
  854. * Will drop all storage and start fresh
  855. *
  856. * @param {function} callback The callback to fire after dropping the data
  857. */
  858. Store.prototype.drop = function (callback) {
  859. var todos = [];
  860. localStorage.setItem(this._dbName, JSON.stringify(todos));
  861. callback.call(this, todos);
  862. };
  863. // Export to window
  864. window.app = window.app || {};
  865. window.app.Store = Store;
  866. })(window);
  867. </script>
  868. <script>
  869. (function (window) {
  870. 'use strict';
  871. /**
  872. * Creates a new Model instance and hooks up the storage.
  873. *
  874. * @constructor
  875. * @param {object} storage A reference to the client side storage class
  876. */
  877. function Model(storage) {
  878. this.storage = storage;
  879. }
  880. /**
  881. * Creates a new todo model
  882. *
  883. * @param {string} [title] The title of the task
  884. * @param {function} [callback] The callback to fire after the model is created
  885. */
  886. Model.prototype.create = function (title, callback) {
  887. title = title || '';
  888. callback = callback || function () {};
  889. var newItem = {
  890. title: title.trim(),
  891. completed: false
  892. };
  893. this.storage.save(newItem, callback);
  894. };
  895. /**
  896. * Finds and returns a model in storage. If no query is given it'll simply
  897. * return everything. If you pass in a string or number it'll look that up as
  898. * the ID of the model to find. Lastly, you can pass it an object to match
  899. * against.
  900. *
  901. * @param {string|number|object} [query] A query to match models against
  902. * @param {function} [callback] The callback to fire after the model is found
  903. *
  904. * @example
  905. * model.read(1, func); // Will find the model with an ID of 1
  906. * model.read('1'); // Same as above
  907. * //Below will find a model with foo equalling bar and hello equalling world.
  908. * model.read({ foo: 'bar', hello: 'world' });
  909. */
  910. Model.prototype.read = function (query, callback) {
  911. var queryType = typeof query;
  912. callback = callback || function () {};
  913. if (queryType === 'function') {
  914. callback = query;
  915. return this.storage.findAll(callback);
  916. } else if (queryType === 'string' || queryType === 'number') {
  917. query = parseInt(query, 10);
  918. this.storage.find({ id: query }, callback);
  919. } else {
  920. this.storage.find(query, callback);
  921. }
  922. };
  923. /**
  924. * Updates a model by giving it an ID, data to update, and a callback to fire when
  925. * the update is complete.
  926. *
  927. * @param {number} id The id of the model to update
  928. * @param {object} data The properties to update and their new value
  929. * @param {function} callback The callback to fire when the update is complete.
  930. */
  931. Model.prototype.update = function (id, data, callback) {
  932. this.storage.save(data, callback, id);
  933. };
  934. /**
  935. * Removes a model from storage
  936. *
  937. * @param {number} id The ID of the model to remove
  938. * @param {function} callback The callback to fire when the removal is complete.
  939. */
  940. Model.prototype.remove = function (id, callback) {
  941. this.storage.remove(id, callback);
  942. };
  943. /**
  944. * WARNING: Will remove ALL data from storage.
  945. *
  946. * @param {function} callback The callback to fire when the storage is wiped.
  947. */
  948. Model.prototype.removeAll = function (callback) {
  949. this.storage.drop(callback);
  950. };
  951. /**
  952. * Returns a count of all todos
  953. */
  954. Model.prototype.getCount = function (callback) {
  955. var todos = {
  956. active: 0,
  957. completed: 0,
  958. total: 0
  959. };
  960. this.storage.findAll(function (data) {
  961. data.forEach(function (todo) {
  962. if (todo.completed) {
  963. todos.completed++;
  964. } else {
  965. todos.active++;
  966. }
  967. todos.total++;
  968. });
  969. callback(todos);
  970. });
  971. };
  972. // Export to window
  973. window.app = window.app || {};
  974. window.app.Model = Model;
  975. })(window);
  976. </script>
  977. <script>
  978. /*jshint laxbreak:true */
  979. (function (window) {
  980. 'use strict';
  981. var htmlEscapes = {
  982. '&': '&amp;',
  983. '<': '&lt;',
  984. '>': '&gt;',
  985. '"': '&quot;',
  986. '\'': '&#x27;',
  987. '`': '&#x60;'
  988. };
  989. var escapeHtmlChar = function (chr) {
  990. return htmlEscapes[chr];
  991. };
  992. var reUnescapedHtml = /[&<>"'`]/g;
  993. var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
  994. var escape = function (string) {
  995. return (string && reHasUnescapedHtml.test(string))
  996. ? string.replace(reUnescapedHtml, escapeHtmlChar)
  997. : string;
  998. };
  999. /**
  1000. * Sets up defaults for all the Template methods such as a default template
  1001. *
  1002. * @constructor
  1003. */
  1004. function Template() {
  1005. this.defaultTemplate
  1006. = '<li data-id="{{id}}" class="{{completed}}">'
  1007. + '<div class="view">'
  1008. + '<input class="toggle" type="checkbox" {{checked}}>'
  1009. + '<label>{{title}}</label>'
  1010. + '<button class="destroy"></button>'
  1011. + '</div>'
  1012. + '</li>';
  1013. }
  1014. /**
  1015. * Creates an <li> HTML string and returns it for placement in your app.
  1016. *
  1017. * NOTE: In real life you should be using a templating engine such as Mustache
  1018. * or Handlebars, however, this is a vanilla JS example.
  1019. *
  1020. * @param {object} data The object containing keys you want to find in the
  1021. * template to replace.
  1022. * @returns {string} HTML String of an <li> element
  1023. *
  1024. * @example
  1025. * view.show({
  1026. * id: 1,
  1027. * title: "Hello World",
  1028. * completed: 0,
  1029. * });
  1030. */
  1031. Template.prototype.show = function (data) {
  1032. var i, l;
  1033. var view = '';
  1034. for (i = 0, l = data.length; i < l; i++) {
  1035. var template = this.defaultTemplate;
  1036. var completed = '';
  1037. var checked = '';
  1038. if (data[i].completed) {
  1039. completed = 'completed';
  1040. checked = 'checked';
  1041. }
  1042. template = template.replace('{{id}}', data[i].id);
  1043. template = template.replace('{{title}}', escape(data[i].title));
  1044. template = template.replace('{{completed}}', completed);
  1045. template = template.replace('{{checked}}', checked);
  1046. view = view + template;
  1047. }
  1048. return view;
  1049. };
  1050. /**
  1051. * Displays a counter of how many to dos are left to complete
  1052. *
  1053. * @param {number} activeTodos The number of active todos.
  1054. * @returns {string} String containing the count
  1055. */
  1056. Template.prototype.itemCounter = function (activeTodos) {
  1057. var plural = activeTodos === 1 ? '' : 's';
  1058. return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
  1059. };
  1060. /**
  1061. * Updates the text within the "Clear completed" button
  1062. *
  1063. * @param {[type]} completedTodos The number of completed todos.
  1064. * @returns {string} String containing the count
  1065. */
  1066. Template.prototype.clearCompletedButton = function (completedTodos) {
  1067. if (completedTodos > 0) {
  1068. return 'Clear completed';
  1069. } else {
  1070. return '';
  1071. }
  1072. };
  1073. // Export to window
  1074. window.app = window.app || {};
  1075. window.app.Template = Template;
  1076. })(window);
  1077. </script>
  1078. <script>
  1079. /*global qs, qsa, $on, $parent, $delegate */
  1080. (function (window) {
  1081. 'use strict';
  1082. /**
  1083. * View that abstracts away the browser's DOM completely.
  1084. * It has two simple entry points:
  1085. *
  1086. * - bind(eventName, handler)
  1087. * Takes a todo application event and registers the handler
  1088. * - render(command, parameterObject)
  1089. * Renders the given command with the options
  1090. */
  1091. function View(template) {
  1092. this.template = template;
  1093. this.ENTER_KEY = 13;
  1094. this.ESCAPE_KEY = 27;
  1095. this.$todoList = qs('.todo-list');
  1096. this.$todoItemCounter = qs('.todo-count');
  1097. this.$clearCompleted = qs('.clear-completed');
  1098. this.$main = qs('.main');
  1099. this.$footer = qs('.footer');
  1100. this.$toggleAll = qs('.toggle-all');
  1101. this.$newTodo = qs('.new-todo');
  1102. }
  1103. View.prototype._removeItem = function (id) {
  1104. var elem = qs('[data-id="' + id + '"]');
  1105. if (elem) {
  1106. this.$todoList.removeChild(elem);
  1107. }
  1108. };
  1109. View.prototype._clearCompletedButton = function (completedCount, visible) {
  1110. this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
  1111. this.$clearCompleted.style.display = visible ? 'block' : 'none';
  1112. };
  1113. View.prototype._setFilter = function (currentPage) {
  1114. qs('.filters .selected').className = '';
  1115. qs('.filters [href="#/' + currentPage + '"]').className = 'selected';
  1116. };
  1117. View.prototype._elementComplete = function (id, completed) {
  1118. var listItem = qs('[data-id="' + id + '"]');
  1119. if (!listItem) {
  1120. return;
  1121. }
  1122. listItem.className = completed ? 'completed' : '';
  1123. // In case it was toggled from an event and not by clicking the checkbox
  1124. qs('input', listItem).checked = completed;
  1125. };
  1126. View.prototype._editItem = function (id, title) {
  1127. var listItem = qs('[data-id="' + id + '"]');
  1128. if (!listItem) {
  1129. return;
  1130. }
  1131. listItem.className = listItem.className + ' editing';
  1132. var input = document.createElement('input');
  1133. input.className = 'edit';
  1134. listItem.appendChild(input);
  1135. input.focus();
  1136. input.value = title;
  1137. };
  1138. View.prototype._editItemDone = function (id, title) {
  1139. var listItem = qs('[data-id="' + id + '"]');
  1140. if (!listItem) {
  1141. return;
  1142. }
  1143. var input = qs('input.edit', listItem);
  1144. listItem.removeChild(input);
  1145. listItem.className = listItem.className.replace('editing', '');
  1146. qsa('label', listItem).forEach(function (label) {
  1147. label.textContent = title;
  1148. });
  1149. };
  1150. View.prototype.render = function (viewCmd, parameter) {
  1151. var self = this;
  1152. var viewCommands = {
  1153. showEntries: function () {
  1154. self.$todoList.innerHTML = self.template.show(parameter);
  1155. },
  1156. removeItem: function () {
  1157. self._removeItem(parameter);
  1158. },
  1159. updateElementCount: function () {
  1160. self.$todoItemCounter.innerHTML = self.template.itemCounter(parameter);
  1161. },
  1162. clearCompletedButton: function () {
  1163. self._clearCompletedButton(parameter.completed, parameter.visible);
  1164. },
  1165. contentBlockVisibility: function () {
  1166. self.$main.style.display = self.$footer.style.display = parameter.visible ? 'block' : 'none';
  1167. },
  1168. toggleAll: function () {
  1169. self.$toggleAll.checked = parameter.checked;
  1170. },
  1171. setFilter: function () {
  1172. self._setFilter(parameter);
  1173. },
  1174. clearNewTodo: function () {
  1175. self.$newTodo.value = '';
  1176. },
  1177. elementComplete: function () {
  1178. self._elementComplete(parameter.id, parameter.completed);
  1179. },
  1180. editItem: function () {
  1181. self._editItem(parameter.id, parameter.title);
  1182. },
  1183. editItemDone: function () {
  1184. self._editItemDone(parameter.id, parameter.title);
  1185. }
  1186. };
  1187. viewCommands[viewCmd]();
  1188. };
  1189. View.prototype._itemId = function (element) {
  1190. var li = $parent(element, 'li');
  1191. return parseInt(li.dataset.id, 10);
  1192. };
  1193. View.prototype._bindItemEditDone = function (handler) {
  1194. var self = this;
  1195. $delegate(self.$todoList, 'li .edit', 'blur', function () {
  1196. if (!this.dataset.iscanceled) {
  1197. handler({
  1198. id: self._itemId(this),
  1199. title: this.value
  1200. });
  1201. }
  1202. });
  1203. $delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
  1204. if (event.keyCode === self.ENTER_KEY) {
  1205. // Remove the cursor from the input when you hit enter just like if it
  1206. // were a real form
  1207. this.blur();
  1208. }
  1209. });
  1210. };
  1211. View.prototype._bindItemEditCancel = function (handler) {
  1212. var self = this;
  1213. $delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
  1214. if (event.keyCode === self.ESCAPE_KEY) {
  1215. this.dataset.iscanceled = true;
  1216. this.blur();
  1217. handler({id: self._itemId(this)});
  1218. }
  1219. });
  1220. };
  1221. View.prototype.bind = function (event, handler) {
  1222. var self = this;
  1223. if (event === 'newTodo') {
  1224. $on(self.$newTodo, 'change', function () {
  1225. handler(self.$newTodo.value);
  1226. });
  1227. } else if (event === 'removeCompleted') {
  1228. $on(self.$clearCompleted, 'click', function () {
  1229. handler();
  1230. });
  1231. } else if (event === 'toggleAll') {
  1232. $on(self.$toggleAll, 'click', function () {
  1233. handler({completed: this.checked});
  1234. });
  1235. } else if (event === 'itemEdit') {
  1236. $delegate(self.$todoList, 'li label', 'dblclick', function () {
  1237. handler({id: self._itemId(this)});
  1238. });
  1239. } else if (event === 'itemRemove') {
  1240. $delegate(self.$todoList, '.destroy', 'click', function () {
  1241. handler({id: self._itemId(this)});
  1242. });
  1243. } else if (event === 'itemToggle') {
  1244. $delegate(self.$todoList, '.toggle', 'click', function () {
  1245. handler({
  1246. id: self._itemId(this),
  1247. completed: this.checked
  1248. });
  1249. });
  1250. } else if (event === 'itemEditDone') {
  1251. self._bindItemEditDone(handler);
  1252. } else if (event === 'itemEditCancel') {
  1253. self._bindItemEditCancel(handler);
  1254. }
  1255. };
  1256. // Export to window
  1257. window.app = window.app || {};
  1258. window.app.View = View;
  1259. }(window));
  1260. </script>
  1261. <script>
  1262. (function (window) {
  1263. 'use strict';
  1264. /**
  1265. * Takes a model and view and acts as the controller between them
  1266. *
  1267. * @constructor
  1268. * @param {object} model The model instance
  1269. * @param {object} view The view instance
  1270. */
  1271. function Controller(model, view) {
  1272. var self = this;
  1273. self.model = model;
  1274. self.view = view;
  1275. self.view.bind('newTodo', function (title) {
  1276. self.addItem(title);
  1277. });
  1278. self.view.bind('itemEdit', function (item) {
  1279. self.editItem(item.id);
  1280. });
  1281. self.view.bind('itemEditDone', function (item) {
  1282. self.editItemSave(item.id, item.title);
  1283. });
  1284. self.view.bind('itemEditCancel', function (item) {
  1285. self.editItemCancel(item.id);
  1286. });
  1287. self.view.bind('itemRemove', function (item) {
  1288. self.removeItem(item.id);
  1289. });
  1290. self.view.bind('itemToggle', function (item) {
  1291. self.toggleComplete(item.id, item.completed);
  1292. });
  1293. self.view.bind('removeCompleted', function () {
  1294. self.removeCompletedItems();
  1295. });
  1296. self.view.bind('toggleAll', function (status) {
  1297. self.toggleAll(status.completed);
  1298. });
  1299. }
  1300. /**
  1301. * Loads and initialises the view
  1302. *
  1303. * @param {string} '' | 'active' | 'completed'
  1304. */
  1305. Controller.prototype.setView = function (locationHash) {
  1306. var route = locationHash.split('/')[1];
  1307. var page = route || '';
  1308. this._updateFilterState(page);
  1309. };
  1310. /**
  1311. * An event to fire on load. Will get all items and display them in the
  1312. * todo-list
  1313. */
  1314. Controller.prototype.showAll = function () {
  1315. var self = this;
  1316. self.model.read(function (data) {
  1317. self.view.render('showEntries', data);
  1318. });
  1319. };
  1320. /**
  1321. * Renders all active tasks
  1322. */
  1323. Controller.prototype.showActive = function () {
  1324. var self = this;
  1325. self.model.read({ completed: false }, function (data) {
  1326. self.view.render('showEntries', data);
  1327. });
  1328. };
  1329. /**
  1330. * Renders all completed tasks
  1331. */
  1332. Controller.prototype.showCompleted = function () {
  1333. var self = this;
  1334. self.model.read({ completed: true }, function (data) {
  1335. self.view.render('showEntries', data);
  1336. });
  1337. };
  1338. /**
  1339. * An event to fire whenever you want to add an item. Simply pass in the event
  1340. * object and it'll handle the DOM insertion and saving of the new item.
  1341. */
  1342. Controller.prototype.addItem = function (title) {
  1343. var self = this;
  1344. if (title.trim() === '') {
  1345. return;
  1346. }
  1347. self.model.create(title, function () {
  1348. self.view.render('clearNewTodo');
  1349. self._filter(true);
  1350. });
  1351. };
  1352. /*
  1353. * Triggers the item editing mode.
  1354. */
  1355. Controller.prototype.editItem = function (id) {
  1356. var self = this;
  1357. self.model.read(id, function (data) {
  1358. self.view.render('editItem', {id: id, title: data[0].title});
  1359. });
  1360. };
  1361. /*
  1362. * Finishes the item editing mode successfully.
  1363. */
  1364. Controller.prototype.editItemSave = function (id, title) {
  1365. var self = this;
  1366. title = title.trim();
  1367. if (title.length !== 0) {
  1368. self.model.update(id, {title: title}, function () {
  1369. self.view.render('editItemDone', {id: id, title: title});
  1370. });
  1371. } else {
  1372. self.removeItem(id);
  1373. }
  1374. };
  1375. /*
  1376. * Cancels the item editing mode.
  1377. */
  1378. Controller.prototype.editItemCancel = function (id) {
  1379. var self = this;
  1380. self.model.read(id, function (data) {
  1381. self.view.render('editItemDone', {id: id, title: data[0].title});
  1382. });
  1383. };
  1384. /**
  1385. * By giving it an ID it'll find the DOM element matching that ID,
  1386. * remove it from the DOM and also remove it from storage.
  1387. *
  1388. * @param {number} id The ID of the item to remove from the DOM and
  1389. * storage
  1390. */
  1391. Controller.prototype.removeItem = function (id) {
  1392. var self = this;
  1393. self.model.remove(id, function () {
  1394. self.view.render('removeItem', id);
  1395. });
  1396. self._filter();
  1397. };
  1398. /**
  1399. * Will remove all completed items from the DOM and storage.
  1400. */
  1401. Controller.prototype.removeCompletedItems = function () {
  1402. var self = this;
  1403. self.model.read({ completed: true }, function (data) {
  1404. data.forEach(function (item) {
  1405. self.removeItem(item.id);
  1406. });
  1407. });
  1408. self._filter();
  1409. };
  1410. /**
  1411. * Give it an ID of a model and a checkbox and it will update the item
  1412. * in storage based on the checkbox's state.
  1413. *
  1414. * @param {number} id The ID of the element to complete or uncomplete
  1415. * @param {object} checkbox The checkbox to check the state of complete
  1416. * or not
  1417. * @param {boolean|undefined} silent Prevent re-filtering the todo items
  1418. */
  1419. Controller.prototype.toggleComplete = function (id, completed, silent) {
  1420. var self = this;
  1421. self.model.update(id, { completed: completed }, function () {
  1422. self.view.render('elementComplete', {
  1423. id: id,
  1424. completed: completed
  1425. });
  1426. });
  1427. if (!silent) {
  1428. self._filter();
  1429. }
  1430. };
  1431. /**
  1432. * Will toggle ALL checkboxes' on/off state and completeness of models.
  1433. * Just pass in the event object.
  1434. */
  1435. Controller.prototype.toggleAll = function (completed) {
  1436. var self = this;
  1437. self.model.read({ completed: !completed }, function (data) {
  1438. data.forEach(function (item) {
  1439. self.toggleComplete(item.id, completed, true);
  1440. });
  1441. });
  1442. self._filter();
  1443. };
  1444. /**
  1445. * Updates the pieces of the page which change depending on the remaining
  1446. * number of todos.
  1447. */
  1448. Controller.prototype._updateCount = function () {
  1449. var self = this;
  1450. self.model.getCount(function (todos) {
  1451. self.view.render('updateElementCount', todos.active);
  1452. self.view.render('clearCompletedButton', {
  1453. completed: todos.completed,
  1454. visible: todos.completed > 0
  1455. });
  1456. self.view.render('toggleAll', {checked: todos.completed === todos.total});
  1457. self.view.render('contentBlockVisibility', {visible: todos.total > 0});
  1458. });
  1459. };
  1460. /**
  1461. * Re-filters the todo items, based on the active route.
  1462. * @param {boolean|undefined} force forces a re-painting of todo items.
  1463. */
  1464. Controller.prototype._filter = function (force) {
  1465. var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1);
  1466. // Update the elements on the page, which change with each completed todo
  1467. this._updateCount();
  1468. // If the last active route isn't "All", or we're switching routes, we
  1469. // re-create the todo item elements, calling:
  1470. // this.show[All|Active|Completed]();
  1471. if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
  1472. this['show' + activeRoute]();
  1473. }
  1474. this._lastActiveRoute = activeRoute;
  1475. };
  1476. /**
  1477. * Simply updates the filter nav's selected states
  1478. */
  1479. Controller.prototype._updateFilterState = function (currentPage) {
  1480. // Store a reference to the active route, allowing us to re-filter todo
  1481. // items as they are marked complete or incomplete.
  1482. this._activeRoute = currentPage;
  1483. if (currentPage === '') {
  1484. this._activeRoute = 'All';
  1485. }
  1486. this._filter();
  1487. this.view.render('setFilter', currentPage);
  1488. };
  1489. // Export to window
  1490. window.app = window.app || {};
  1491. window.app.Controller = Controller;
  1492. })(window);
  1493. </script>
  1494. <script>
  1495. /*global app, $on */
  1496. (function () {
  1497. 'use strict';
  1498. /**
  1499. * Sets up a brand new Todo list.
  1500. *
  1501. * @param {string} name The name of your new to do list.
  1502. */
  1503. function Todo(name) {
  1504. this.storage = new app.Store(name);
  1505. this.model = new app.Model(this.storage);
  1506. this.template = new app.Template();
  1507. this.view = new app.View(this.template);
  1508. this.controller = new app.Controller(this.model, this.view);
  1509. }
  1510. var todo = new Todo('todos-vanillajs');
  1511. function setView() {
  1512. todo.controller.setView(document.location.hash);
  1513. }
  1514. $on(window, 'load', setView);
  1515. $on(window, 'hashchange', setView);
  1516. })();
  1517. </script>
  1518. </body>
  1519. </html>