442 lines
11 KiB
JavaScript
Executable file
442 lines
11 KiB
JavaScript
Executable file
/*jshint quotmark:false */
|
|
/*jshint white:false */
|
|
/*jshint trailing:false */
|
|
/*jshint newcap:false */
|
|
/*global React, Router*/
|
|
var app = app || {};
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
app.ALL_TODOS = 'all';
|
|
app.ACTIVE_TODOS = 'active';
|
|
app.COMPLETED_TODOS = 'completed';
|
|
|
|
app.Utils = {
|
|
uuid: function () {
|
|
/*jshint bitwise:false */
|
|
var i, random;
|
|
var uuid = '';
|
|
|
|
for (i = 0; i < 32; i++) {
|
|
random = Math.random() * 16 | 0;
|
|
if (i === 8 || i === 12 || i === 16 || i === 20) {
|
|
uuid += '-';
|
|
}
|
|
uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random))
|
|
.toString(16);
|
|
}
|
|
|
|
return uuid;
|
|
},
|
|
|
|
pluralize: function (count, word) {
|
|
return count === 1 ? word : word + 's';
|
|
},
|
|
|
|
extend: function () {
|
|
var newObj = {};
|
|
for (var i = 0; i < arguments.length; i++) {
|
|
var obj = arguments[i];
|
|
for (var key in obj) {
|
|
if (obj.hasOwnProperty(key)) {
|
|
newObj[key] = obj[key];
|
|
}
|
|
}
|
|
}
|
|
return newObj;
|
|
}
|
|
};
|
|
|
|
var Utils = app.Utils;
|
|
// Generic "model" object. You can use whatever
|
|
// framework you want. For this application it
|
|
// may not even be worth separating this logic
|
|
// out, but we do this to demonstrate one way to
|
|
// separate out parts of your application.
|
|
app.TodoModel = function (key) {
|
|
this.key = key;
|
|
this.todos = [];
|
|
this.onChanges = [];
|
|
};
|
|
|
|
app.TodoModel.prototype.subscribe = function (onChange) {
|
|
this.onChanges.push(onChange);
|
|
};
|
|
|
|
app.TodoModel.prototype.inform = function () {
|
|
this.onChanges.forEach(function (cb) { cb(); });
|
|
};
|
|
|
|
app.TodoModel.prototype.addTodo = function (title) {
|
|
this.todos = this.todos.concat({
|
|
id: Utils.uuid(),
|
|
title: title,
|
|
completed: false
|
|
});
|
|
|
|
this.inform();
|
|
};
|
|
|
|
app.TodoModel.prototype.toggleAll = function (checked) {
|
|
// Note: it's usually better to use immutable data structures since they're
|
|
// easier to reason about and React works very well with them. That's why
|
|
// we use map() and filter() everywhere instead of mutating the array or
|
|
// todo items themselves.
|
|
this.todos = this.todos.map(function (todo) {
|
|
return Utils.extend({}, todo, {completed: checked});
|
|
});
|
|
|
|
this.inform();
|
|
};
|
|
|
|
app.TodoModel.prototype.toggle = function (todoToToggle) {
|
|
this.todos = this.todos.map(function (todo) {
|
|
return todo !== todoToToggle ?
|
|
todo :
|
|
Utils.extend({}, todo, {completed: !todo.completed});
|
|
});
|
|
|
|
this.inform();
|
|
};
|
|
|
|
app.TodoModel.prototype.destroy = function (todo) {
|
|
this.todos = this.todos.filter(function (candidate) {
|
|
return candidate !== todo;
|
|
});
|
|
|
|
this.inform();
|
|
};
|
|
|
|
app.TodoModel.prototype.save = function (todoToSave, text) {
|
|
this.todos = this.todos.map(function (todo) {
|
|
return todo !== todoToSave ? todo : Utils.extend({}, todo, {title: text});
|
|
});
|
|
|
|
this.inform();
|
|
};
|
|
|
|
app.TodoModel.prototype.clearCompleted = function () {
|
|
this.todos = this.todos.filter(function (todo) {
|
|
return !todo.completed;
|
|
});
|
|
|
|
this.inform();
|
|
};
|
|
|
|
|
|
var TodoFooter = React.createClass({
|
|
render: function () {
|
|
var activeTodoWord = app.Utils.pluralize(this.props.count, 'item');
|
|
var clearButton = null;
|
|
|
|
if (this.props.completedCount > 0) {
|
|
clearButton = (
|
|
<button
|
|
className="clear-completed"
|
|
onClick={this.props.onClearCompleted}>
|
|
Clear completed
|
|
</button>
|
|
);
|
|
}
|
|
|
|
var nowShowing = this.props.nowShowing;
|
|
return (
|
|
<footer className="footer">
|
|
<span className="todo-count">
|
|
<strong>{this.props.count}</strong> {activeTodoWord} left
|
|
</span>
|
|
<ul className="filters">
|
|
<li>
|
|
<a
|
|
href="#/"
|
|
className={classNames({selected: nowShowing === app.ALL_TODOS})}>
|
|
All
|
|
</a>
|
|
</li>
|
|
{' '}
|
|
<li>
|
|
<a
|
|
href="#/active"
|
|
className={classNames({selected: nowShowing === app.ACTIVE_TODOS})}>
|
|
Active
|
|
</a>
|
|
</li>
|
|
{' '}
|
|
<li>
|
|
<a
|
|
href="#/completed"
|
|
className={classNames({selected: nowShowing === app.COMPLETED_TODOS})}>
|
|
Completed
|
|
</a>
|
|
</li>
|
|
</ul>
|
|
{clearButton}
|
|
</footer>
|
|
);
|
|
}
|
|
});
|
|
|
|
var ESCAPE_KEY = 27;
|
|
var ENTER_KEY = 13;
|
|
|
|
var TodoItem = React.createClass({
|
|
handleSubmit: function (event) {
|
|
var val = this.state.editText.trim();
|
|
if (val) {
|
|
this.props.onSave(val);
|
|
this.setState({editText: val});
|
|
} else {
|
|
this.props.onDestroy();
|
|
}
|
|
},
|
|
|
|
handleEdit: function () {
|
|
this.props.onEdit();
|
|
this.setState({editText: this.props.todo.title});
|
|
},
|
|
|
|
handleKeyDown: function (event) {
|
|
if (event.which === ESCAPE_KEY) {
|
|
this.setState({editText: this.props.todo.title});
|
|
this.props.onCancel(event);
|
|
} else if (event.which === ENTER_KEY) {
|
|
this.handleSubmit(event);
|
|
}
|
|
},
|
|
|
|
handleChange: function (event) {
|
|
if (this.props.editing) {
|
|
this.setState({editText: event.target.value});
|
|
}
|
|
},
|
|
|
|
getInitialState: function () {
|
|
return {editText: this.props.todo.title};
|
|
},
|
|
|
|
/**
|
|
* This is a completely optional performance enhancement that you can
|
|
* implement on any React component. If you were to delete this method
|
|
* the app would still work correctly (and still be very performant!), we
|
|
* just use it as an example of how little code it takes to get an order
|
|
* of magnitude performance improvement.
|
|
*/
|
|
shouldComponentUpdate: function (nextProps, nextState) {
|
|
return (
|
|
nextProps.todo !== this.props.todo ||
|
|
nextProps.editing !== this.props.editing ||
|
|
nextState.editText !== this.state.editText
|
|
);
|
|
},
|
|
|
|
/**
|
|
* Safely manipulate the DOM after updating the state when invoking
|
|
* `this.props.onEdit()` in the `handleEdit` method above.
|
|
* For more info refer to notes at https://facebook.github.io/react/docs/component-api.html#setstate
|
|
* and https://facebook.github.io/react/docs/component-specs.html#updating-componentdidupdate
|
|
*/
|
|
componentDidUpdate: function (prevProps) {
|
|
if (!prevProps.editing && this.props.editing) {
|
|
var node = React.findDOMNode(this.refs.editField);
|
|
node.focus();
|
|
node.setSelectionRange(node.value.length, node.value.length);
|
|
}
|
|
},
|
|
|
|
render: function () {
|
|
return (
|
|
<li className={classNames({
|
|
completed: this.props.todo.completed,
|
|
editing: this.props.editing
|
|
})}>
|
|
<div className="view">
|
|
<input
|
|
className="toggle"
|
|
type="checkbox"
|
|
checked={this.props.todo.completed}
|
|
onChange={this.props.onToggle}
|
|
/>
|
|
<label onDoubleClick={this.handleEdit}>
|
|
{this.props.todo.title}
|
|
</label>
|
|
<button className="destroy" onClick={this.props.onDestroy} />
|
|
</div>
|
|
<input
|
|
ref="editField"
|
|
className="edit"
|
|
value={this.state.editText}
|
|
onBlur={this.handleSubmit}
|
|
onChange={this.handleChange}
|
|
onKeyDown={this.handleKeyDown}
|
|
/>
|
|
</li>
|
|
);
|
|
}
|
|
});
|
|
|
|
var ENTER_KEY = 13;
|
|
|
|
var TodoApp = React.createClass({
|
|
getInitialState: function () {
|
|
return {
|
|
nowShowing: app.ALL_TODOS,
|
|
editing: null,
|
|
newTodo: ''
|
|
};
|
|
},
|
|
|
|
componentDidMount: function () {
|
|
var setState = this.setState;
|
|
var router = Router({
|
|
'/': setState.bind(this, {nowShowing: app.ALL_TODOS}),
|
|
'/active': setState.bind(this, {nowShowing: app.ACTIVE_TODOS}),
|
|
'/completed': setState.bind(this, {nowShowing: app.COMPLETED_TODOS})
|
|
});
|
|
router.init('/');
|
|
},
|
|
|
|
handleChange: function (event) {
|
|
this.setState({newTodo: event.target.value});
|
|
},
|
|
|
|
handleNewTodoKeyDown: function (event) {
|
|
if (event.keyCode !== ENTER_KEY) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
var val = this.state.newTodo.trim();
|
|
|
|
if (val) {
|
|
this.props.model.addTodo(val);
|
|
this.setState({newTodo: ''});
|
|
}
|
|
},
|
|
|
|
toggleAll: function (event) {
|
|
var checked = event.target.checked;
|
|
this.props.model.toggleAll(checked);
|
|
},
|
|
|
|
toggle: function (todoToToggle) {
|
|
this.props.model.toggle(todoToToggle);
|
|
},
|
|
|
|
destroy: function (todo) {
|
|
this.props.model.destroy(todo);
|
|
},
|
|
|
|
edit: function (todo) {
|
|
this.setState({editing: todo.id});
|
|
},
|
|
|
|
save: function (todoToSave, text) {
|
|
this.props.model.save(todoToSave, text);
|
|
this.setState({editing: null});
|
|
},
|
|
|
|
cancel: function () {
|
|
this.setState({editing: null});
|
|
},
|
|
|
|
clearCompleted: function () {
|
|
this.props.model.clearCompleted();
|
|
},
|
|
|
|
render: function () {
|
|
var footer;
|
|
var main;
|
|
var todos = this.props.model.todos;
|
|
|
|
var shownTodos = todos.filter(function (todo) {
|
|
switch (this.state.nowShowing) {
|
|
case app.ACTIVE_TODOS:
|
|
return !todo.completed;
|
|
case app.COMPLETED_TODOS:
|
|
return todo.completed;
|
|
default:
|
|
return true;
|
|
}
|
|
}, this);
|
|
|
|
var todoItems = shownTodos.map(function (todo) {
|
|
return (
|
|
<TodoItem
|
|
key={todo.id}
|
|
todo={todo}
|
|
onToggle={this.toggle.bind(this, todo)}
|
|
onDestroy={this.destroy.bind(this, todo)}
|
|
onEdit={this.edit.bind(this, todo)}
|
|
editing={this.state.editing === todo.id}
|
|
onSave={this.save.bind(this, todo)}
|
|
onCancel={this.cancel}
|
|
/>
|
|
);
|
|
}, this);
|
|
|
|
var activeTodoCount = todos.reduce(function (accum, todo) {
|
|
return todo.completed ? accum : accum + 1;
|
|
}, 0);
|
|
|
|
var completedCount = todos.length - activeTodoCount;
|
|
|
|
if (activeTodoCount || completedCount) {
|
|
footer =
|
|
<TodoFooter
|
|
count={activeTodoCount}
|
|
completedCount={completedCount}
|
|
nowShowing={this.state.nowShowing}
|
|
onClearCompleted={this.clearCompleted}
|
|
/>;
|
|
}
|
|
|
|
if (todos.length) {
|
|
main = (
|
|
<section className="main">
|
|
<input
|
|
className="toggle-all"
|
|
type="checkbox"
|
|
onChange={this.toggleAll}
|
|
checked={activeTodoCount === 0}
|
|
/>
|
|
<ul className="todo-list">
|
|
{todoItems}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div>
|
|
<header className="header">
|
|
<h1>todos</h1>
|
|
<input
|
|
className="new-todo"
|
|
placeholder="What needs to be done?"
|
|
value={this.state.newTodo}
|
|
onKeyDown={this.handleNewTodoKeyDown}
|
|
onChange={this.handleChange}
|
|
autoFocus={true}
|
|
/>
|
|
</header>
|
|
{main}
|
|
{footer}
|
|
</div>
|
|
);
|
|
}
|
|
});
|
|
|
|
var model = new app.TodoModel('react-todos');
|
|
|
|
function render() {
|
|
ReactDOM.render(
|
|
<TodoApp model={model}/>,
|
|
document.getElementsByClassName('todoapp')[0]
|
|
);
|
|
}
|
|
|
|
model.subscribe(render);
|
|
render();
|
|
})();
|