Commit 63b63f46 authored by TasteBot's avatar TasteBot

update the build files for gh-pages [ci skip]

parent 26747e55
module Task where
import Html (..)
import Html.Attributes (..)
import Html.Events (..)
import Json.Decode as Json
import LocalChannel as LC
import Maybe
import Signal
import String
-- MODEL
type alias Model =
{ description : String
, completed : Bool
, edits : Maybe String
, id : Int
}
init : String -> Int -> Model
init desc id =
{ description = desc
, completed = False
, edits = Nothing
, id = id
}
-- UPDATE
type Action
= Focus
| Edit String
| Cancel
| Commit
| Completed Bool
| Delete
update : Action -> Model -> Maybe Model
update update task =
case update of
Focus ->
Just { task | edits <- Just task.description }
Edit description ->
Just { task | edits <- Just description }
Cancel ->
Just { task | edits <- Nothing }
Commit ->
case task.edits of
Nothing ->
Just task
Just rawDescription ->
let description = String.trim rawDescription in
if String.isEmpty description then Nothing else
Just
{ task |
edits <- Nothing,
description <- description
}
Completed bool ->
Just { task | completed <- bool }
Delete ->
Nothing
-- VIEW
view : LC.LocalChannel (Int, Action) -> Model -> Html
view channel task =
let className =
(if task.completed then "completed " else "") ++
case task.edits of
Just _ -> "editing"
Nothing -> ""
description =
Maybe.withDefault task.description task.edits
in
li
[ class className ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked task.completed
, onClick (LC.send channel (task.id, Completed (not task.completed)))
]
[]
, label
[ onDoubleClick (LC.send channel (task.id, Focus)) ]
[ text description ]
, button
[ class "destroy"
, onClick (LC.send channel (task.id, Delete))
]
[]
]
, input
[ class "edit"
, value description
, name "title"
, id ("todo-" ++ toString task.id)
, on "input" targetValue (\desc -> LC.send channel (task.id, Edit desc))
, onBlur (LC.send channel (task.id, Commit))
, onFinish
(LC.send channel (task.id, Commit))
(LC.send channel (task.id, Cancel))
]
[]
]
onFinish : Signal.Message -> Signal.Message -> Attribute
onFinish enterMessage escapeMessage =
let select key =
case key of
13 -> Ok enterMessage
27 -> Ok escapeMessage
_ -> Err "Not a 'finish' key, such as ENTER or ESCAPE"
in
on "keydown" (Json.customDecoder keyCode select) identity
This diff is collapsed.
module Todo.Task exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Json.Decode
import String
-- MODEL
type alias Model =
{ description : String
, completed : Bool
, edits : Maybe String
, id : Int
}
init : String -> Int -> Model
init desc id =
{ description = desc
, completed = False
, edits = Nothing
, id = id
}
-- UPDATE
type Msg
= Focus String
| Edit String
| Cancel
| Commit
| Completed Bool
| Delete
update : Msg -> Model -> Maybe Model
update msg model =
case msg of
Focus elementId ->
Just { model | edits = Just model.description }
Edit description ->
Just { model | edits = Just description }
Cancel ->
Just { model | edits = Nothing }
Commit ->
case model.edits of
Nothing ->
Just model
Just rawDescription ->
let
description =
String.trim rawDescription
in
if String.isEmpty description then
Nothing
else
Just
{ model
| edits = Nothing
, description = description
}
Completed bool ->
Just { model | completed = bool }
Delete ->
Nothing
-- VIEW
view : Model -> Html Msg
view model =
let
className =
(if model.completed then
"completed "
else
""
)
++ case model.edits of
Just _ ->
"editing"
Nothing ->
""
description =
Maybe.withDefault model.description model.edits
elementId =
"todo-" ++ toString model.id
in
li
[ class className ]
[ div
[ class "view" ]
[ input
[ class "toggle"
, type' "checkbox"
, checked model.completed
, onClick (Completed (not model.completed))
]
[]
, label
[ onDoubleClick (Focus elementId) ]
[ text description ]
, button
[ class "destroy"
, onClick Delete
]
[]
]
, input
[ class "edit"
, value description
, name "title"
, id (elementId)
, onInput Edit
, onBlur Commit
, onFinish Commit Cancel
]
[]
]
onFinish : msg -> msg -> Attribute msg
onFinish enterMessage escapeMessage =
let
select key =
case key of
13 ->
enterMessage
_ ->
-- Not a 'finish' key, such as ENTER or ESCAPE
escapeMessage
in
on "keydown" (Json.Decode.map select keyCode)
This diff is collapsed.
......@@ -8,8 +8,11 @@
],
"exposed-modules": [],
"dependencies": {
"elm-lang/core": "1.0.0 <= v < 2.0.0",
"evancz/elm-html": "1.0.0 <= v < 2.0.0",
"evancz/local-channel": "1.0.0 <= v < 2.0.0"
}
}
\ No newline at end of file
"elm-community/string-extra": "1.0.2 <= v < 2.0.0",
"elm-lang/core": "4.0.5 <= v < 5.0.0",
"elm-lang/dom": "1.1.0 <= v < 2.0.0",
"elm-lang/html": "1.1.0 <= v < 2.0.0",
"elm-lang/navigation": "1.0.0 <= v < 2.0.0"
},
"elm-version": "0.17.1 <= v < 0.18.0"
}
This diff is collapsed.
......@@ -16,47 +16,17 @@
</head>
<body>
<script src="elm.js"></script>
<script src="build/elm.js"></script>
<script>
(function () {
var result = localStorage.getItem('elm-todo-model');
var savedModel = result ? JSON.parse(result) : null;
var todomvc = Elm.fullscreen(Elm.Todo, {
savedModel: savedModel,
route: getRoute()
});
var todomvc = Elm.Todo.fullscreen(savedModel);
todomvc.ports.save.subscribe(function (model) {
localStorage.setItem('elm-todo-model', JSON.stringify(model));
});
// Routing
window.addEventListener('popstate', function () {
todomvc.ports.route.send(getRoute());
}, false);
function getRoute() {
var hash = location.href.split('#')[1] || '';
var route = hash.replace('/', '');
if (['all', 'active', 'completed'].indexOf(route) >= 0) {
return route[0].toUpperCase() + route.substr(1);
}
return 'All';
}
// Setting focus manually
todomvc.ports.focus.subscribe(function (id) {
setTimeout(function () {
if (id === null) {
return;
}
var node = document.getElementById('todo-' + id);
if (document.activeElement !== node) {
node.focus();
}
}, 50);
});
}());
</script>
<script async src="node_modules/todomvc-common/base.js"></script>
......
......@@ -15,12 +15,9 @@ button {
font-weight: inherit;
color: inherit;
-webkit-appearance: none;
-ms-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
......@@ -32,22 +29,19 @@ body {
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
button,
input[type="checkbox"] {
outline: none;
:focus {
outline: 0;
}
.hidden {
display: none;
}
#todoapp {
.todoapp {
background: #fff;
margin: 130px 0 40px 0;
position: relative;
......@@ -55,25 +49,25 @@ input[type="checkbox"] {
0 25px 50px 0 rgba(0, 0, 0, 0.1);
}
#todoapp input::-webkit-input-placeholder {
.todoapp input::-webkit-input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::-moz-placeholder {
.todoapp input::-moz-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp input::input-placeholder {
.todoapp input::input-placeholder {
font-style: italic;
font-weight: 300;
color: #e6e6e6;
}
#todoapp h1 {
.todoapp h1 {
position: absolute;
top: -155px;
width: 100%;
......@@ -83,11 +77,10 @@ input[type="checkbox"] {
color: rgba(175, 47, 47, 0.15);
-webkit-text-rendering: optimizeLegibility;
-moz-text-rendering: optimizeLegibility;
-ms-text-rendering: optimizeLegibility;
text-rendering: optimizeLegibility;
}
#new-todo,
.new-todo,
.edit {
position: relative;
margin: 0;
......@@ -97,27 +90,23 @@ input[type="checkbox"] {
font-weight: inherit;
line-height: 1.4em;
border: 0;
outline: none;
color: inherit;
padding: 6px;
border: 1px solid #999;
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
-ms-box-sizing: border-box;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
-ms-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
#new-todo {
.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 {
.main {
position: relative;
z-index: 2;
border-top: 1px solid #e6e6e6;
......@@ -127,7 +116,7 @@ label[for='toggle-all'] {
display: none;
}
#toggle-all {
.toggle-all {
position: absolute;
top: -55px;
left: -12px;
......@@ -137,50 +126,50 @@ label[for='toggle-all'] {
border: none; /* Mobile Safari */
}
#toggle-all:before {
.toggle-all:before {
content: '❯';
font-size: 22px;
color: #e6e6e6;
padding: 10px 27px 10px 27px;
}
#toggle-all:checked:before {
.toggle-all:checked:before {
color: #737373;
}
#todo-list {
.todo-list {
margin: 0;
padding: 0;
list-style: none;
}
#todo-list li {
.todo-list li {
position: relative;
font-size: 24px;
border-bottom: 1px solid #ededed;
}
#todo-list li:last-child {
.todo-list li:last-child {
border-bottom: none;
}
#todo-list li.editing {
.todo-list li.editing {
border-bottom: none;
padding: 0;
}
#todo-list li.editing .edit {
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
#todo-list li.editing .view {
.todo-list li.editing .view {
display: none;
}
#todo-list li .toggle {
.todo-list li .toggle {
text-align: center;
width: 40px;
/* auto, since non-WebKit browsers doesn't support input styling */
......@@ -191,20 +180,18 @@ label[for='toggle-all'] {
margin: auto 0;
border: none; /* Mobile Safari */
-webkit-appearance: none;
-ms-appearance: none;
appearance: none;
}
#todo-list li .toggle:after {
.todo-list li .toggle:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#ededed" stroke-width="3"/></svg>');
}
#todo-list li .toggle:checked:after {
.todo-list li .toggle:checked:after {
content: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="-10 -18 100 135"><circle cx="50" cy="50" r="50" fill="none" stroke="#bddad5" stroke-width="3"/><path fill="#5dc2af" d="M72 25L42 71 27 56l-4 4 20 20 34-52z"/></svg>');
}
#todo-list li label {
white-space: pre-line;
.todo-list li label {
word-break: break-all;
padding: 15px 60px 15px 15px;
margin-left: 45px;
......@@ -213,12 +200,12 @@ label[for='toggle-all'] {
transition: color 0.4s;
}
#todo-list li.completed label {
.todo-list li.completed label {
color: #d9d9d9;
text-decoration: line-through;
}
#todo-list li .destroy {
.todo-list li .destroy {
display: none;
position: absolute;
top: 0;
......@@ -233,27 +220,27 @@ label[for='toggle-all'] {
transition: color 0.2s ease-out;
}
#todo-list li .destroy:hover {
.todo-list li .destroy:hover {
color: #af5b5e;
}
#todo-list li .destroy:after {
.todo-list li .destroy:after {
content: '×';
}
#todo-list li:hover .destroy {
.todo-list li:hover .destroy {
display: block;
}
#todo-list li .edit {
.todo-list li .edit {
display: none;
}
#todo-list li.editing:last-child {
.todo-list li.editing:last-child {
margin-bottom: -1px;
}
#footer {
.footer {
color: #777;
padding: 10px 15px;
height: 20px;
......@@ -261,7 +248,7 @@ label[for='toggle-all'] {
border-top: 1px solid #e6e6e6;
}
#footer:before {
.footer:before {
content: '';
position: absolute;
right: 0;
......@@ -276,16 +263,16 @@ label[for='toggle-all'] {
0 17px 2px -6px rgba(0, 0, 0, 0.2);
}
#todo-count {
.todo-count {
float: left;
text-align: left;
}
#todo-count strong {
.todo-count strong {
font-weight: 300;
}
#filters {
.filters {
margin: 0;
padding: 0;
list-style: none;
......@@ -294,11 +281,11 @@ label[for='toggle-all'] {
left: 0;
}
#filters li {
.filters li {
display: inline;
}
#filters li a {
.filters li a {
color: inherit;
margin: 3px;
padding: 3px 7px;
......@@ -307,39 +294,28 @@ label[for='toggle-all'] {
border-radius: 3px;
}
#filters li a.selected,
#filters li a:hover {
.filters li a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
#filters li a.selected {
.filters li a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
#clear-completed,
html #clear-completed:active {
.clear-completed,
html .clear-completed:active {
float: right;
position: relative;
line-height: 20px;
text-decoration: none;
cursor: pointer;
visibility: hidden;
position: relative;
}
#clear-completed::after {
visibility: visible;
content: 'Clear completed';
position: absolute;
right: 0;
white-space: nowrap;
}
#clear-completed:hover::after {
.clear-completed:hover {
text-decoration: underline;
}
#info {
.info {
margin: 65px auto 0;
color: #bfbfbf;
font-size: 10px;
......@@ -347,17 +323,17 @@ html #clear-completed:active {
text-align: center;
}
#info p {
.info p {
line-height: 1;
}
#info a {
.info a {
color: inherit;
text-decoration: none;
font-weight: 400;
}
#info a:hover {
.info a:hover {
text-decoration: underline;
}
......@@ -366,16 +342,16 @@ html #clear-completed:active {
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 {
.toggle-all,
.todo-list li .toggle {
background: none;
}
#todo-list li .toggle {
.todo-list li .toggle {
height: 40px;
}
#toggle-all {
.toggle-all {
-webkit-transform: rotate(90deg);
transform: rotate(90deg);
-webkit-appearance: none;
......@@ -384,11 +360,11 @@ html #clear-completed:active {
}
@media (max-width: 430px) {
#footer {
.footer {
height: 50px;
}
#filters {
.filters {
bottom: 10px;
}
}
......@@ -114,7 +114,12 @@
})({});
if (location.hostname === 'todomvc.com') {
window._gaq = [['_setAccount','UA-31081062-1'],['_trackPageview']];(function(d,t){var g=d.createElement(t),s=d.getElementsByTagName(t)[0];g.src='//www.google-analytics.com/ga.js';s.parentNode.insertBefore(g,s)}(document,'script'));
(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 */
......@@ -228,7 +233,7 @@
xhr.onload = function (e) {
var parsedResponse = JSON.parse(e.target.responseText);
if (parsedResponse instanceof Array) {
var count = parsedResponse.length
var count = parsedResponse.length;
if (count !== 0) {
issueLink.innerHTML = 'This app has ' + count + ' open issues';
document.getElementById('issue-count').style.display = 'inline';
......
{
"private": true,
"dependencies": {
"todomvc-app-css": "^1.0.0",
"todomvc-common": "^1.0.1"
"todomvc-app-css": "^2.0.6",
"todomvc-common": "^1.0.2"
}
}
# Elm TodoMVC Example
> A functional reactive language for interactive applications
> A functional language for interactive applications
> _[Elm](http://elm-lang.org/)_
......@@ -14,7 +14,7 @@ Here are some links you may find helpful:
* [Try Elm](http://elm-lang.org/try)
* [Learn Elm](http://elm-lang.org/Learn.elm)
* [Elm Snippets](http://www.share-elm.com/)
* [An Introduction to Elm](http://guide.elm-lang.org/)
Get help from other Elm users:
......@@ -28,25 +28,25 @@ _If you have other helpful links to share, or find any of the links above no lon
## Project Structure
All of the Elm code lives in `Todo.elm` and `Task.elm` and relies
on the [elm-html][] library.
All of the Elm code lives in `Todo.elm` and `Todo/Task.elm` and relies
on the [elm-html][] and [elm-navigation][] packages.
[elm-html]: http://library.elm-lang.org/catalog/evancz-elm-html/latest
[elm-html]: http://package.elm-lang.org/packages/elm-lang/html/latest/
[elm-navigation]: http://package.elm-lang.org/packages/elm-lang/navigation/latest/
There also is a port handler set up in `index.html` to set the focus on
particular text fields when necessary.
## Build Instructions
You need to install
[elm](https://github.com/elm-lang/elm-platform/blob/master/README.md#elm-platform)
You need to install [elm](http://elm-lang.org/install)
on your machine first.
Run the following commands from the root of this project:
```bash
elm-package install
elm-make Todo.elm --output build/Todo.js
elm-package install -y
elm-make Todo.elm --output build/elm.js
```
Then open `index.html` in your browser!
......
# Vanilla ES6 (ES2015) • [TodoMVC](http://todomvc.com)
> An exact port of the [Vanilla JS Example](http://todomvc.com/examples/vanillajs/), but translated into ES6, also known as ES2015.
> A port of the [Vanilla JS Example](http://todomvc.com/examples/vanillajs/), but translated into ES6, also known as ES2015.
## Learning ES6
......@@ -34,9 +34,10 @@ npm run compile
## Implementation
Uses [Babel JS](https://babeljs.io/) to compile ES6 code to ES5, which is then readable by all browsers.
Uses [Google Closure Compiler](https://developers.google.com/closure/compiler/) to compile ES6 code to ES5, which is then readable by all browsers.
## Credit
Created by [Luke Edwards](http://www.lukeed.com)
Refactored by [Aaron Muir Hamilton](https://github.com/xorgy)
'use strict';
var _controller = require('./controller');
var _controller2 = _interopRequireDefault(_controller);
var _helpers = require('./helpers');
var helpers = _interopRequireWildcard(_helpers);
var _template = require('./template');
var _template2 = _interopRequireDefault(_template);
var _store = require('./store');
var _store2 = _interopRequireDefault(_store);
var _model = require('./model');
var _model2 = _interopRequireDefault(_model);
var _view = require('./view');
var _view2 = _interopRequireDefault(_view);
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var $on = helpers.$on;
var setView = function setView() {
return todo.controller.setView(document.location.hash);
};
var Todo =
/**
* Init new Todo List
* @param {string} The name of your list
*/
function Todo(name) {
_classCallCheck(this, Todo);
this.storage = new _store2.default(name);
this.model = new _model2.default(this.storage);
this.template = new _template2.default();
this.view = new _view2.default(this.template);
this.controller = new _controller2.default(this.model, this.view);
};
var todo = new Todo('todos-vanillajs');
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
\ No newline at end of file
This diff is collapsed.
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var Controller = function () {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
*/
function Controller(model, view) {
var _this = this;
_classCallCheck(this, Controller);
this.model = model;
this.view = view;
this.view.bind('newTodo', function (title) {
return _this.addItem(title);
});
this.view.bind('itemEdit', function (item) {
return _this.editItem(item.id);
});
this.view.bind('itemEditDone', function (item) {
return _this.editItemSave(item.id, item.title);
});
this.view.bind('itemEditCancel', function (item) {
return _this.editItemCancel(item.id);
});
this.view.bind('itemRemove', function (item) {
return _this.removeItem(item.id);
});
this.view.bind('itemToggle', function (item) {
return _this.toggleComplete(item.id, item.completed);
});
this.view.bind('removeCompleted', function () {
return _this.removeCompletedItems();
});
this.view.bind('toggleAll', function (status) {
return _this.toggleAll(status.completed);
});
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
_createClass(Controller, [{
key: 'setView',
value: function setView(hash) {
var route = hash.split('/')[1];
var page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
}, {
key: 'showAll',
value: function showAll() {
var _this2 = this;
this.model.read(function (data) {
return _this2.view.render('showEntries', data);
});
}
/**
* Renders all active tasks
*/
}, {
key: 'showActive',
value: function showActive() {
var _this3 = this;
this.model.read({ completed: false }, function (data) {
return _this3.view.render('showEntries', data);
});
}
/**
* Renders all completed tasks
*/
}, {
key: 'showCompleted',
value: function showCompleted() {
var _this4 = this;
this.model.read({ completed: true }, function (data) {
return _this4.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.
*/
}, {
key: 'addItem',
value: function addItem(title) {
var _this5 = this;
if (title.trim() === '') {
return;
}
this.model.create(title, function () {
_this5.view.render('clearNewTodo');
_this5._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
}, {
key: 'editItem',
value: function editItem(id) {
var _this6 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this6.view.render('editItem', { id: id, title: title });
});
}
/*
* Finishes the item editing mode successfully.
*/
}, {
key: 'editItemSave',
value: function editItemSave(id, title) {
var _this7 = this;
title = title.trim();
if (title.length !== 0) {
this.model.update(id, { title: title }, function () {
_this7.view.render('editItemDone', { id: id, title: title });
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
*/
}, {
key: 'editItemCancel',
value: function editItemCancel(id) {
var _this8 = this;
this.model.read(id, function (data) {
var title = data[0].title;
_this8.view.render('editItemDone', { id: id, title: title });
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
*/
}, {
key: 'removeItem',
value: function removeItem(id) {
var _this9 = this;
this.model.remove(id, function () {
return _this9.view.render('removeItem', id);
});
this._filter();
}
/**
* Will remove all completed items from the DOM and storage.
*/
}, {
key: 'removeCompletedItems',
value: function removeCompletedItems() {
var _this10 = this;
this.model.read({ completed: true }, function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var item = _step.value;
_this10.removeItem(item.id);
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
});
this._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
*/
}, {
key: 'toggleComplete',
value: function toggleComplete(id, completed, silent) {
var _this11 = this;
this.model.update(id, { completed: completed }, function () {
_this11.view.render('elementComplete', { id: id, completed: completed });
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
*/
}, {
key: 'toggleAll',
value: function toggleAll(completed) {
var _this12 = this;
this.model.read({ completed: !completed }, function (data) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = data[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
_this12.toggleComplete(item.id, completed, true);
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
});
this._filter();
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
}, {
key: '_updateCount',
value: function _updateCount() {
var _this13 = this;
this.model.getCount(function (todos) {
var completed = todos.completed;
var visible = completed > 0;
var checked = completed === todos.total;
_this13.view.render('updateElementCount', todos.active);
_this13.view.render('clearCompletedButton', { completed: completed, visible: visible });
_this13.view.render('toggleAll', { checked: checked });
_this13.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.
*/
}, {
key: '_filter',
value: function _filter(force) {
var active = this._activeRoute;
var activeRoute = active.charAt(0).toUpperCase() + active.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
*/
}, {
key: '_updateFilter',
value: function _updateFilter(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);
}
}]);
return Controller;
}();
exports.default = Controller;
\ No newline at end of file
'use strict';
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.qs = qs;
exports.qsa = qsa;
exports.$on = $on;
exports.$delegate = $delegate;
exports.$parent = $parent;
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
function $on(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
function $delegate(target, selector, type, handler) {
var dispatchEvent = function dispatchEvent(event) {
var targetElement = event.target;
var potentialElements = qsa(selector, target);
var hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
var useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
\ No newline at end of file
'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
var Model = function () {
function Model(storage) {
_classCallCheck(this, Model);
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
*/
_createClass(Model, [{
key: 'create',
value: function create(title, callback) {
title = title || '';
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' })
*/
}, {
key: 'read',
value: function read(query, callback) {
var queryType = typeof query === 'undefined' ? 'undefined' : _typeof(query);
if (queryType === 'function') {
this.storage.findAll(query);
} 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.
*/
}, {
key: 'update',
value: function update(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.
*/
}, {
key: 'remove',
value: function remove(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.
*/
}, {
key: 'removeAll',
value: function removeAll(callback) {
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
}, {
key: 'getCount',
value: function getCount(callback) {
var todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(function (data) {
var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;
try {
for (var _iterator = data[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
var todo = _step.value;
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
} catch (err) {
_didIteratorError = true;
_iteratorError = err;
} finally {
try {
if (!_iteratorNormalCompletion && _iterator.return) {
_iterator.return();
}
} finally {
if (_didIteratorError) {
throw _iteratorError;
}
}
}
callback(todos);
});
}
}]);
return Model;
}();
exports.default = Model;
\ No newline at end of file
"use strict";
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
/*jshint eqeqeq:false */
/**
* 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
*/
var Store = function () {
function Store(name, callback) {
_classCallCheck(this, Store);
this._dbName = name;
if (!localStorage[name]) {
var data = {
todos: []
};
localStorage[name] = JSON.stringify(data);
}
if (callback) {
callback.call(this, JSON.parse(localStorage[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
* })
*/
_createClass(Store, [{
key: "find",
value: function find(query, callback) {
var todos = JSON.parse(localStorage[this._dbName]).todos;
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
*/
}, {
key: "findAll",
value: function findAll(callback) {
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* 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
*/
}, {
key: "save",
value: function save(updateData, callback, id) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
// If an ID was actually given, find the item and update each property
if (id) {
for (var i = 0; i < len; i++) {
if (todos[i].id === id) {
for (var key in updateData) {
todos[i][key] = updateData[key];
}
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
} else {
// Generate an ID
updateData.id = new Date().getTime();
todos.push(updateData);
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
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
*/
}, {
key: "remove",
value: function remove(id, callback) {
var data = JSON.parse(localStorage[this._dbName]);
var todos = data.todos;
var len = todos.length;
for (var i = 0; i < todos.length; i++) {
if (todos[i].id == id) {
todos.splice(i, 1);
break;
}
}
localStorage[this._dbName] = JSON.stringify(data);
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
/**
* Will drop all storage and start fresh
*
* @param {function} callback The callback to fire after dropping the data
*/
}, {
key: "drop",
value: function drop(callback) {
localStorage[this._dbName] = JSON.stringify({ todos: [] });
if (callback) {
callback.call(this, JSON.parse(localStorage[this._dbName]).todos);
}
}
}]);
return Store;
}();
exports.default = Store;
\ No newline at end of file
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
var reUnescapedHtml = /[&<>"'`]/g;
var reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
var escape = function escape(str) {
return str && reHasUnescapedHtml.test(str) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
};
var escapeHtmlChar = function escapeHtmlChar(chr) {
return htmlEscapes[chr];
};
var Template = function () {
function Template() {
_classCallCheck(this, Template);
this.defaultTemplate = '\n\t\t\t<li data-id="{{id}}" class="{{completed}}">\n\t\t\t\t<div class="view">\n\t\t\t\t\t<input class="toggle" type="checkbox" {{checked}}>\n\t\t\t\t\t<label>{{title}}</label>\n\t\t\t\t\t<button class="destroy"></button>\n\t\t\t\t</div>\n\t\t\t</li>\n\t\t';
}
/**
* 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,
* })
*/
_createClass(Template, [{
key: 'show',
value: function show(data) {
var _this = this;
var view = data.map(function (d) {
var template = _this.defaultTemplate;
var completed = d.completed ? 'completed' : '';
var checked = d.completed ? 'checked' : '';
return _this.defaultTemplate.replace('{{id}}', d.id).replace('{{title}}', escape(d.title)).replace('{{completed}}', completed).replace('{{checked}}', checked);
});
return view.join('');
}
/**
* 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
*/
}, {
key: 'itemCounter',
value: function itemCounter(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
*/
}, {
key: 'clearCompletedButton',
value: function clearCompletedButton(completedTodos) {
return completedTodos > 0 ? 'Clear completed' : '';
}
}]);
return Template;
}();
exports.default = Template;
\ No newline at end of file
'use strict';
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
Object.defineProperty(exports, "__esModule", {
value: true
});
var _helpers = require('./helpers');
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var _itemId = function _itemId(element) {
return parseInt((0, _helpers.$parent)(element, 'li').dataset.id, 10);
};
var _setFilter = function _setFilter(currentPage) {
(0, _helpers.qs)('.filters .selected').className = '';
(0, _helpers.qs)('.filters [href="#/' + currentPage + '"]').className = 'selected';
};
var _elementComplete = function _elementComplete(id, completed) {
var listItem = (0, _helpers.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
(0, _helpers.qs)('input', listItem).checked = completed;
};
var _editItem = function _editItem(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
listItem.className += ' editing';
var input = document.createElement('input');
input.className = 'edit';
listItem.appendChild(input);
input.focus();
input.value = title;
};
/**
* 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
*/
var View = function () {
function View(template) {
var _this = this;
_classCallCheck(this, View);
this.template = template;
this.ENTER_KEY = 13;
this.ESCAPE_KEY = 27;
this.$todoList = (0, _helpers.qs)('.todo-list');
this.$todoItemCounter = (0, _helpers.qs)('.todo-count');
this.$clearCompleted = (0, _helpers.qs)('.clear-completed');
this.$main = (0, _helpers.qs)('.main');
this.$footer = (0, _helpers.qs)('.footer');
this.$toggleAll = (0, _helpers.qs)('.toggle-all');
this.$newTodo = (0, _helpers.qs)('.new-todo');
this.viewCommands = {
showEntries: function showEntries(parameter) {
return _this.$todoList.innerHTML = _this.template.show(parameter);
},
removeItem: function removeItem(parameter) {
return _this._removeItem(parameter);
},
updateElementCount: function updateElementCount(parameter) {
return _this.$todoItemCounter.innerHTML = _this.template.itemCounter(parameter);
},
clearCompletedButton: function clearCompletedButton(parameter) {
return _this._clearCompletedButton(parameter.completed, parameter.visible);
},
contentBlockVisibility: function contentBlockVisibility(parameter) {
return _this.$main.style.display = _this.$footer.style.display = parameter.visible ? 'block' : 'none';
},
toggleAll: function toggleAll(parameter) {
return _this.$toggleAll.checked = parameter.checked;
},
setFilter: function setFilter(parameter) {
return _setFilter(parameter);
},
clearNewTodo: function clearNewTodo(parameter) {
return _this.$newTodo.value = '';
},
elementComplete: function elementComplete(parameter) {
return _elementComplete(parameter.id, parameter.completed);
},
editItem: function editItem(parameter) {
return _editItem(parameter.id, parameter.title);
},
editItemDone: function editItemDone(parameter) {
return _this._editItemDone(parameter.id, parameter.title);
}
};
}
_createClass(View, [{
key: '_removeItem',
value: function _removeItem(id) {
var elem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (elem) {
this.$todoList.removeChild(elem);
}
}
}, {
key: '_clearCompletedButton',
value: function _clearCompletedButton(completedCount, visible) {
this.$clearCompleted.innerHTML = this.template.clearCompletedButton(completedCount);
this.$clearCompleted.style.display = visible ? 'block' : 'none';
}
}, {
key: '_editItemDone',
value: function _editItemDone(id, title) {
var listItem = (0, _helpers.qs)('[data-id="' + id + '"]');
if (!listItem) {
return;
}
var input = (0, _helpers.qs)('input.edit', listItem);
listItem.removeChild(input);
listItem.className = listItem.className.replace(' editing', '');
(0, _helpers.qsa)('label', listItem).forEach(function (label) {
return label.textContent = title;
});
}
}, {
key: 'render',
value: function render(viewCmd, parameter) {
this.viewCommands[viewCmd](parameter);
}
}, {
key: '_bindItemEditDone',
value: function _bindItemEditDone(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'blur', function () {
if (!this.dataset.iscanceled) {
handler({
id: _itemId(this),
title: this.value
});
}
});
// Remove the cursor from the input when you hit enter just like if it were a real form
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keypress', function (event) {
if (event.keyCode === self.ENTER_KEY) {
this.blur();
}
});
}
}, {
key: '_bindItemEditCancel',
value: function _bindItemEditCancel(handler) {
var self = this;
(0, _helpers.$delegate)(self.$todoList, 'li .edit', 'keyup', function (event) {
if (event.keyCode === self.ESCAPE_KEY) {
var id = _itemId(this);
this.dataset.iscanceled = true;
this.blur();
handler({ id: id });
}
});
}
}, {
key: 'bind',
value: function bind(event, handler) {
var _this2 = this;
switch (event) {
case 'newTodo':
(0, _helpers.$on)(this.$newTodo, 'change', function () {
return handler(_this2.$newTodo.value);
});
break;
case 'removeCompleted':
(0, _helpers.$on)(this.$clearCompleted, 'click', handler);
break;
case 'toggleAll':
(0, _helpers.$on)(this.$toggleAll, 'click', function () {
handler({ completed: this.checked });
});
break;
case 'itemEdit':
(0, _helpers.$delegate)(this.$todoList, 'li label', 'dblclick', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemRemove':
(0, _helpers.$delegate)(this.$todoList, '.destroy', 'click', function () {
handler({ id: _itemId(this) });
});
break;
case 'itemToggle':
(0, _helpers.$delegate)(this.$todoList, '.toggle', 'click', function () {
handler({
id: _itemId(this),
completed: this.checked
});
});
break;
case 'itemEditDone':
this._bindItemEditDone(handler);
break;
case 'itemEditCancel':
this._bindItemEditCancel(handler);
break;
}
}
}]);
return View;
}();
exports.default = View;
\ No newline at end of file
<!DOCTYPE html>
<html>
<!doctype html>
<html lang="en" data-framework="es6">
<head>
<meta charset="utf-8">
<title>ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<title>Vanilla ES6 • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
</head>
<body>
......@@ -12,31 +11,29 @@
<h1>todos</h1>
<input class="new-todo" placeholder="What needs to be done?" autofocus>
</header>
<section class="main">
<section style="display:none" class="main">
<input class="toggle-all" type="checkbox">
<label for="toggle-all">Mark all as complete</label>
<ul class="todo-list"></ul>
<footer class="footer">
<span class="todo-count"></span>
<div class="filters">
<a href="#/" class="selected">All</a>
<a href="#/active">Active</a>
<a href="#/completed">Completed</a>
</div>
<button class="clear-completed">Clear completed</button>
</footer>
</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>
<br>
<p>Written by <a href="http://twitter.com/lukeed05">Luke Edwards</a></p>
<p>Refactored by <a href="https://github.com/xorgy">Aaron Muir Hamilton</a></p>
<p>Part of <a href="http://todomvc.com">TodoMVC</a></p>
</footer>
<script src="dist/bundle.js"></script>
<script src="node_modules/todomvc-common/base.js"></script>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
</body>
</html>
......@@ -17,8 +17,7 @@ button {
-webkit-appearance: none;
appearance: none;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
......@@ -30,8 +29,7 @@ body {
max-width: 550px;
margin: 0 auto;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-weight: 300;
}
......@@ -100,8 +98,7 @@ input[type="checkbox"] {
box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-font-smoothing: antialiased;
font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.new-todo {
......@@ -163,17 +160,19 @@ label[for='toggle-all'] {
padding: 0;
}
.todo-list li.editing button,
.todo-list li.editing label,
.todo-list li.editing .toggle{
display: none;
}
.todo-list li.editing .edit {
display: block;
width: 506px;
padding: 13px 17px 12px 17px;
padding: 12px 16px;
margin: 0 0 0 43px;
}
.todo-list li.editing .view {
display: none;
}
.todo-list li .toggle {
text-align: center;
width: 40px;
......@@ -287,11 +286,7 @@ label[for='toggle-all'] {
left: 0;
}
.filters li {
display: inline;
}
.filters li a {
.filters a {
color: inherit;
margin: 3px;
padding: 3px 7px;
......@@ -300,12 +295,12 @@ label[for='toggle-all'] {
border-radius: 3px;
}
.filters li a.selected,
.filters li a:hover {
.filters a.selected,
.filters a:hover {
border-color: rgba(175, 47, 47, 0.1);
}
.filters li a.selected {
.filters a.selected {
border-color: rgba(175, 47, 47, 0.2);
}
......
/* 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();
getFile('learn.json', Learn);
})();
{
"private": true,
"scripts": {
"compile": "babel src --presets es2015 --out-dir=dist && browserify dist/app.js > dist/bundle.js",
"compile": "java -jar node_modules/google-closure-compiler/compiler.jar -O ADVANCED --language_in=ES6_STRICT --new_type_inf --js_output_file='dist/bundle.js' 'src/**.js' -W VERBOSE",
"prepublish": "npm run compile"
},
"dependencies": {
......@@ -9,8 +9,6 @@
"todomvc-common": "^1.0.2"
},
"devDependencies": {
"babel-core": "^6.1.0",
"babel-preset-es2015": "^6.1.18",
"browserify": "^12.0.1"
"google-closure-compiler": "^20160315.2.0"
}
}
import Controller from './controller';
import * as helpers from './helpers';
import {$on} from './helpers';
import Template from './template';
import Store from './store';
import Model from './model';
import View from './view';
const $on = helpers.$on;
const setView = () => todo.controller.setView(document.location.hash);
const store = new Store('todos-vanilla-es6');
class Todo {
/**
* Init new Todo List
* @param {string} The name of your list
*/
constructor(name) {
this.storage = new Store(name);
this.model = new Model(this.storage);
const template = new Template();
const view = new View(template);
this.template = new Template();
this.view = new View(this.template);
this.controller = new Controller(this.model, this.view);
}
}
const todo = new Todo('todos-vanillajs');
/**
* @type {Controller}
*/
const controller = new Controller(store, view);
const setView = () => controller.setView(document.location.hash);
$on(window, 'load', setView);
$on(window, 'hashchange', setView);
import {emptyItemQuery} from './item';
import Store from './store';
import View from './view';
export default class Controller {
/**
* Take a model & view, then act as controller between them
* @param {object} model The model instance
* @param {object} view The view instance
* @param {!Store} store A Store instance
* @param {!View} view A View instance
*/
constructor(model, view) {
this.model = model;
constructor(store, view) {
this.store = store;
this.view = view;
this.view.bind('newTodo', title => this.addItem(title));
this.view.bind('itemEdit', item => this.editItem(item.id));
this.view.bind('itemEditDone', item => this.editItemSave(item.id, item.title));
this.view.bind('itemEditCancel', item => this.editItemCancel(item.id));
this.view.bind('itemRemove', item => this.removeItem(item.id));
this.view.bind('itemToggle', item => this.toggleComplete(item.id, item.completed));
this.view.bind('removeCompleted', () => this.removeCompletedItems());
this.view.bind('toggleAll', status => this.toggleAll(status.completed));
}
/**
* Load & Initialize the view
* @param {string} '' | 'active' | 'completed'
*/
setView(hash) {
const route = hash.split('/')[1];
const page = route || '';
this._updateFilter(page);
}
/**
* Event fires on load. Gets all items & displays them
*/
showAll() {
this.model.read(data => this.view.render('showEntries', data));
}
view.bindAddItem(this.addItem.bind(this));
view.bindEditItemSave(this.editItemSave.bind(this));
view.bindEditItemCancel(this.editItemCancel.bind(this));
view.bindRemoveItem(this.removeItem.bind(this));
view.bindToggleItem((id, completed) => {
this.toggleCompleted(id, completed);
this._filter();
});
view.bindRemoveCompleted(this.removeCompletedItems.bind(this));
view.bindToggleAll(this.toggleAll.bind(this));
/**
* Renders all active tasks
*/
showActive() {
this.model.read({completed: false}, data => this.view.render('showEntries', data));
this._activeRoute = '';
this._lastActiveRoute = null;
}
/**
* Renders all completed tasks
* Set and render the active route.
*
* @param {string} raw '' | '#/' | '#/active' | '#/completed'
*/
showCompleted() {
this.model.read({completed: true}, data => this.view.render('showEntries', data));
setView(raw) {
const route = raw.replace(/^#\//, '');
this._activeRoute = route;
this._filter();
this.view.updateFilterButtons(route);
}
/**
* 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.
* Add an Item to the Store and display it in the list.
*
* @param {!string} title Title of the new item
*/
addItem(title) {
if (title.trim() === '') {
return;
}
this.model.create(title, () => {
this.view.render('clearNewTodo');
this.store.insert({
id: Date.now(),
title,
completed: false
}, () => {
this.view.clearNewTodo();
this._filter(true);
});
}
/*
* Triggers the item editing mode.
*/
editItem(id) {
this.model.read(id, data => {
const title = data[0].title;
this.view.render('editItem', {id, title});
});
}
/*
* Finishes the item editing mode successfully.
/**
* Save an Item in edit.
*
* @param {number} id ID of the Item in edit
* @param {!string} title New title for the Item in edit
*/
editItemSave(id, title) {
title = title.trim();
if (title.length !== 0) {
this.model.update(id, {title}, () => {
this.view.render('editItemDone', {id, title});
if (title.length) {
this.store.update({id, title}, () => {
this.view.editItemDone(id, title);
});
} else {
this.removeItem(id);
}
}
/*
* Cancels the item editing mode.
/**
* Cancel the item editing mode.
*
* @param {!number} id ID of the Item in edit
*/
editItemCancel(id) {
this.model.read(id, data => {
this.store.find({id}, data => {
const title = data[0].title;
this.view.render('editItemDone', {id, title});
this.view.editItemDone(id, title);
});
}
/**
* Find the DOM element with given ID,
* Then remove it from DOM & Storage
* Remove the data and elements related to an Item.
*
* @param {!number} id Item ID of item to remove
*/
removeItem(id) {
this.model.remove(id, () => this.view.render('removeItem', id));
this._filter();
this.store.remove({id}, () => {
this._filter();
this.view.removeItem(id);
});
}
/**
* Will remove all completed items from the DOM and storage.
* Remove all completed items.
*/
removeCompletedItems() {
this.model.read({completed: true}, data => {
for (let item of data) {
this.removeItem(item.id);
}
});
this._filter();
this.store.remove({completed: true}, this._filter.bind(this));
}
/**
* Give it an ID of a model and a checkbox and it will update the item
* in storage based on the checkbox's state.
* Update an Item in storage based on the state of completed.
*
* @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
* @param {!number} id ID of the target Item
* @param {!boolean} completed Desired completed state
*/
toggleComplete(id, completed, silent) {
this.model.update(id, {completed}, () => {
this.view.render('elementComplete', {id, completed});
toggleCompleted(id, completed) {
this.store.update({id, completed}, () => {
this.view.setItemComplete(id, completed);
});
if (!silent) {
this._filter();
}
}
/**
* Will toggle ALL checkboxes' on/off state and completeness of models.
* Just pass in the event object.
* Set all items to complete or active.
*
* @param {boolean} completed Desired completed state
*/
toggleAll(completed) {
this.model.read({completed: !completed}, data => {
for (let item of data) {
this.toggleComplete(item.id, completed, true);
this.store.find({completed: !completed}, data => {
for (let {id} of data) {
this.toggleCompleted(id, completed);
}
});
......@@ -155,58 +129,31 @@ export default class Controller {
}
/**
* Updates the pieces of the page which change depending on the remaining
* number of todos.
*/
_updateCount() {
this.model.getCount(todos => {
const completed = todos.completed;
const visible = completed > 0;
const checked = completed === todos.total;
this.view.render('updateElementCount', todos.active);
this.view.render('clearCompletedButton', {completed, visible});
this.view.render('toggleAll', {checked});
this.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.
* Refresh the list based on the current route.
*
* @param {boolean} [force] Force a re-paint of the list
*/
_filter(force) {
const active = this._activeRoute;
const activeRoute = active.charAt(0).toUpperCase() + active.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]();
const route = this._activeRoute;
if (force || this._lastActiveRoute !== '' || this._lastActiveRoute !== route) {
/* jscs:disable disallowQuotedKeysInObjects */
this.store.find({
'': emptyItemQuery,
'active': {completed: false},
'completed': {completed: true}
}[route], this.view.showItems.bind(this.view));
/* jscs:enable disallowQuotedKeysInObjects */
}
this._lastActiveRoute = activeRoute;
}
/**
* Simply updates the filter nav's selected states
*/
_updateFilter(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;
this.store.count((total, active, completed) => {
this.view.setItemsLeft(active);
this.view.setClearCompletedButtonVisibility(completed);
if (currentPage === '') {
this._activeRoute = 'All';
}
this._filter();
this.view.setCompleteAllCheckbox(completed === total);
this.view.setMainVisibility(total);
});
this.view.render('setFilter', currentPage);
this._lastActiveRoute = route;
}
}
// Allow for looping on nodes by chaining:
// qsa('.foo').forEach(function () {})
NodeList.prototype.forEach = Array.prototype.forEach;
// Get element(s) by CSS selector:
/**
* querySelector wrapper
*
* @param {string} selector Selector to query
* @param {Element} [scope] Optional scope element for the selector
*/
export function qs(selector, scope) {
return (scope || document).querySelector(selector);
}
export function qsa(selector, scope) {
return (scope || document).querySelectorAll(selector);
}
// addEventListener wrapper:
export function $on(target, type, callback, useCapture) {
target.addEventListener(type, callback, !!useCapture);
/**
* addEventListener wrapper
*
* @param {Element|Window} target Target Element
* @param {string} type Event name to bind to
* @param {Function} callback Event callback
* @param {boolean} [capture] Capture the event
*/
export function $on(target, type, callback, capture) {
target.addEventListener(type, callback, !!capture);
}
// Attach a handler to event for all elements that match the selector,
// now or in the future, based on a root element
export function $delegate(target, selector, type, handler) {
/**
* Attach a handler to an event for all elements matching a selector.
*
* @param {Element} target Element which the event must bubble to
* @param {string} selector Selector to match
* @param {string} type Event name
* @param {Function} handler Function called when the event bubbles to target
* from an element matching selector
* @param {boolean} [capture] Capture the event
*/
export function $delegate(target, selector, type, handler, capture) {
const dispatchEvent = event => {
const targetElement = event.target;
const potentialElements = qsa(selector, target);
const hasMatch = Array.from(potentialElements).includes(targetElement);
if (hasMatch) {
handler.call(targetElement, event);
const potentialElements = target.querySelectorAll(selector);
let i = potentialElements.length;
while (i--) {
if (potentialElements[i] === targetElement) {
handler.call(targetElement, event);
break;
}
}
};
// https://developer.mozilla.org/en-US/docs/Web/Events/blur
const useCapture = type === 'blur' || type === 'focus';
$on(target, type, dispatchEvent, useCapture);
$on(target, type, dispatchEvent, !!capture);
}
// Find the element's parent with the given tag name:
// $parent(qs('a'), 'div')
export function $parent(element, tagName) {
if (!element.parentNode) {
return;
}
if (element.parentNode.tagName.toLowerCase() === tagName.toLowerCase()) {
return element.parentNode;
}
return $parent(element.parentNode, tagName);
}
/**
* Encode less-than and ampersand characters with entity codes to make user-
* provided text safe to parse as HTML.
*
* @param {string} s String to escape
*
* @returns {string} String with unsafe characters escaped with entity codes
*/
export const escapeForHTML = s => s.replace(/[&<]/g, c => c === '&' ? '&amp;' : '&lt;');
/**
* @typedef {!{id: number, completed: boolean, title: string}}
*/
export var Item;
/**
* @typedef {!Array<Item>}
*/
export var ItemList;
/**
* Enum containing a known-empty record type, matching only empty records unlike Object.
*
* @enum {Object}
*/
const Empty = {
Record: {}
};
/**
* Empty ItemQuery type, based on the Empty @enum.
*
* @typedef {Empty}
*/
export var EmptyItemQuery;
/**
* Reference to the only EmptyItemQuery instance.
*
* @type {EmptyItemQuery}
*/
export const emptyItemQuery = Empty.Record;
/**
* @typedef {!({id: number}|{completed: boolean}|EmptyItemQuery)}
*/
export var ItemQuery;
/**
* @typedef {!({id: number, title: string}|{id: number, completed: boolean})}
*/
export var ItemUpdate;
/**
* Creates a new Model instance and hooks up the storage.
* @constructor
* @param {object} storage A reference to the client side storage class
*/
export default class Model {
constructor(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
*/
create(title, callback){
title = title || '';
const 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' })
*/
read(query, callback){
const queryType = typeof query;
if (queryType === 'function') {
this.storage.findAll(query);
} 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.
*/
update(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.
*/
remove(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.
*/
removeAll(callback){
this.storage.drop(callback);
}
/**
* Returns a count of all todos
*/
getCount(callback){
const todos = {
active: 0,
completed: 0,
total: 0
};
this.storage.findAll(data => {
for (let todo of data) {
if (todo.completed) {
todos.completed++;
} else {
todos.active++;
}
todos.total++;
}
callback(todos);
});
}
}
This diff is collapsed.
const htmlEscapes = {
'&': '&amp',
'<': '&lt',
'>': '&gt',
'"': '&quot',
'\'': '&#x27',
'`': '&#x60'
};
import {ItemList} from './item';
const reUnescapedHtml = /[&<>"'`]/g;
const reHasUnescapedHtml = new RegExp(reUnescapedHtml.source);
const escape = str => (str && reHasUnescapedHtml.test(str)) ? str.replace(reUnescapedHtml, escapeHtmlChar) : str;
const escapeHtmlChar = chr => htmlEscapes[chr];
import {escapeForHTML} from './helpers';
export default class Template {
constructor() {
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.
* Format the contents of a todo list.
*
* 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
* @param {ItemList} items Object containing keys you want to find in the template to replace.
* @returns {!string} Contents for a todo list
*
* @example
* view.show({
* id: 1,
* title: "Hello World",
* completed: 0,
* })
* id: 1,
* title: "Hello World",
* completed: false,
* })
*/
show(data){
const view = data.map(d => {
const template = this.defaultTemplate;
const completed = d.completed ? 'completed' : '';
const checked = d.completed ? 'checked' : '';
return this.defaultTemplate
.replace('{{id}}', d.id)
.replace('{{title}}', escape(d.title))
.replace('{{completed}}', completed)
.replace('{{checked}}', checked);
});
return view.join('');
itemList(items) {
return items.reduce((a, item) => a + `
<li data-id="${item.id}"${item.completed ? ' class="completed"' : ''}>
<input class="toggle" type="checkbox" ${item.completed ? 'checked' : ''}>
<label>${escapeForHTML(item.title)}</label>
<button class="destroy"></button>
</li>`, '');
}
/**
* Displays a counter of how many to dos are left to complete
* Format the contents of an "items left" indicator.
*
* @param {number} activeTodos The number of active todos.
* @returns {string} String containing the count
*/
itemCounter(activeTodos){
const plural = activeTodos === 1 ? '' : 's';
return `<strong>${activeTodos}</strong> item${plural} left`;
}
/**
* Updates the text within the "Clear completed" button
* @param {number} activeTodos Number of active todos
*
* @param {[type]} completedTodos The number of completed todos.
* @returns {string} String containing the count
* @returns {!string} Contents for an "items left" indicator
*/
clearCompletedButton(completedTodos){
return (completedTodos > 0) ? 'Clear completed' : '';
itemCounter(activeTodos) {
return `${activeTodos} item${activeTodos !== 1 ? 's' : ''} left`;
}
}
This diff is collapsed.
This diff is collapsed.
......@@ -1123,6 +1123,15 @@
"url": "examples/vanillajs"
}]
},
"es6": {
"name": "ECMAScript 6",
"description": "The ECMAScript 6 (ES2015) standard was ratified in 2015 following years of work standardizing improvements to ECMAScript 3. The committee introduced a wide variety of improvements such as arrow functions, const declarations, and native Promises.",
"homepage": "developer.mozilla.org/en-US/docs/JavaScript",
"examples": [{
"name": "Vanilla ES6 Example",
"url": "examples/vanilla-es6"
}]
},
"js_of_ocaml": {
"name": "js_of_ocaml",
"description": "Js_of_ocaml is a compiler of OCaml bytecode to Javascript. It makes it possible to run Ocaml programs in a Web browser.",
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment