Commit 3a6db4da authored by Addy Osmani's avatar Addy Osmani

Merge pull request #88 from rniemeyer/knockout-template-update

Update Knockout.js sample to conform to template for Issue #68
parents 2e76a44a 73ebf519
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, font, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-weight: inherit;
font-style: inherit;
font-size: 100%;
font-family: inherit;
vertical-align: baseline;
}
body {
line-height: 1;
color: black;
background: white;
}
ol, ul {
list-style: none;
}
a img {
border: none;
}
html {
background: #eeeeee;
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 14px;
line-height: 1.4em;
background: #eeeeee;
color: #333333;
}
#todoapp {
width: 480px;
margin: 0 auto 40px;
background: white;
padding: 20px;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
box-shadow: rgba(0, 0, 0, 0.2) 0 5px 6px 0;
-moz-border-radius: 0 0 5px 5px;
-o-border-radius: 0 0 5px 5px;
-webkit-border-radius: 0 0 5px 5px;
border-radius: 0 0 5px 5px;
}
#todoapp h1 {
font-size: 36px;
font-weight: bold;
text-align: center;
padding: 20px 0 30px 0;
line-height: 1;
}
#create-todo {
position: relative;
}
#create-todo input {
width: 466px;
font-size: 24px;
font-family: inherit;
line-height: 1.4em;
border: 0;
outline: none;
padding: 6px;
border: 1px solid #999999;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}
#create-todo input::-webkit-input-placeholder {
font-style: italic;
}
#create-todo span {
position: absolute;
z-index: 999;
width: 170px;
left: 50%;
margin-left: -85px;
}
#todo-list {
margin-top: 10px;
}
#todo-list li {
padding: 12px 20px 11px 0;
position: relative;
font-size: 24px;
line-height: 1.1em;
border-bottom: 1px solid #cccccc;
}
#todo-list li:after {
content: "\0020";
display: block;
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
#todo-list li.editing {
padding: 0;
border-bottom: 0;
}
#todo-list .editing .display,
#todo-list .edit {
display: none;
}
#todo-list .editing .edit {
display: block;
}
#todo-list .editing input {
width: 444px;
font-size: 24px;
font-family: inherit;
margin: 0;
line-height: 1.6em;
border: 0;
outline: none;
padding: 10px 7px 0px 27px;
border: 1px solid #999999;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}
#todo-list .check {
position: relative;
top: 9px;
margin: 0 10px 0 7px;
float: left;
}
#todo-list .done .todo-content {
text-decoration: line-through;
color: #777777;
}
#todo-list .todo-destroy {
position: absolute;
right: 5px;
top: 14px;
display: none;
cursor: pointer;
width: 20px;
height: 20px;
background: url(destroy.png) no-repeat 0 0;
}
#todo-list li:hover .todo-destroy {
display: block;
}
#todo-list .todo-destroy:hover {
background-position: 0 -20px;
}
#todo-stats {
*zoom: 1;
margin-top: 10px;
color: #777777;
}
#todo-stats:after {
content: "\0020";
display: block;
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
#todo-stats .todo-count {
float: left;
}
#todo-stats .todo-count .number {
font-weight: bold;
color: #333333;
}
#todo-stats .todo-clear {
float: right;
}
#todo-stats .todo-clear a {
color: #777777;
font-size: 12px;
}
/* these two rules are overridden below. find ".todo-clear a" */
#todo-stats .todo-clear a:visited {
color: #777777;
}
#todo-stats .todo-clear a:hover, #todo-stats .todo-clear a:focus {
color: #336699;
}
#instructions {
width: 520px;
margin: 10px auto;
color: #777777;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#instructions a {
color: #336699;
}
#credits {
width: 520px;
margin: 30px auto;
color: #999;
text-shadow: rgba(255, 255, 255, 0.8) 0 1px 0;
text-align: center;
}
#credits a {
color: #888;
}
/*
* François 'cahnory' Germain
*/
.ui-tooltip, .ui-tooltip-top, .ui-tooltip-right, .ui-tooltip-bottom, .ui-tooltip-left {
color:#ffffff;
cursor:normal;
display:-moz-inline-stack;
display:inline-block;
font-size:12px;
font-family:arial;
padding:.5em 1em;
position:relative;
text-align:center;
text-shadow:0 -1px 1px #111111;
-webkit-border-top-left-radius:4px ;
-webkit-border-top-right-radius:4px ;
-webkit-border-bottom-right-radius:4px ;
-webkit-border-bottom-left-radius:4px ;
-khtml-border-top-left-radius:4px ;
-khtml-border-top-right-radius:4px ;
-khtml-border-bottom-right-radius:4px ;
-khtml-border-bottom-left-radius:4px ;
-moz-border-radius-topleft:4px ;
-moz-border-radius-topright:4px ;
-moz-border-radius-bottomright:4px ;
-moz-border-radius-bottomleft:4px ;
border-top-left-radius:4px ;
border-top-right-radius:4px ;
border-bottom-right-radius:4px ;
border-bottom-left-radius:4px ;
-o-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
-moz-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
-khtml-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
-webkit-box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
box-shadow:0 1px 2px #000000, inset 0 0 0 1px #222222, inset 0 2px #666666, inset 0 -2px 2px #444444;
background-color:#3b3b3b;
background-image:-moz-linear-gradient(top,#555555,#222222);
background-image:-webkit-gradient(linear,left top,left bottom,color-stop(0,#555555),color-stop(1,#222222));
filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222);
-ms-filter:progid:DXImageTransform.Microsoft.gradient(startColorStr=#555555,EndColorStr=#222222);
}
.ui-tooltip:after, .ui-tooltip-top:after, .ui-tooltip-right:after, .ui-tooltip-bottom:after, .ui-tooltip-left:after {
content:"\25B8";
display:block;
font-size:2em;
height:0;
line-height:0;
position:absolute;
}
.ui-tooltip:after, .ui-tooltip-bottom:after {
color:#2a2a2a;
bottom:0;
left:1px;
text-align:center;
text-shadow:1px 0 2px #000000;
-o-transform:rotate(90deg);
-moz-transform:rotate(90deg);
-khtml-transform:rotate(90deg);
-webkit-transform:rotate(90deg);
width:100%;
}
.ui-tooltip-top:after {
bottom:auto;
color:#4f4f4f;
left:-2px;
top:0;
text-align:center;
text-shadow:none;
-o-transform:rotate(-90deg);
-moz-transform:rotate(-90deg);
-khtml-transform:rotate(-90deg);
-webkit-transform:rotate(-90deg);
width:100%;
}
.ui-tooltip-right:after {
color:#222222;
right:-0.375em;
top:50%;
margin-top:-.05em;
text-shadow:0 1px 2px #000000;
-o-transform:rotate(0);
-moz-transform:rotate(0);
-khtml-transform:rotate(0);
-webkit-transform:rotate(0);
}
.ui-tooltip-left:after {
color:#222222;
left:-0.375em;
top:50%;
margin-top:.1em;
text-shadow:0 -1px 2px #000000;
-o-transform:rotate(180deg);
-moz-transform:rotate(180deg);
-khtml-transform:rotate(180deg);
-webkit-transform:rotate(180deg);
}
/* new additions - cleanup required*/
/* line 109 */
#todoapp #todo-stats {
*zoom: 1;
margin-top: 10px;
color: #555555;
-moz-border-radius-bottomleft: 5px;
-webkit-border-bottom-left-radius: 5px;
-o-border-bottom-left-radius: 5px;
-ms-border-bottom-left-radius: 5px;
-khtml-border-bottom-left-radius: 5px;
border-bottom-left-radius: 5px;
-moz-border-radius-bottomright: 5px;
-webkit-border-bottom-right-radius: 5px;
-o-border-bottom-right-radius: 5px;
-ms-border-bottom-right-radius: 5px;
-khtml-border-bottom-right-radius: 5px;
border-bottom-right-radius: 5px;
background: #f4fce8;
border-top: 1px solid #ededed;
padding: 0 20px;
line-height: 36px;
}
/* line 22, /opt/ree/lib/ruby/gems/1.8/gems/compass-0.10.5/frameworks/compass/stylesheets/compass/utilities/general/_clearfix.scss */
#todoapp #todo-stats:after {
content: "\0020";
display: block;
height: 0;
clear: both;
overflow: hidden;
visibility: hidden;
}
/* line 118 */
#todoapp #todo-stats .todo-count {
float: left;
}
/* line 120 */
#todoapp #todo-stats .todo-count .number {
font-weight: bold;
color: #555555;
}
/* line 123 */
#todoapp #todo-stats .todo-clear {
float: right;
}
/* line 125 */
#todoapp #todo-stats .todo-clear a {
display: block;
line-height: 20px;
text-decoration: none;
-moz-border-radius: 12px;
-webkit-border-radius: 12px;
-o-border-radius: 12px;
-ms-border-radius: 12px;
-khtml-border-radius: 12px;
border-radius: 12px;
background: rgba(0, 0, 0, 0.1);
color: #555555;
font-size: 11px;
margin-top: 8px;
padding: 0 10px 1px;
-moz-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
-o-box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
box-shadow: rgba(0, 0, 0, 0.2) 0 -1px 0 0;
}
/* line 136 */
#todoapp #todo-stats .todo-clear a:hover, #todoapp #todo-stats .todo-clear a:focus {
background: rgba(0, 0, 0, 0.15);
-moz-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-webkit-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
-o-box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
box-shadow: rgba(0, 0, 0, 0.3) 0 -1px 0 0;
}
/* line 139 */
#todoapp #todo-stats .todo-clear a:active {
position: relative;
top: 1px;
}
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Knockout.js</title>
<link href="css/todos.css" media="all" rel="stylesheet" type="text/css" />
<link rel="stylesheet" href="../../assets/base.css">
</head>
<body>
<div id="todoapp">
<div class="title">
<h1>Todos</h1>
</div>
<div class="content">
<div id="create-todo">
<input id="new-todo" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add" placeholder="What needs to be done?" />
<span class="ui-tooltip-top" data-bind="visible: showTooltip" style="display: none;">Press Enter to save this task</span>
</div>
<div id="todos">
<div data-bind="visible: todos().length">
<input id="check-all" class="check" type="checkbox" data-bind="checked: allCompleted" />
<label for="check-all">Mark all as complete</label>
<div id="todoapp">
<header>
<h1>Todos</h1>
<input id="new-todo" type="text" data-bind="value: current, valueUpdate: 'afterkeydown', enterKey: add"
placeholder="What needs to be done?"/>
</header>
<section id="main" data-bind="block: todos().length">
<input id="toggle-all" type="checkbox" data-bind="checked: allCompleted">
<label for="toggle-all">Mark all as complete</label>
<ul id="todo-list" data-bind="foreach: todos">
<li data-bind="css: { done: done, editing: editing }">
<div class="view" data-bind="event: { dblclick: $root.editItem }">
<input class="toggle" type="checkbox" data-bind="checked: done">
<label data-bind="text: content"></label>
<a class="destroy" href="#" data-bind="click: $root.remove"></a>
</div>
<ul id="todo-list" data-bind="foreach: todos">
<li data-bind="css: { editing: editing }">
<div class="todo" data-bind="css: { done : done }">
<div class="display">
<input class="check" type="checkbox" data-bind="checked: done" />
<div class="todo-content" data-bind="text: content, event: { dblclick: edit }" style="cursor: pointer;"></div>
<span class="todo-destroy" data-bind="click: $root.remove"></span>
</div>
<div class="edit">
<input class="todo-input" data-bind="value: content, valueUpdate: 'afterkeydown', enterKey: stopEditing, event: { blur: stopEditing }"/>
</div>
</div>
</li>
</ul>
</div>
<div id="todo-stats">
<span class="todo-count" data-bind="visible: remainingCount">
<span class="number" data-bind="text: remainingCount"></span>
<span class="word" data-bind="text: getLabel(remainingCount)"></span> left.
</span>
<span class="todo-clear" data-bind="visible: completedCount">
<a href="#" data-bind="click: removeCompleted">
Clear <span class="number-done" data-bind="text: completedCount"></span>
completed <span class="word-done" data-bind="text: getLabel(completedCount)"></span>
</a>
</span>
</div>
</div>
</div>
<ul id="instructions">
<li data-bind="visible: todos().length">Double-click to edit a todo.</li>
</ul>
<div id="credits">
Created by
<br />
<a href="http://jgn.me/">J&eacute;r&ocirc;me Gravel-Niquet</a>
<br />
Modified to use knockout.js by
<br />
<a href="https://github.com/ashish01/knockoutjs-todos">Ashish Sharma</a>
<br/>
Updated to use knockout.js 2.0 by
<br/>
<a href="http://knockmeout.net">Ryan Niemeyer</a>
<br />
Patches/fixes for cross-browser compat:
<br />
<a href="http://twitter.com/addyosmani">Addy Osmani</a>
</div>
<input class="edit" type="text"
data-bind="value: content, valueUpdate: 'afterkeydown', enterKey: $root.stopEditing, selectAndFocus: editing, event: { blur: $root.stopEditing }"/>
</li>
</ul>
</section>
<footer data-bind="block: completedCount() || remainingCount()">
<a id="clear-completed" href="#" data-bind="inline: completedCount, click: removeCompleted">
Clear <span data-bind="text: completedCount"></span>
completed <span data-bind="text: getLabel(completedCount)"></span>
</a>
<!-- Knockout has no direct dependencies -->
<script src="js/knockout-2.0.0.js" type="text/javascript"></script>
<!-- needed to support JSON.stringify in older browsers (for local storage) -->
<script src="js/json2.js" type="text/javascript"></script>
<!-- used for local storage -->
<script src="js/amplify.store.min.js" type="text/javascript"></script>
<!-- our app code -->
<script src="js/todos.js" type="text/javascript"></script>
<div id="todo-count">
<span data-bind="text: remainingCount"></span>
<span data-bind="text: getLabel(remainingCount)" style="font-weight: normal"></span> left.
</div>
</footer>
</div>
<div id="instructions" data-bind="visible: todos().length">
Double-click to edit a todo.
</div>
<div id="credits">
Created by
<br/>
<a href="http://jgn.me/">J&eacute;r&ocirc;me Gravel-Niquet</a>
<br/>
Modified to use knockout.js by
<br/>
<a href="https://github.com/ashish01/knockoutjs-todos">Ashish Sharma</a>
<br/>
Updated to use knockout.js 2.0 by
<br/>
<a href="http://knockmeout.net">Ryan Niemeyer</a>
<br/>
Patches/fixes for cross-browser compat:
<br/>
<a href="http://twitter.com/addyosmani">Addy Osmani</a>
</div>
<!-- Knockout has no direct dependencies -->
<script src="js/libs/knockout-2.0.0.js" type="text/javascript"></script>
<!-- needed to support JSON.stringify in older browsers (for local storage) -->
<script src="js/libs/json2.js" type="text/javascript"></script>
<!-- used for local storage -->
<script src="js/libs/amplify.store.min.js" type="text/javascript"></script>
<!-- our app code -->
<script src="js/app.js" type="text/javascript"></script>
</body>
</html>
\ No newline at end of file
(function() {
(function () {
//trim polyfill
if (!String.prototype.trim) {
String.prototype.trim = function () {
return this.replace(/^\s+|\s+$/g, '');
};
}
//a custom binding to handle the enter key (could go in a separate library)
ko.bindingHandlers.enterKey = {
init: function(element, valueAccessor, allBindingsAccessor, data) {
init:function (element, valueAccessor, allBindingsAccessor, data) {
var wrappedHandler, newValueAccessor;
//wrap the handler with a check for the enter key
wrappedHandler = function(data, event) {
wrappedHandler = function (data, event) {
if (event.keyCode === 13) {
valueAccessor().call(this, data, event);
}
};
//create a valueAccessor with the options that we would want to pass to the event binding
newValueAccessor = function() {
return { keyup: wrappedHandler };
newValueAccessor = function () {
return { keyup:wrappedHandler };
};
//call the real event binding's init function
......@@ -21,6 +28,38 @@
}
};
//wrapper to hasfocus that also selects text and applies focus async
ko.bindingHandlers.selectAndFocus = {
init:function (element, valueAccessor, allBindingsAccessor) {
ko.bindingHandlers.hasfocus.init(element, valueAccessor, allBindingsAccessor);
ko.utils.registerEventHandler(element, "focus", function () {
element.select();
});
},
update:function (element, valueAccessor) {
ko.utils.unwrapObservable(valueAccessor()); //for dependency
//ensure that element is visible before trying to focus
setTimeout(function () {
ko.bindingHandlers.hasfocus.update(element, valueAccessor);
}, 0);
}
};
//alternative to "visible" binding that will specifically set "block" to override what is in css
ko.bindingHandlers.block = {
update:function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
element.style.display = value ? "block" : "none";
}
};
//alternative to "visible" binding that will specifically set "inline" to override what is in css
ko.bindingHandlers.inline = {
update:function (element, valueAccessor) {
var value = ko.utils.unwrapObservable(valueAccessor());
element.style.display = value ? "inline" : "none";
}
};
//represent a single todo item
var Todo = function (content, done) {
......@@ -29,18 +68,11 @@
this.editing = ko.observable(false);
};
//can place methods on prototype, as there can be many todos
ko.utils.extend(Todo.prototype, {
edit: function() { this.editing(true); },
stopEditing: function() { this.editing(false); }
});
//our main view model
var ViewModel = function(todos) {
var ViewModel = function (todos) {
var self = this;
//map array of passed in todos to an observableArray of Todo objects
self.todos = ko.observableArray(ko.utils.arrayMap(todos, function(todo) {
self.todos = ko.observableArray(ko.utils.arrayMap(todos, function (todo) {
return new Todo(todo.content, todo.done);
}));
......@@ -49,9 +81,12 @@
//add a new todo, when enter key is pressed
self.add = function (data, event) {
var newTodo = new Todo(self.current());
self.todos.push(newTodo);
self.current("");
var newTodo, current = self.current().trim();
if (current) {
newTodo = new Todo(current);
self.todos.push(newTodo);
self.current("");
}
};
//remove a single todo
......@@ -61,16 +96,30 @@
//remove all completed todos
self.removeCompleted = function () {
self.todos.remove(function(todo) {
self.todos.remove(function (todo) {
return todo.done();
});
};
//edit an item
self.editItem = function(item) {
item.editing(true);
};
//stop editing an item. Remove the item, if it is now empty
self.stopEditing = function(item) {
item.editing(false);
if (!item.content().trim()) {
self.remove(item);
}
};
//count of all completed todos
self.completedCount = ko.computed(function () {
return ko.utils.arrayFilter(self.todos(), function(todo) {
return todo.done();
}).length;
return ko.utils.arrayFilter(self.todos(),
function (todo) {
return todo.done();
}).length;
});
//count of todos that are not complete
......@@ -81,51 +130,32 @@
//writeable computed observable to handle marking all complete/incomplete
self.allCompleted = ko.computed({
//always return true/false based on the done flag of all todos
read: function() {
read:function () {
return !self.remainingCount();
},
//set all todos to the written value (true/false)
write: function(newValue) {
ko.utils.arrayForEach(self.todos(), function(todo) {
write:function (newValue) {
ko.utils.arrayForEach(self.todos(), function (todo) {
//set even if value is the same, as subscribers are not notified in that case
todo.done(newValue);
});
}
});
//track whether the tooltip should be shown
self.showTooltip = ko.observable(false);
self.showTooltip.setTrue = function() { self.showTooltip(true); }; //avoid an anonymous function each time
//watch the current value
self.current.subscribe(function(newValue) {
//if the value was just updated, then the tooltip should not be shown
self.showTooltip(false);
//clear the current timer, as it is actively being updated
if (self.showTooltip.timer) {
clearTimeout(self.showTooltip.timer);
}
//if there is a value and then show the tooltip after 1 second
if (newValue) {
self.showTooltip.timer = setTimeout(self.showTooltip.setTrue, 1000);
}
});
//helper function to keep expressions out of markup
self.getLabel = function(count) {
self.getLabel = function (count) {
return ko.utils.unwrapObservable(count) === 1 ? "item" : "items";
};
//internal computed observable that fires whenever anything changes in our todos
ko.computed(function() {
//get a clean copy of the todos, which also creates a dependency on the observableArray and all observables in each item
var todos = ko.toJS(self.todos);
//store to local storage
amplify.store("todos-knockout", todos);
}).extend({ throttle: 1000 }); //save at most once per second
ko.computed(
function () {
//get a clean copy of the todos, which also creates a dependency on the observableArray and all observables in each item
var todos = ko.toJS(self.todos);
//store to local storage
amplify.store("todos-knockout", todos);
}).extend({ throttle:500 }); //save at most once per second
};
//check local storage for todos
......
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