123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773 |
- <!doctype html>
- <html lang="en" data-framework="javascript">
- <head>
- <meta charset="utf-8">
- <title>VanillaJS • TodoMVC</title>
- <style>
- hr {
- margin: 20px 0;
- border: 0;
- border-top: 1px dashed #c5c5c5;
- border-bottom: 1px dashed #f7f7f7;
- }
- .learn a {
- font-weight: normal;
- text-decoration: none;
- color: #b83f45;
- }
- .learn a:hover {
- text-decoration: underline;
- color: #787e7e;
- }
- .learn h3,
- .learn h4,
- .learn h5 {
- margin: 10px 0;
- font-weight: 500;
- line-height: 1.2;
- color: #000;
- }
- .learn h3 {
- font-size: 24px;
- }
- .learn h4 {
- font-size: 18px;
- }
- .learn h5 {
- margin-bottom: 0;
- font-size: 14px;
- }
- .learn ul {
- padding: 0;
- margin: 0 0 30px 25px;
- }
- .learn li {
- line-height: 20px;
- }
- .learn p {
- font-size: 15px;
- font-weight: 300;
- line-height: 1.3;
- margin-top: 0;
- margin-bottom: 0;
- }
- #issue-count {
- display: none;
- }
- .quote {
- border: none;
- margin: 20px 0 60px 0;
- }
- .quote p {
- font-style: italic;
- }
- .quote p:before {
- content: '“';
- font-size: 50px;
- opacity: .15;
- position: absolute;
- top: -20px;
- left: 3px;
- }
- .quote p:after {
- content: '”';
- font-size: 50px;
- opacity: .15;
- position: absolute;
- bottom: -42px;
- right: 3px;
- }
- .quote footer {
- position: absolute;
- bottom: -40px;
- right: 0;
- }
- .quote footer img {
- border-radius: 3px;
- }
- .quote footer a {
- margin-left: 5px;
- vertical-align: middle;
- }
- .speech-bubble {
- position: relative;
- padding: 10px;
- background: rgba(0, 0, 0, .04);
- border-radius: 5px;
- }
- .speech-bubble:after {
- content: '';
- position: absolute;
- top: 100%;
- right: 30px;
- border: 13px solid transparent;
- border-top-color: rgba(0, 0, 0, .04);
- }
- .learn-bar > .learn {
- position: absolute;
- width: 272px;
- top: 8px;
- left: -300px;
- padding: 10px;
- border-radius: 5px;
- background-color: rgba(255, 255, 255, .6);
- transition-property: left;
- transition-duration: 500ms;
- }
- @media (min-width: 899px) {
- .learn-bar {
- width: auto;
- padding-left: 300px;
- }
- .learn-bar > .learn {
- left: 8px;
- }
- }
- </style>
- <style>
- html,
- body {
- margin: 0;
- padding: 0;
- }
- button {
- margin: 0;
- padding: 0;
- border: 0;
- background: none;
- font-size: 100%;
- vertical-align: baseline;
- font-family: inherit;
- font-weight: inherit;
- color: inherit;
- -webkit-appearance: none;
- appearance: none;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- body {
- font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif;
- line-height: 1.4em;
- background: #f5f5f5;
- color: #4d4d4d;
- min-width: 230px;
- max-width: 550px;
- margin: 0 auto;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- font-weight: 300;
- }
- :focus {
- outline: 0;
- }
- .hidden {
- display: none;
- }
- .todoapp {
- background: #fff;
- margin: 130px 0 40px 0;
- position: relative;
- box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2),
- 0 25px 50px 0 rgba(0, 0, 0, 0.1);
- }
- .todoapp input::-webkit-input-placeholder {
- font-style: italic;
- font-weight: 300;
- color: #e6e6e6;
- }
- .todoapp input::-moz-placeholder {
- font-style: italic;
- font-weight: 300;
- color: #e6e6e6;
- }
- .todoapp input::input-placeholder {
- font-style: italic;
- font-weight: 300;
- color: #e6e6e6;
- }
- .todoapp h1 {
- position: absolute;
- top: -155px;
- width: 100%;
- font-size: 100px;
- font-weight: 100;
- text-align: center;
- color: rgba(175, 47, 47, 0.15);
- -webkit-text-rendering: optimizeLegibility;
- -moz-text-rendering: optimizeLegibility;
- text-rendering: optimizeLegibility;
- }
- .new-todo,
- .edit {
- position: relative;
- margin: 0;
- width: 100%;
- font-size: 24px;
- font-family: inherit;
- font-weight: inherit;
- line-height: 1.4em;
- border: 0;
- color: inherit;
- padding: 6px;
- border: 1px solid #999;
- box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
- box-sizing: border-box;
- -webkit-font-smoothing: antialiased;
- -moz-osx-font-smoothing: grayscale;
- }
- .new-todo {
- padding: 16px 16px 16px 60px;
- border: none;
- background: rgba(0, 0, 0, 0.003);
- box-shadow: inset 0 -2px 1px rgba(0,0,0,0.03);
- }
- .main {
- position: relative;
- z-index: 2;
- border-top: 1px solid #e6e6e6;
- }
- .toggle-all {
- width: 1px;
- height: 1px;
- border: none; /* Mobile Safari */
- opacity: 0;
- position: absolute;
- right: 100%;
- bottom: 100%;
- }
- .toggle-all + label {
- width: 60px;
- height: 34px;
- font-size: 0;
- position: absolute;
- top: -52px;
- left: -13px;
- -webkit-transform: rotate(90deg);
- transform: rotate(90deg);
- }
- .toggle-all + label:before {
- content: '❯';
- font-size: 22px;
- color: #e6e6e6;
- padding: 10px 27px 10px 27px;
- }
- .toggle-all:checked + label:before {
- color: #737373;
- }
- .todo-list {
- margin: 0;
- padding: 0;
- list-style: none;
- }
- .todo-list li {
- position: relative;
- font-size: 24px;
- border-bottom: 1px solid #ededed;
- }
- .todo-list li:last-child {
- border-bottom: none;
- }
- .todo-list li.editing {
- border-bottom: none;
- padding: 0;
- }
- .todo-list li.editing .edit {
- display: block;
- width: 506px;
- padding: 12px 16px;
- margin: 0 0 0 43px;
- }
- .todo-list li.editing .view {
- display: none;
- }
- .todo-list li .toggle {
- text-align: center;
- width: 40px;
- /* auto, since non-WebKit browsers doesn't support input styling */
- height: auto;
- position: absolute;
- top: 0;
- bottom: 0;
- margin: auto 0;
- border: none; /* Mobile Safari */
- -webkit-appearance: none;
- appearance: none;
- }
- .todo-list li .toggle {
- opacity: 0;
- }
- .todo-list li .toggle + label {
- /*
- Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433
- IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/
- */
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
- background-repeat: no-repeat;
- background-position: center left;
- }
- .todo-list li .toggle:checked + label {
- background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
- }
- .todo-list li label {
- word-break: break-all;
- padding: 15px 15px 15px 60px;
- display: block;
- line-height: 1.2;
- transition: color 0.4s;
- }
- .todo-list li.completed label {
- color: #d9d9d9;
- text-decoration: line-through;
- }
- .todo-list li .destroy {
- display: none;
- position: absolute;
- top: 0;
- right: 10px;
- bottom: 0;
- width: 40px;
- height: 40px;
- margin: auto 0;
- font-size: 30px;
- color: #cc9a9a;
- margin-bottom: 11px;
- transition: color 0.2s ease-out;
- }
- .todo-list li .destroy:hover {
- color: #af5b5e;
- }
- .todo-list li .destroy:after {
- content: '×';
- }
- .todo-list li:hover .destroy {
- display: block;
- }
- .todo-list li .edit {
- display: none;
- }
- .todo-list li.editing:last-child {
- margin-bottom: -1px;
- }
- .footer {
- color: #777;
- padding: 10px 15px;
- height: 20px;
- text-align: center;
- border-top: 1px solid #e6e6e6;
- }
- .footer:before {
- content: '';
- position: absolute;
- right: 0;
- bottom: 0;
- left: 0;
- height: 50px;
- overflow: hidden;
- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2),
- 0 8px 0 -3px #f6f6f6,
- 0 9px 1px -3px rgba(0, 0, 0, 0.2),
- 0 16px 0 -6px #f6f6f6,
- 0 17px 2px -6px rgba(0, 0, 0, 0.2);
- }
- .todo-count {
- float: left;
- text-align: left;
- }
- .todo-count strong {
- font-weight: 300;
- }
- .filters {
- margin: 0;
- padding: 0;
- list-style: none;
- position: absolute;
- right: 0;
- left: 0;
- }
- .filters li {
- display: inline;
- }
- .filters li a {
- color: inherit;
- margin: 3px;
- padding: 3px 7px;
- text-decoration: none;
- border: 1px solid transparent;
- border-radius: 3px;
- }
- .filters li a:hover {
- border-color: rgba(175, 47, 47, 0.1);
- }
- .filters li a.selected {
- border-color: rgba(175, 47, 47, 0.2);
- }
- .clear-completed,
- html .clear-completed:active {
- float: right;
- position: relative;
- line-height: 20px;
- text-decoration: none;
- cursor: pointer;
- }
- .clear-completed:hover {
- text-decoration: underline;
- }
- .info {
- margin: 65px auto 0;
- color: #bfbfbf;
- font-size: 10px;
- text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
- text-align: center;
- }
- .info p {
- line-height: 1;
- }
- .info a {
- color: inherit;
- text-decoration: none;
- font-weight: 400;
- }
- .info a:hover {
- text-decoration: underline;
- }
- /*
- Hack to remove background from Mobile Safari.
- Can't use it globally since it destroys checkboxes in Firefox
- */
- @media screen and (-webkit-min-device-pixel-ratio:0) {
- .toggle-all,
- .todo-list li .toggle {
- background: none;
- }
- .todo-list li .toggle {
- height: 40px;
- }
- }
- @media (max-width: 430px) {
- .footer {
- height: 50px;
- }
- .filters {
- bottom: 10px;
- }
- }
- </style>
- </head>
- <body>
- <section class="todoapp">
- <header class="header">
- <h1>todos</h1>
- <input class="new-todo" placeholder="What needs to be done?" autofocus>
- </header>
- <section class="main">
- <input id="toggle-all" class="toggle-all" type="checkbox">
- <label for="toggle-all">Mark all as complete</label>
- <ul class="todo-list"></ul>
- </section>
- <footer class="footer">
- <span class="todo-count"></span>
- <ul class="filters">
- <li>
- <a href="#/" class="selected">All</a>
- </li>
- <li>
- <a href="#/active">Active</a>
- </li>
- <li>
- <a href="#/completed">Completed</a>
- </li>
- </ul>
- <button class="clear-completed">Clear completed</button>
- </footer>
- </section>
- <footer class="info">
- <p>Double-click to edit a todo</p>
- <p>Created by <a href="http://twitter.com/oscargodson">Oscar Godson</a></p>
- <p>Refactored by <a href="https://github.com/cburgmer">Christoph Burgmer</a></p>
- <p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
- </footer>
- <script>
- /* global _ */
- (function () {
- 'use strict';
- /* jshint ignore:start */
- // Underscore's Template Module
- // Courtesy of underscorejs.org
- var _ = (function (_) {
- _.defaults = function (object) {
- if (!object) {
- return object;
- }
- for (var argsIndex = 1, argsLength = arguments.length; argsIndex < argsLength; argsIndex++) {
- var iterable = arguments[argsIndex];
- if (iterable) {
- for (var key in iterable) {
- if (object[key] == null) {
- object[key] = iterable[key];
- }
- }
- }
- }
- return object;
- };
- // By default, Underscore uses ERB-style template delimiters, change the
- // following template settings to use alternative delimiters.
- _.templateSettings = {
- evaluate : /<%([\s\S]+?)%>/g,
- interpolate : /<%=([\s\S]+?)%>/g,
- escape : /<%-([\s\S]+?)%>/g
- };
- // When customizing `templateSettings`, if you don't want to define an
- // interpolation, evaluation or escaping regex, we need one that is
- // guaranteed not to match.
- var noMatch = /(.)^/;
- // Certain characters need to be escaped so that they can be put into a
- // string literal.
- var escapes = {
- "'": "'",
- '\\': '\\',
- '\r': 'r',
- '\n': 'n',
- '\t': 't',
- '\u2028': 'u2028',
- '\u2029': 'u2029'
- };
- var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g;
- // JavaScript micro-templating, similar to John Resig's implementation.
- // Underscore templating handles arbitrary delimiters, preserves whitespace,
- // and correctly escapes quotes within interpolated code.
- _.template = function(text, data, settings) {
- var render;
- settings = _.defaults({}, settings, _.templateSettings);
- // Combine delimiters into one regular expression via alternation.
- var matcher = new RegExp([
- (settings.escape || noMatch).source,
- (settings.interpolate || noMatch).source,
- (settings.evaluate || noMatch).source
- ].join('|') + '|$', 'g');
- // Compile the template source, escaping string literals appropriately.
- var index = 0;
- var source = "__p+='";
- text.replace(matcher, function(match, escape, interpolate, evaluate, offset) {
- source += text.slice(index, offset)
- .replace(escaper, function(match) { return '\\' + escapes[match]; });
- if (escape) {
- source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'";
- }
- if (interpolate) {
- source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
- }
- if (evaluate) {
- source += "';\n" + evaluate + "\n__p+='";
- }
- index = offset + match.length;
- return match;
- });
- source += "';\n";
- // If a variable is not specified, place data values in local scope.
- if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n';
- source = "var __t,__p='',__j=Array.prototype.join," +
- "print=function(){__p+=__j.call(arguments,'');};\n" +
- source + "return __p;\n";
- try {
- render = new Function(settings.variable || 'obj', '_', source);
- } catch (e) {
- e.source = source;
- throw e;
- }
- if (data) return render(data, _);
- var template = function(data) {
- return render.call(this, data, _);
- };
- // Provide the compiled function source as a convenience for precompilation.
- template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}';
- return template;
- };
- return _;
- })({});
- if (location.hostname === 'todomvc.com') {
- (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
- (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
- m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
- })(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
- ga('create', 'UA-31081062-1', 'auto');
- ga('send', 'pageview');
- }
- /* jshint ignore:end */
- function redirect() {
- if (location.hostname === 'tastejs.github.io') {
- location.href = location.href.replace('tastejs.github.io/todomvc', 'todomvc.com');
- }
- }
- function findRoot() {
- var base = location.href.indexOf('examples/');
- return location.href.substr(0, base);
- }
- function getFile(file, callback) {
- if (!location.host) {
- return console.info('Miss the info bar? Run TodoMVC from a server to avoid a cross-origin error.');
- }
- var xhr = new XMLHttpRequest();
- xhr.open('GET', findRoot() + file, true);
- xhr.send();
- xhr.onload = function () {
- if (xhr.status === 200 && callback) {
- callback(xhr.responseText);
- }
- };
- }
- function Learn(learnJSON, config) {
- if (!(this instanceof Learn)) {
- return new Learn(learnJSON, config);
- }
- var template, framework;
- if (typeof learnJSON !== 'object') {
- try {
- learnJSON = JSON.parse(learnJSON);
- } catch (e) {
- return;
- }
- }
- if (config) {
- template = config.template;
- framework = config.framework;
- }
- if (!template && learnJSON.templates) {
- template = learnJSON.templates.todomvc;
- }
- if (!framework && document.querySelector('[data-framework]')) {
- framework = document.querySelector('[data-framework]').dataset.framework;
- }
- this.template = template;
- if (learnJSON.backend) {
- this.frameworkJSON = learnJSON.backend;
- this.frameworkJSON.issueLabel = framework;
- this.append({
- backend: true
- });
- } else if (learnJSON[framework]) {
- this.frameworkJSON = learnJSON[framework];
- this.frameworkJSON.issueLabel = framework;
- this.append();
- }
- this.fetchIssueCount();
- }
- Learn.prototype.append = function (opts) {
- var aside = document.createElement('aside');
- aside.innerHTML = _.template(this.template, this.frameworkJSON);
- aside.className = 'learn';
- if (opts && opts.backend) {
- // Remove demo link
- var sourceLinks = aside.querySelector('.source-links');
- var heading = sourceLinks.firstElementChild;
- var sourceLink = sourceLinks.lastElementChild;
- // Correct link path
- var href = sourceLink.getAttribute('href');
- sourceLink.setAttribute('href', href.substr(href.lastIndexOf('http')));
- sourceLinks.innerHTML = heading.outerHTML + sourceLink.outerHTML;
- } else {
- // Localize demo links
- var demoLinks = aside.querySelectorAll('.demo-link');
- Array.prototype.forEach.call(demoLinks, function (demoLink) {
- if (demoLink.getAttribute('href').substr(0, 4) !== 'http') {
- demoLink.setAttribute('href', findRoot() + demoLink.getAttribute('href'));
- }
- });
- }
- document.body.className = (document.body.className + ' learn-bar').trim();
- document.body.insertAdjacentHTML('afterBegin', aside.outerHTML);
- };
- Learn.prototype.fetchIssueCount = function () {
- var issueLink = document.getElementById('issue-count-link');
- if (issueLink) {
- var url = issueLink.href.replace('https://github.com', 'https://api.github.com/repos');
- var xhr = new XMLHttpRequest();
- xhr.open('GET', url, true);
- xhr.onload = function (e) {
- var parsedResponse = JSON.parse(e.target.responseText);
- if (parsedResponse instanceof Array) {
- var count = parsedResponse.length;
- if (count !== 0) {
- issueLink.innerHTML = 'This app has ' + count + ' open issues';
- document.getElementById('issue-count').style.display = 'inline';
- }
- }
- };
- xhr.send();
- }
- };
- redirect();
- })();
- </script>
- <script>
- /*global NodeList */
- (function (window) {
- 'use strict';
- // Get element(s) by CSS selector:
- window.qs = function (selector, scope) {
- return (scope || document).querySelector(selector);
- };
- window.qsa = function (selector, scope) {
- return (scope || document).querySelectorAll(selector);
- };
- // addEventListener wrapper:
- window.$on = function (target, type, callback, useCapture) {
- target.addEventListener(type, callback, !!useCapture);
- };
- // Attach a handler to event for all elements that match the selector,
- // now or in the future, based on a root element
- window.$delegate = function (target, selector, type, handler) {
- function dispatchEvent(event) {
- var targetElement = event.target;
- var potentialElements = window.qsa(selector, target);
- var hasMatch = Array.prototype.indexOf.call(potentialElements, targetElement) >= 0;
- if (hasMatch) {
- handler.call(targetElement, event);
- }
- }
- // https://developer.mozilla.org/en-US/docs/Web/Events/blur
- var useCapture = type === 'blur' || type === 'focus';
- window.$on(target, type, dispatchEvent, useCapture);
- };
- // Find the element's parent with the given tag name:
- // $parent(qs('a'), 'div');
- window.$parent = function (element, tagName) {
- if (!element.parentNode) {
- return;
- }
- if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
- return element.parentNode;
- }
- return window.$parent(element.parentNode, tagName);
- };
- // Allow for looping on nodes by chaining:
- // qsa('.foo').forEach(function () {})
- NodeList.prototype.forEach = Array.prototype.forEach;
- })(window);
- </script>
- <script>
- /*jshint eqeqeq:false */
- (function (window) {
- 'use strict';
- /**
- * Creates a new client side storage object and will create an empty
- * collection if no collection already exists.
- *
- * @param {string} name The name of our DB we want to use
- * @param {function} callback Our fake DB uses callbacks because in
- * real life you probably would be making AJAX calls
- */
- function Store(name, callback) {
- callback = callback || function () {};
- this._dbName = name;
- if (!localStorage.getItem(name)) {
- var todos = [];
- localStorage.setItem(name, JSON.stringify(todos));
- }
- callback.call(this, JSON.parse(localStorage.getItem(name)));
- }
- /**
- * Finds items based on a query given as a JS object
- *
- * @param {object} query The query to match against (i.e. {foo: 'bar'})
- * @param {function} callback The callback to fire when the query has
- * completed running
- *
- * @example
- * db.find({foo: 'bar', hello: 'world'}, function (data) {
- * // data will return any items that have foo: bar and
- * // hello: world in their properties
- * });
- */
- Store.prototype.find = function (query, callback) {
- if (!callback) {
- return;
- }
- var todos = JSON.parse(localStorage.getItem(this._dbName));
- callback.call(this, todos.filter(function (todo) {
- for (var q in query) {
- if (query[q] !== todo[q]) {
- return false;
- }
- }
- return true;
- }));
- };
- /**
- * Will retrieve all data from the collection
- *
- * @param {function} callback The callback to fire upon retrieving data
- */
- Store.prototype.findAll = function (callback) {
- callback = callback || function () {};
- callback.call(this, JSON.parse(localStorage.getItem(this._dbName)));
- };
- /**
- * Will save the given data to the DB. If no item exists it will create a new
- * item, otherwise it'll simply update an existing item's properties
- *
- * @param {object} updateData The data to save back into the DB
- * @param {function} callback The callback to fire after saving
- * @param {number} id An optional param to enter an ID of an item to update
- */
- Store.prototype.save = function (updateData, callback, id) {
- var todos = JSON.parse(localStorage.getItem(this._dbName));
- callback = callback || function() {};
- // If an ID was actually given, find the item and update each property
- if (id) {
- for (var i = 0; i < todos.length; i++) {
- if (todos[i].id === id) {
- for (var key in updateData) {
- todos[i][key] = updateData[key];
- }
- break;
- }
- }
- localStorage.setItem(this._dbName, JSON.stringify(todos));
- callback.call(this, todos);
- } else {
- // Generate an ID
- updateData.id = new Date().getTime();
- todos.push(updateData);
- localStorage.setItem(this._dbName, JSON.stringify(todos));
- callback.call(this, [updateData]);
- }
- };
- /**
- * Will remove an item from the Store based on its ID
- *
- * @param {number} id The ID of the item you want to remove
- * @param {function} callback The callback to fire after saving
- */
- Store.prototype.remove = function (id, callback) {
- var todos = JSON.parse(localStorage.getItem(this._dbName));
- for (var i = 0; i < todos.length; i++) {
- if (todos[i].id == id) {
- todos.splice(i, 1);
- break;
- }
- }
- localStorage.setItem(this._dbName, JSON.stringify(todos));
- callback.call(this, todos);
- };
- /**
- * Will drop all storage and start fresh
- *
- * @param {function} callback The callback to fire after dropping the data
- */
- Store.prototype.drop = function (callback) {
- var todos = [];
- localStorage.setItem(this._dbName, JSON.stringify(todos));
- callback.call(this, todos);
- };
- // Export to window
- window.app = window.app || {};
- window.app.Store = Store;
- })(window);
- </script>
- <script>
- (function (window) {
- 'use strict';
- /**
- * Creates a new Model instance and hooks up the storage.
- *
- * @constructor
- * @param {object} storage A reference to the client side storage class
- */
- function Model(storage) {
- this.storage = storage;
- }
- /**
- * Creates a new todo model
- *
- * @param {string} [title] The title of the task
- * @param {function} [callback] The callback to fire after the model is created
- */
- Model.prototype.create = function (title, callback) {
- title = title || '';
- callback = callback || function () {};
- var newItem = {
- title: title.trim(),
- completed: false
- };
- this.storage.save(newItem, callback);
- };
- /**
- * Finds and returns a model in storage. If no query is given it'll simply
- * return everything. If you pass in a string or number it'll look that up as
- * the ID of the model to find. Lastly, you can pass it an object to match
- * against.
- *
- * @param {string|number|object} [query] A query to match models against
- * @param {function} [callback] The callback to fire after the model is found
- *
- * @example
- * model.read(1, func); // Will find the model with an ID of 1
- * model.read('1'); // Same as above
- * //Below will find a model with foo equalling bar and hello equalling world.
- * model.read({ foo: 'bar', hello: 'world' });
- */
- Model.prototype.read = function (query, callback) {
- var queryType = typeof query;
- callback = callback || function () {};
- if (queryType === 'function') {
- callback = query;
- return this.storage.findAll(callback);
- } else if (queryType === 'string' || queryType === 'number') {
- query = parseInt(query, 10);
- this.storage.find({ id: query }, callback);
- } else {
- this.storage.find(query, callback);
- }
- };
- /**
- * Updates a model by giving it an ID, data to update, and a callback to fire when
- * the update is complete.
- *
- * @param {number} id The id of the model to update
- * @param {object} data The properties to update and their new value
- * @param {function} callback The callback to fire when the update is complete.
- */
- Model.prototype.update = function (id, data, callback) {
- this.storage.save(data, callback, id);
- };
- /**
- * Removes a model from storage
- *
- * @param {number} id The ID of the model to remove
- * @param {function} callback The callback to fire when the removal is complete.
- */
- Model.prototype.remove = function (id, callback) {
- this.storage.remove(id, callback);
- };
- /**
- * WARNING: Will remove ALL data from storage.
- *
- * @param {function} callback The callback to fire when the storage is wiped.
- */
- Model.prototype.removeAll = function (callback) {
- this.storage.drop(callback);
- };
- /**
- * Returns a count of all todos
- */
- Model.prototype.getCount = function (callback) {
- var todos = {
- active: 0,
- completed: 0,
- total: 0
- };
- this.storage.findAll(function (data) {
- data.forEach(function (todo) {
- if (todo.completed) {
- todos.completed++;
- } else {
- todos.active++;
- }
- todos.total++;
- });
- callback(todos);
- });
- };
- // Export to window
- window.app = window.app || {};
- window.app.Model = Model;
- })(window);
- </script>
- <script>
- /*jshint laxbreak:true */
- (function (window) {
- 'use strict';
- var htmlEscapes = {
- '&': '&',
- '<': '<',
- '>': '>',
- '"': '"',
- '\'': ''',
- '`': '`'
- };
- var escapeHtmlChar = function (chr) {
- return htmlEscapes[chr];
- };
- var reUnescapedHtml = /[&<>"'`]/g;
- var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
- var escape = function (string) {
- return (string && reHasUnescapedHtml.test(string))
- ? string.replace(reUnescapedHtml, escapeHtmlChar)
- : string;
- };
- /**
- * Sets up defaults for all the Template methods such as a default template
- *
- * @constructor
- */
- function Template() {
- this.defaultTemplate
- = '<li data-id="{{id}}" class="{{completed}}">'
- + '<div class="view">'
- + '<input class="toggle" type="checkbox" {{checked}}>'
- + '<label>{{title}}</label>'
- + '<button class="destroy"></button>'
- + '</div>'
- + '</li>';
- }
- /**
- * Creates an <li> HTML string and returns it for placement in your app.
- *
- * NOTE: In real life you should be using a templating engine such as Mustache
- * or Handlebars, however, this is a vanilla JS example.
- *
- * @param {object} data The object containing keys you want to find in the
- * template to replace.
- * @returns {string} HTML String of an <li> element
- *
- * @example
- * view.show({
- * id: 1,
- * title: "Hello World",
- * completed: 0,
- * });
- */
- Template.prototype.show = function (data) {
- var i, l;
- var view = '';
- for (i = 0, l = data.length; i < l; i++) {
- var template = this.defaultTemplate;
- var completed = '';
- var checked = '';
- if (data[i].completed) {
- completed = 'completed';
- checked = 'checked';
- }
- template = template.replace('{{id}}', data[i].id);
- template = template.replace('{{title}}', escape(data[i].title));
- template = template.replace('{{completed}}', completed);
- template = template.replace('{{checked}}', checked);
- view = view + template;
- }
- return view;
- };
- /**
- * Displays a counter of how many to dos are left to complete
- *
- * @param {number} activeTodos The number of active todos.
- * @returns {string} String containing the count
- */
- Template.prototype.itemCounter = function (activeTodos) {
- var plural = activeTodos === 1 ? '' : 's';
- return '<strong>' + activeTodos + '</strong> item' + plural + ' left';
- };
- /**
- * Updates the text within the "Clear completed" button
- *
- * @param {[type]} completedTodos The number of completed todos.
- * @returns {string} String containing the count
- */
- Template.prototype.clearCompletedButton = function (completedTodos) {
- if (completedTodos > 0) {
- return 'Clear completed';
- } else {
- return '';
- }
- };
- // Export to window
- window.app = window.app || {};
- window.app.Template = Template;
- })(window);
- </script>
- <script>
- /*global qs, qsa, $on, $parent, $delegate */
- (function (window) {
- 'use strict';
- /**
- * View that abstracts away the browser's DOM completely.
- * It has two simple entry points:
- *
- * - bind(eventName, handler)
- * Takes a todo application event and registers the handler
- * - render(command, parameterObject)
- * Renders the given command with the options
- */
- function View(template) {
- this.template = template;
- this.ENTER_KEY = 13;
- this.ESCAPE_KEY = 27;
- this.$todoList = qs('.todo-list');
- this.$todoItemCounter = qs('.todo-count');
- this.$clearCompleted = qs('.clear-completed');
- this.$main = qs('.main');
- this.$footer = qs('.footer');
- this.$toggleAll = qs('.toggle-all');
- this.$newTodo = qs('.new-todo');
- }
- View.prototype._removeItem = function (id) {
- var elem = qs('[data-id="' + id + '"]');
- if (elem) {
- this.$todoList.removeChild(elem);
- }
- };
- View.prototype._clearCompletedButton = function (completedCount, visible) {
- this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
- this.$clearCompleted.style.display = visible ? 'block' : 'none';
- };
- View.prototype._setFilter = function (currentPage) {
- qs('.filters .selected').className = '';
- qs('.filters [href="#/' + currentPage + '"]').className = 'selected';
- };
- View.prototype._elementComplete = function (id, completed) {
- var listItem = qs('[data-id="' + id + '"]');
- if (!listItem) {
- return;
- }
- listItem.className = completed ? 'completed' : '';
- // In case it was toggled from an event and not by clicking the checkbox
- qs('input', listItem).checked = completed;
- };
- View.prototype._editItem = function (id, title) {
- var listItem = qs('[data-id="' + id + '"]');
- if (!listItem) {
- return;
- }
- listItem.className = listItem.className + ' editing';
- var input = document.createElement('input');
- input.className = 'edit';
- listItem.appendChild(input);
- input.focus();
- input.value = title;
- };
- View.prototype._editItemDone = function (id, title) {
- var listItem = qs('[data-id="' + id + '"]');
- if (!listItem) {
- return;
- }
- var input = qs('input.edit', listItem);
- listItem.removeChild(input);
- listItem.className = listItem.className.replace('editing', '');
- qsa('label', listItem).forEach(function (label) {
- label.textContent = title;
- });
- };
- View.prototype.render = function (viewCmd, parameter) {
- var self = this;
- var viewCommands = {
- showEntries: function () {
- self.$todoList.innerHTML = self.template.show(parameter);
- },
- removeItem: function () {
- self._removeItem(parameter);
- },
- updateElementCount: function () {
- self.$todoItemCounter.innerHTML = self.template.itemCounter(parameter);
- },
- clearCompletedButton: function () {
- self._clearCompletedButton(parameter.completed, parameter.visible);
- },
- contentBlockVisibility: function () {
- self.$main.style.display = self.$footer.style.display = parameter.visible ? 'block' : 'none';
- },
- toggleAll: function () {
- self.$toggleAll.checked = parameter.checked;
- },
- setFilter: function () {
- self._setFilter(parameter);
- },
- clearNewTodo: function () {
- self.$newTodo.value = '';
- },
- elementComplete: function () {
- self._elementComplete(parameter.id, parameter.completed);
- },
- editItem: function () {
- self._editItem(parameter.id, parameter.title);
- },
- editItemDone: function () {
- self._editItemDone(parameter.id, parameter.title);
- }
- };
- viewCommands[viewCmd]();
- };
- View.prototype._itemId = function (element) {
- var li = $parent(element, 'li');
- return parseInt(li.dataset.id, 10);
- };
- View.prototype._bindItemEditDone = function (handler) {
- var self = this;
- $delegate(self.$todoList, 'li .edit', 'blur', function () {
- if (!this.dataset.iscanceled) {
- handler({
- id: self._itemId(this),
- title: this.value
- });
- }
- });
- $delegate(self.$todoList, 'li .edit', 'keypress', function (event) {
- if (event.keyCode === self.ENTER_KEY) {
- // Remove the cursor from the input when you hit enter just like if it
- // were a real form
- this.blur();
- }
- });
- };
- View.prototype._bindItemEditCancel = function (handler) {
- var self = this;
- $delegate(self.$todoList, 'li .edit', 'keyup', function (event) {
- if (event.keyCode === self.ESCAPE_KEY) {
- this.dataset.iscanceled = true;
- this.blur();
- handler({id: self._itemId(this)});
- }
- });
- };
- View.prototype.bind = function (event, handler) {
- var self = this;
- if (event === 'newTodo') {
- $on(self.$newTodo, 'change', function () {
- handler(self.$newTodo.value);
- });
- } else if (event === 'removeCompleted') {
- $on(self.$clearCompleted, 'click', function () {
- handler();
- });
- } else if (event === 'toggleAll') {
- $on(self.$toggleAll, 'click', function () {
- handler({completed: this.checked});
- });
- } else if (event === 'itemEdit') {
- $delegate(self.$todoList, 'li label', 'dblclick', function () {
- handler({id: self._itemId(this)});
- });
- } else if (event === 'itemRemove') {
- $delegate(self.$todoList, '.destroy', 'click', function () {
- handler({id: self._itemId(this)});
- });
- } else if (event === 'itemToggle') {
- $delegate(self.$todoList, '.toggle', 'click', function () {
- handler({
- id: self._itemId(this),
- completed: this.checked
- });
- });
- } else if (event === 'itemEditDone') {
- self._bindItemEditDone(handler);
- } else if (event === 'itemEditCancel') {
- self._bindItemEditCancel(handler);
- }
- };
- // Export to window
- window.app = window.app || {};
- window.app.View = View;
- }(window));
- </script>
- <script>
- (function (window) {
- 'use strict';
- /**
- * Takes a model and view and acts as the controller between them
- *
- * @constructor
- * @param {object} model The model instance
- * @param {object} view The view instance
- */
- function Controller(model, view) {
- var self = this;
- self.model = model;
- self.view = view;
- self.view.bind('newTodo', function (title) {
- self.addItem(title);
- });
- self.view.bind('itemEdit', function (item) {
- self.editItem(item.id);
- });
- self.view.bind('itemEditDone', function (item) {
- self.editItemSave(item.id, item.title);
- });
- self.view.bind('itemEditCancel', function (item) {
- self.editItemCancel(item.id);
- });
- self.view.bind('itemRemove', function (item) {
- self.removeItem(item.id);
- });
- self.view.bind('itemToggle', function (item) {
- self.toggleComplete(item.id, item.completed);
- });
- self.view.bind('removeCompleted', function () {
- self.removeCompletedItems();
- });
- self.view.bind('toggleAll', function (status) {
- self.toggleAll(status.completed);
- });
- }
- /**
- * Loads and initialises the view
- *
- * @param {string} '' | 'active' | 'completed'
- */
- Controller.prototype.setView = function (locationHash) {
- var route = locationHash.split('/')[1];
- var page = route || '';
- this._updateFilterState(page);
- };
- /**
- * An event to fire on load. Will get all items and display them in the
- * todo-list
- */
- Controller.prototype.showAll = function () {
- var self = this;
- self.model.read(function (data) {
- self.view.render('showEntries', data);
- });
- };
- /**
- * Renders all active tasks
- */
- Controller.prototype.showActive = function () {
- var self = this;
- self.model.read({ completed: false }, function (data) {
- self.view.render('showEntries', data);
- });
- };
- /**
- * Renders all completed tasks
- */
- Controller.prototype.showCompleted = function () {
- var self = this;
- self.model.read({ completed: true }, function (data) {
- self.view.render('showEntries', data);
- });
- };
- /**
- * An event to fire whenever you want to add an item. Simply pass in the event
- * object and it'll handle the DOM insertion and saving of the new item.
- */
- Controller.prototype.addItem = function (title) {
- var self = this;
- if (title.trim() === '') {
- return;
- }
- self.model.create(title, function () {
- self.view.render('clearNewTodo');
- self._filter(true);
- });
- };
- /*
- * Triggers the item editing mode.
- */
- Controller.prototype.editItem = function (id) {
- var self = this;
- self.model.read(id, function (data) {
- self.view.render('editItem', {id: id, title: data[0].title});
- });
- };
- /*
- * Finishes the item editing mode successfully.
- */
- Controller.prototype.editItemSave = function (id, title) {
- var self = this;
- title = title.trim();
- if (title.length !== 0) {
- self.model.update(id, {title: title}, function () {
- self.view.render('editItemDone', {id: id, title: title});
- });
- } else {
- self.removeItem(id);
- }
- };
- /*
- * Cancels the item editing mode.
- */
- Controller.prototype.editItemCancel = function (id) {
- var self = this;
- self.model.read(id, function (data) {
- self.view.render('editItemDone', {id: id, title: data[0].title});
- });
- };
- /**
- * By giving it an ID it'll find the DOM element matching that ID,
- * remove it from the DOM and also remove it from storage.
- *
- * @param {number} id The ID of the item to remove from the DOM and
- * storage
- */
- Controller.prototype.removeItem = function (id) {
- var self = this;
- self.model.remove(id, function () {
- self.view.render('removeItem', id);
- });
- self._filter();
- };
- /**
- * Will remove all completed items from the DOM and storage.
- */
- Controller.prototype.removeCompletedItems = function () {
- var self = this;
- self.model.read({ completed: true }, function (data) {
- data.forEach(function (item) {
- self.removeItem(item.id);
- });
- });
- self._filter();
- };
- /**
- * Give it an ID of a model and a checkbox and it will update the item
- * in storage based on the checkbox's state.
- *
- * @param {number} id The ID of the element to complete or uncomplete
- * @param {object} checkbox The checkbox to check the state of complete
- * or not
- * @param {boolean|undefined} silent Prevent re-filtering the todo items
- */
- Controller.prototype.toggleComplete = function (id, completed, silent) {
- var self = this;
- self.model.update(id, { completed: completed }, function () {
- self.view.render('elementComplete', {
- id: id,
- completed: completed
- });
- });
- if (!silent) {
- self._filter();
- }
- };
- /**
- * Will toggle ALL checkboxes' on/off state and completeness of models.
- * Just pass in the event object.
- */
- Controller.prototype.toggleAll = function (completed) {
- var self = this;
- self.model.read({ completed: !completed }, function (data) {
- data.forEach(function (item) {
- self.toggleComplete(item.id, completed, true);
- });
- });
- self._filter();
- };
- /**
- * Updates the pieces of the page which change depending on the remaining
- * number of todos.
- */
- Controller.prototype._updateCount = function () {
- var self = this;
- self.model.getCount(function (todos) {
- self.view.render('updateElementCount', todos.active);
- self.view.render('clearCompletedButton', {
- completed: todos.completed,
- visible: todos.completed > 0
- });
- self.view.render('toggleAll', {checked: todos.completed === todos.total});
- self.view.render('contentBlockVisibility', {visible: todos.total > 0});
- });
- };
- /**
- * Re-filters the todo items, based on the active route.
- * @param {boolean|undefined} force forces a re-painting of todo items.
- */
- Controller.prototype._filter = function (force) {
- var activeRoute = this._activeRoute.charAt(0).toUpperCase() + this._activeRoute.substr(1);
- // Update the elements on the page, which change with each completed todo
- this._updateCount();
- // If the last active route isn't "All", or we're switching routes, we
- // re-create the todo item elements, calling:
- // this.show[All|Active|Completed]();
- if (force || this._lastActiveRoute !== 'All' || this._lastActiveRoute !== activeRoute) {
- this['show' + activeRoute]();
- }
- this._lastActiveRoute = activeRoute;
- };
- /**
- * Simply updates the filter nav's selected states
- */
- Controller.prototype._updateFilterState = function (currentPage) {
- // Store a reference to the active route, allowing us to re-filter todo
- // items as they are marked complete or incomplete.
- this._activeRoute = currentPage;
- if (currentPage === '') {
- this._activeRoute = 'All';
- }
- this._filter();
- this.view.render('setFilter', currentPage);
- };
- // Export to window
- window.app = window.app || {};
- window.app.Controller = Controller;
- })(window);
- </script>
- <script>
- /*global app, $on */
- (function () {
- 'use strict';
- /**
- * Sets up a brand new Todo list.
- *
- * @param {string} name The name of your new to do list.
- */
- function Todo(name) {
- this.storage = new app.Store(name);
- this.model = new app.Model(this.storage);
- this.template = new app.Template();
- this.view = new app.View(this.template);
- this.controller = new app.Controller(this.model, this.view);
- }
- var todo = new Todo('todos-vanillajs');
- function setView() {
- todo.controller.setView(document.location.hash);
- }
- $on(window, 'load', setView);
- $on(window, 'hashchange', setView);
- })();
- </script>
- </body>
- </html>
|