Commit 1c04c6b5 authored by Sindre Sorhus's avatar Sindre Sorhus

tabs

parent 21c9a3f9
...@@ -10,23 +10,23 @@ var TodoAppView, TodoList, TodoModel, TodoView, localStorageName = 'todos-yuilib ...@@ -10,23 +10,23 @@ var TodoAppView, TodoList, TodoModel, TodoView, localStorageName = 'todos-yuilib
// attributes and methods useful for todo items. // attributes and methods useful for todo items.
TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], { TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], {
// This tells the Model to use a localStorage sync provider (which we'll // This tells the Model to use a localStorage sync provider (which we'll
// create below) to save and load information about a todo item. // create below) to save and load information about a todo item.
sync: LocalStorageSync(localStorageName), sync: LocalStorageSync(localStorageName),
// This method will toggle the `done` attribute from `true` to `false`, or // This method will toggle the `done` attribute from `true` to `false`, or
// vice versa. // vice versa.
toggleDone: function () { toggleDone: function () {
this.set('done', !this.get('done')).save(); this.set('done', !this.get('done')).save();
} }
}, { }, {
ATTRS: { ATTRS: {
// Indicates whether or not this todo item has been completed. // Indicates whether or not this todo item has been completed.
done: {value: false}, done: {value: false},
// Contains the text of the todo item. // Contains the text of the todo item.
text: {value: ''} text: {value: ''}
} }
}); });
// -- ModelList ---------------------------------------------------------------- // -- ModelList ----------------------------------------------------------------
...@@ -36,28 +36,28 @@ TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], { ...@@ -36,28 +36,28 @@ TodoModel = Y.TodoModel = Y.Base.create('todoModel', Y.Model, [], {
// information about the todo items in the list. // information about the todo items in the list.
TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], { TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], {
// This tells the list that it will hold instances of the TodoModel class. // This tells the list that it will hold instances of the TodoModel class.
model: TodoModel, model: TodoModel,
// This tells the list to use a localStorage sync provider (which we'll // This tells the list to use a localStorage sync provider (which we'll
// create below) to load the list of todo items. // create below) to load the list of todo items.
sync : LocalStorageSync(localStorageName), sync : LocalStorageSync(localStorageName),
// Returns an array of all models in this list with the `done` attribute // Returns an array of all models in this list with the `done` attribute
// set to `true`. // set to `true`.
done: function () { done: function () {
return Y.Array.filter(this.toArray(), function (model) { return Y.Array.filter(this.toArray(), function (model) {
return model.get('done'); return model.get('done');
}); });
}, },
// Returns an array of all models in this list with the `done` attribute // Returns an array of all models in this list with the `done` attribute
// set to `false`. // set to `false`.
remaining: function () { remaining: function () {
return Y.Array.filter(this.toArray(), function (model) { return Y.Array.filter(this.toArray(), function (model) {
return !model.get('done'); return !model.get('done');
}); });
} }
}); });
// -- Todo App View ------------------------------------------------------------ // -- Todo App View ------------------------------------------------------------
...@@ -71,165 +71,165 @@ TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], { ...@@ -71,165 +71,165 @@ TodoList = Y.TodoList = Y.Base.create('todoList', Y.ModelList, [], {
// initially loaded or reset. // initially loaded or reset.
TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], { TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], {
// The container node is the wrapper for this view. All the view's events // The container node is the wrapper for this view. All the view's events
// will be delegated from the container. In this case, the #todo-app // will be delegated from the container. In this case, the #todo-app
// node already exists on the page, so we don't need to create it. // node already exists on the page, so we don't need to create it.
container: Y.one('#todo-app'), container: Y.one('#todo-app'),
// This is a custom property that we'll use to hold a reference to the // This is a custom property that we'll use to hold a reference to the
// "new todo" input field. // "new todo" input field.
inputNode: Y.one('#new-todo'), inputNode: Y.one('#new-todo'),
// The `template` property is a convenience property for holding a template // The `template` property is a convenience property for holding a template
// for this view. In this case, we'll use it to store the contents of the // for this view. In this case, we'll use it to store the contents of the
// #todo-stats-template element, which will serve as the template for the // #todo-stats-template element, which will serve as the template for the
// statistics displayed at the bottom of the list. // statistics displayed at the bottom of the list.
template: Y.one('#todo-stats-template').getContent(), template: Y.one('#todo-stats-template').getContent(),
// This is where we attach DOM events for the view. The `events` object is a // This is where we attach DOM events for the view. The `events` object is a
// mapping of selectors to an object containing one or more events to attach // mapping of selectors to an object containing one or more events to attach
// to the node(s) matching each selector. // to the node(s) matching each selector.
events: { events: {
// Handle <enter> keypresses on the "new todo" input field. // Handle <enter> keypresses on the "new todo" input field.
'#new-todo': {keypress: 'createTodo'}, '#new-todo': {keypress: 'createTodo'},
// Clear all completed items from the list when the "Clear" link is // Clear all completed items from the list when the "Clear" link is
// clicked. // clicked.
'.todo-clear': {click: 'clearDone'}, '.todo-clear': {click: 'clearDone'},
// Add and remove hover states on todo items. // Add and remove hover states on todo items.
'.todo-item': { '.todo-item': {
mouseover: 'hoverOn', mouseover: 'hoverOn',
mouseout : 'hoverOff' mouseout : 'hoverOff'
} }
}, },
// The initializer runs when a TodoAppView instance is created, and gives // The initializer runs when a TodoAppView instance is created, and gives
// us an opportunity to set up the view. // us an opportunity to set up the view.
initializer: function () { initializer: function () {
// Create a new TodoList instance to hold the todo items. // Create a new TodoList instance to hold the todo items.
var list = this.todoList = new TodoList(); var list = this.todoList = new TodoList();
// Update the display when a new item is added to the list, or when the // Update the display when a new item is added to the list, or when the
// entire list is reset. // entire list is reset.
list.after('add', this.add, this); list.after('add', this.add, this);
list.after('reset', this.reset, this); list.after('reset', this.reset, this);
// Re-render the stats in the footer whenever an item is added, removed // Re-render the stats in the footer whenever an item is added, removed
// or changed, or when the entire list is reset. // or changed, or when the entire list is reset.
list.after(['add', 'reset', 'remove', 'todoModel:doneChange'], list.after(['add', 'reset', 'remove', 'todoModel:doneChange'],
this.render, this); this.render, this);
// Load saved items from localStorage, if available. // Load saved items from localStorage, if available.
list.load(); list.load();
}, },
// The render function is called whenever a todo item is added, removed, or // The render function is called whenever a todo item is added, removed, or
// changed, thanks to the list event handler we attached in the initializer // changed, thanks to the list event handler we attached in the initializer
// above. // above.
render: function () { render: function () {
var todoList = this.todoList, var todoList = this.todoList,
stats = this.container.one('#todo-stats'), stats = this.container.one('#todo-stats'),
numRemaining, numDone; numRemaining, numDone;
// If there are no todo items, then clear the stats. // If there are no todo items, then clear the stats.
if (todoList.isEmpty()) { if (todoList.isEmpty()) {
stats.empty(); stats.empty();
return this; return this;
} }
// Figure out how many todo items are completed and how many remain. // Figure out how many todo items are completed and how many remain.
numDone = todoList.done().length; numDone = todoList.done().length;
numRemaining = todoList.remaining().length; numRemaining = todoList.remaining().length;
// Update the statistics. // Update the statistics.
stats.setContent(Y.Lang.sub(this.template, { stats.setContent(Y.Lang.sub(this.template, {
numDone : numDone, numDone : numDone,
numRemaining : numRemaining, numRemaining : numRemaining,
doneLabel : numDone === 1 ? 'task' : 'tasks', doneLabel : numDone === 1 ? 'task' : 'tasks',
remainingLabel: numRemaining === 1 ? 'task' : 'tasks' remainingLabel: numRemaining === 1 ? 'task' : 'tasks'
})); }));
// If there are no completed todo items, don't show the "Clear // If there are no completed todo items, don't show the "Clear
// completed items" link. // completed items" link.
if (!numDone) { if (!numDone) {
stats.one('.todo-clear').remove(); stats.one('.todo-clear').remove();
} }
return this; return this;
}, },
// -- Event Handlers ------------------------------------------------------- // -- Event Handlers -------------------------------------------------------
// Creates a new TodoView instance and renders it into the list whenever a // Creates a new TodoView instance and renders it into the list whenever a
// todo item is added to the list. // todo item is added to the list.
add: function (e) { add: function (e) {
var view = new TodoView({model: e.model}); var view = new TodoView({model: e.model});
this.container.one('#todo-list').append(view.render().container); this.container.one('#todo-list').append(view.render().container);
}, },
// Removes all finished todo items from the list. // Removes all finished todo items from the list.
clearDone: function (e) { clearDone: function (e) {
var done = this.todoList.done(); var done = this.todoList.done();
e.preventDefault(); e.preventDefault();
// Remove all finished items from the list, but do it silently so as not // Remove all finished items from the list, but do it silently so as not
// to re-render the app view after each item is removed. // to re-render the app view after each item is removed.
this.todoList.remove(done, {silent: true}); this.todoList.remove(done, {silent: true});
// Destroy each removed TodoModel instance. // Destroy each removed TodoModel instance.
Y.Array.each(done, function (todo) { Y.Array.each(done, function (todo) {
// Passing {'delete': true} to the todo model's `destroy()` method // Passing {'delete': true} to the todo model's `destroy()` method
// tells it to delete itself from localStorage as well. // tells it to delete itself from localStorage as well.
todo.destroy({'delete': true}); todo.destroy({'delete': true});
}); });
// Finally, re-render the app view. // Finally, re-render the app view.
this.render(); this.render();
}, },
// Creates a new todo item when the enter key is pressed in the new todo // Creates a new todo item when the enter key is pressed in the new todo
// input field. // input field.
createTodo: function (e) { createTodo: function (e) {
var value; var value;
if (e.keyCode === 13) { // enter key if (e.keyCode === 13) { // enter key
value = Y.Lang.trim(this.inputNode.get('value')); value = Y.Lang.trim(this.inputNode.get('value'));
if (!value) { return; } if (!value) { return; }
// This tells the list to create a new TodoModel instance with the // This tells the list to create a new TodoModel instance with the
// specified text and automatically save it to localStorage in a // specified text and automatically save it to localStorage in a
// single step. // single step.
this.todoList.create({text: value}); this.todoList.create({text: value});
this.inputNode.set('value', ''); this.inputNode.set('value', '');
} }
}, },
// Turns off the hover state on a todo item. // Turns off the hover state on a todo item.
hoverOff: function (e) { hoverOff: function (e) {
e.currentTarget.removeClass('todo-hover'); e.currentTarget.removeClass('todo-hover');
}, },
// Turns on the hover state on a todo item. // Turns on the hover state on a todo item.
hoverOn: function (e) { hoverOn: function (e) {
e.currentTarget.addClass('todo-hover'); e.currentTarget.addClass('todo-hover');
}, },
// Creates and renders views for every todo item in the list when the entire // Creates and renders views for every todo item in the list when the entire
// list is reset. // list is reset.
reset: function (e) { reset: function (e) {
var fragment = Y.one(Y.config.doc.createDocumentFragment()); var fragment = Y.one(Y.config.doc.createDocumentFragment());
Y.Array.each(e.models, function (model) { Y.Array.each(e.models, function (model) {
var view = new TodoView({model: model}); var view = new TodoView({model: model});
fragment.append(view.render().container); fragment.append(view.render().container);
}); });
this.container.one('#todo-list').setContent(fragment); this.container.one('#todo-list').setContent(fragment);
} }
}); });
// -- Todo item view ----------------------------------------------------------- // -- Todo item view -----------------------------------------------------------
...@@ -239,101 +239,101 @@ TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], { ...@@ -239,101 +239,101 @@ TodoAppView = Y.TodoAppView = Y.Base.create('todoAppView', Y.View, [], {
// allow it to be edited and removed from the list. // allow it to be edited and removed from the list.
TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], { TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], {
// Specifying an HTML string as this view's container element causes that // Specifying an HTML string as this view's container element causes that
// HTML to be automatically converted into an unattached Y.Node instance. // HTML to be automatically converted into an unattached Y.Node instance.
// The TodoAppView (above) will take care of appending it to the list. // The TodoAppView (above) will take care of appending it to the list.
container: '<li class="todo-item"/>', container: '<li class="todo-item"/>',
// The template property holds the contents of the #todo-item-template // The template property holds the contents of the #todo-item-template
// element, which will be used as the HTML template for each todo item. // element, which will be used as the HTML template for each todo item.
template: Y.one('#todo-item-template').getContent(), template: Y.one('#todo-item-template').getContent(),
// Delegated DOM events to handle this view's interactions. // Delegated DOM events to handle this view's interactions.
events: { events: {
// Toggle the "done" state of this todo item when the checkbox is // Toggle the "done" state of this todo item when the checkbox is
// clicked. // clicked.
'.todo-checkbox': {click: 'toggleDone'}, '.todo-checkbox': {click: 'toggleDone'},
// When the text of this todo item is clicked or focused, switch to edit // When the text of this todo item is clicked or focused, switch to edit
// mode to allow editing. // mode to allow editing.
'.todo-content': { '.todo-content': {
click: 'edit', click: 'edit',
focus: 'edit' focus: 'edit'
}, },
// On the edit field, when enter is pressed or the field loses focus, // On the edit field, when enter is pressed or the field loses focus,
// save the current value and switch out of edit mode. // save the current value and switch out of edit mode.
'.todo-input' : { '.todo-input' : {
blur : 'save', blur : 'save',
keypress: 'enter' keypress: 'enter'
}, },
// When the remove icon is clicked, delete this todo item. // When the remove icon is clicked, delete this todo item.
'.todo-remove': {click: 'remove'} '.todo-remove': {click: 'remove'}
}, },
initializer: function () { initializer: function () {
// The model property is set to a TodoModel instance by TodoAppView when // The model property is set to a TodoModel instance by TodoAppView when
// it instantiates this TodoView. // it instantiates this TodoView.
var model = this.model; var model = this.model;
// Re-render this view when the model changes, and destroy this view // Re-render this view when the model changes, and destroy this view
// when the model is destroyed. // when the model is destroyed.
model.after('change', this.render, this); model.after('change', this.render, this);
model.after('destroy', this.destroy, this); model.after('destroy', this.destroy, this);
}, },
render: function () { render: function () {
var container = this.container, var container = this.container,
model = this.model, model = this.model,
done = model.get('done'); done = model.get('done');
container.setContent(Y.Lang.sub(this.template, { container.setContent(Y.Lang.sub(this.template, {
checked: done ? 'checked' : '', checked: done ? 'checked' : '',
text : model.getAsHTML('text') text : model.getAsHTML('text')
})); }));
container[done ? 'addClass' : 'removeClass']('todo-done'); container[done ? 'addClass' : 'removeClass']('todo-done');
this.inputNode = container.one('.todo-input'); this.inputNode = container.one('.todo-input');
return this; return this;
}, },
// -- Event Handlers ------------------------------------------------------- // -- Event Handlers -------------------------------------------------------
// Toggles this item into edit mode. // Toggles this item into edit mode.
edit: function () { edit: function () {
this.container.addClass('editing'); this.container.addClass('editing');
this.inputNode.focus(); this.inputNode.focus();
}, },
// When the enter key is pressed, focus the new todo input field. This // When the enter key is pressed, focus the new todo input field. This
// causes a blur event on the current edit field, which calls the save() // causes a blur event on the current edit field, which calls the save()
// handler below. // handler below.
enter: function (e) { enter: function (e) {
if (e.keyCode === 13) { // enter key if (e.keyCode === 13) { // enter key
Y.one('#new-todo').focus(); Y.one('#new-todo').focus();
} }
}, },
// Removes this item from the list. // Removes this item from the list.
remove: function (e) { remove: function (e) {
e.preventDefault(); e.preventDefault();
this.constructor.superclass.remove.call(this); this.constructor.superclass.remove.call(this);
this.model.destroy({'delete': true}); this.model.destroy({'delete': true});
}, },
// Toggles this item out of edit mode and saves it. // Toggles this item out of edit mode and saves it.
save: function () { save: function () {
this.container.removeClass('editing'); this.container.removeClass('editing');
this.model.set('text', this.inputNode.get('value')).save(); this.model.set('text', this.inputNode.get('value')).save();
}, },
// Toggles the `done` state on this item's model. // Toggles the `done` state on this item's model.
toggleDone: function () { toggleDone: function () {
this.model.toggleDone(); this.model.toggleDone();
} }
}); });
// -- localStorage Sync Implementation ----------------------------------------- // -- localStorage Sync Implementation -----------------------------------------
...@@ -343,99 +343,99 @@ TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], { ...@@ -343,99 +343,99 @@ TodoView = Y.TodoView = Y.Base.create('todoView', Y.View, [], {
// TodoModel and TodoList instances above use it to save and load items. // TodoModel and TodoList instances above use it to save and load items.
function LocalStorageSync(key) { function LocalStorageSync(key) {
var localStorage; var localStorage;
if (!key) { if (!key) {
Y.error('No storage key specified.'); Y.error('No storage key specified.');
} }
if (Y.config.win.localStorage) { if (Y.config.win.localStorage) {
localStorage = Y.config.win.localStorage; localStorage = Y.config.win.localStorage;
} }
// Try to retrieve existing data from localStorage, if there is any. // Try to retrieve existing data from localStorage, if there is any.
// Otherwise, initialize `data` to an empty object. // Otherwise, initialize `data` to an empty object.
var data = Y.JSON.parse((localStorage && localStorage.getItem(key)) || '{}'); var data = Y.JSON.parse((localStorage && localStorage.getItem(key)) || '{}');
// Delete a model with the specified id. // Delete a model with the specified id.
function destroy(id) { function destroy(id) {
var modelHash; var modelHash;
if ((modelHash = data[id])) { if ((modelHash = data[id])) {
delete data[id]; delete data[id];
save(); save();
} }
return modelHash; return modelHash;
} }
// Generate a unique id to assign to a newly-created model. // Generate a unique id to assign to a newly-created model.
function generateId() { function generateId() {
var id = '', var id = '',
i = 4; i = 4;
while (i--) { while (i--) {
id += (((1 + Math.random()) * 0x10000) | 0) id += (((1 + Math.random()) * 0x10000) | 0)
.toString(16).substring(1); .toString(16).substring(1);
} }
return id; return id;
} }
// Loads a model with the specified id. This method is a little tricky, // Loads a model with the specified id. This method is a little tricky,
// since it handles loading for both individual models and for an entire // since it handles loading for both individual models and for an entire
// model list. // model list.
// //
// If an id is specified, then it loads a single model. If no id is // If an id is specified, then it loads a single model. If no id is
// specified then it loads an array of all models. This allows the same sync // specified then it loads an array of all models. This allows the same sync
// layer to be used for both the TodoModel and TodoList classes. // layer to be used for both the TodoModel and TodoList classes.
function get(id) { function get(id) {
return id ? data[id] : Y.Object.values(data); return id ? data[id] : Y.Object.values(data);
} }
// Saves the entire `data` object to localStorage. // Saves the entire `data` object to localStorage.
function save() { function save() {
localStorage && localStorage.setItem(key, Y.JSON.stringify(data)); localStorage && localStorage.setItem(key, Y.JSON.stringify(data));
} }
// Sets the id attribute of the specified model (generating a new id if // Sets the id attribute of the specified model (generating a new id if
// necessary), then saves it to localStorage. // necessary), then saves it to localStorage.
function set(model) { function set(model) {
var hash = model.toJSON(), var hash = model.toJSON(),
idAttribute = model.idAttribute; idAttribute = model.idAttribute;
if (!Y.Lang.isValue(hash[idAttribute])) { if (!Y.Lang.isValue(hash[idAttribute])) {
hash[idAttribute] = generateId(); hash[idAttribute] = generateId();
} }
data[hash[idAttribute]] = hash; data[hash[idAttribute]] = hash;
save(); save();
return hash; return hash;
} }
// Returns a `sync()` function that can be used with either a Model or a // Returns a `sync()` function that can be used with either a Model or a
// ModelList instance. // ModelList instance.
return function (action, options, callback) { return function (action, options, callback) {
// `this` refers to the Model or ModelList instance to which this sync // `this` refers to the Model or ModelList instance to which this sync
// method is attached. // method is attached.
var isModel = Y.Model && this instanceof Y.Model; var isModel = Y.Model && this instanceof Y.Model;
switch (action) { switch (action) {
case 'create': // intentional fallthru case 'create': // intentional fallthru
case 'update': case 'update':
callback(null, set(this)); callback(null, set(this));
return; return;
case 'read': case 'read':
callback(null, get(isModel && this.get('id'))); callback(null, get(isModel && this.get('id')));
return; return;
case 'delete': case 'delete':
callback(null, destroy(isModel && this.get('id'))); callback(null, destroy(isModel && this.get('id')));
return; return;
} }
}; };
} }
// -- Start your engines! ------------------------------------------------------ // -- Start your engines! ------------------------------------------------------
......
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