diff options
author | Franco Correa <franco@francocorrea.com.uy> | 2014-05-03 02:24:14 (GMT) |
---|---|---|
committer | Franco Correa <franco@francocorrea.com.uy> | 2014-05-03 02:24:14 (GMT) |
commit | dfb33607ef494043f9880f35c8305012c3e078a6 (patch) | |
tree | 0d16b712b75b96208373c57e41f6f439ba4d0582 |
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 4 | ||||
-rwxr-xr-x | css/todos.css | 448 | ||||
-rw-r--r-- | images/bg.png | bin | 0 -> 2126 bytes | |||
-rwxr-xr-x | images/destroy.png | bin | 0 -> 555 bytes | |||
-rw-r--r-- | images/spinner.gif | bin | 0 -> 63429 bytes | |||
-rwxr-xr-x | index.html | 104 | ||||
-rwxr-xr-x | js/todos.js | 341 | ||||
-rwxr-xr-x | js/underscore-1.1.6.js | 807 |
9 files changed, 1705 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..78e8ec9 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +iHomework +Simple cloud homework management... + +The data is hosted in Parse Cloud diff --git a/css/todos.css b/css/todos.css new file mode 100755 index 0000000..7aa00fb --- /dev/null +++ b/css/todos.css @@ -0,0 +1,448 @@ +html,body { + margin: 0; + padding: 0; +} + +button { + margin: 0; + padding: 0; + border: 0; + background: none; + font-size: 100%; + vertical-align: baseline; + font-family: inherit; + color: inherit; + -webkit-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +body { + font: 14px 'Roboto', sans-serif; + font-weight: 300; + line-height: 1.4em; + background: #22BCE0; + color: #4d4d4d; + width: 550px; + margin: 0 auto; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + -o-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#todoapp { + background: #fff; + background: rgba(255, 255, 255, 0.9); + margin: 150px 0 40px 0; + position: relative; +} + + + +#todoapp input::-webkit-input-placeholder { + font-family: 'Roboto', sans-serif; + +} + +#todoapp input:-moz-placeholder { + font-family: 'Roboto', sans-serif; +} + +#todoapp h1 { + position: absolute; + top: -120px; + width: 100%; + font-size: 70px; + font-weight: normal; + text-align: center; + color: white; + text-shadow: 0 5px 0 rgba(0,0,0,0.2); +} + +.section { + position: relative; +} + +#header { + padding-top: 15px; +} + +#header:before { + content: ''; + position: absolute; + top: 0; + right: 0; + left: 0; + height: 15px; + z-index: 2; + border-top-left-radius: 1px; + border-top-right-radius: 1px; +} + +#new-todo, +.edit { + position: relative; + margin: 0; + width: 100%; + font-size: 24px; + font-family: 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); + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + -ms-box-sizing: border-box; + -o-box-sizing: border-box; + box-sizing: border-box; + -webkit-font-smoothing: antialiased; + -moz-font-smoothing: antialiased; + -ms-font-smoothing: antialiased; + -o-font-smoothing: antialiased; + font-smoothing: antialiased; +} + +#new-todo { + padding: 16px 16px 16px 60px; + border: none; + background: rgba(0, 0, 0, 0.02); + z-index: 2; + box-shadow: none; +} + +#main { + position: relative; + z-index: 2; + border-top: 1px dotted #adadad; +} + +label[for='toggle-all'] { + display: none; +} + +#toggle-all { + position: absolute; + top: -56px; + left: -15px; + width: 65px; + height: 41px; + text-align: center; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; + -webkit-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} + +#toggle-all:before { + content: '»'; + font-size: 28px; + color: #d9d9d9; + padding: 0 25px 7px; +} + +#toggle-all:checked:before { + color: #737373; +} + +#todo-list { + margin: 0; + padding: 0; + list-style: none; + text-align: center; +} + +#todo-list .spinner { + margin: 15px auto; +} + +#todo-list li { + text-align: left; + position: relative; + font-size: 24px; + border-bottom: 1px dotted #ccc; +} + +#todo-list li:last-child { + border-bottom: none; +} + +#todo-list li.editing { + border-bottom: none; + padding: 0; +} + +#todo-list li.editing .edit { + display: block; + width: 506px; + padding: 13px 17px 12px 17px; + margin: 0 0 0 43px; +} + +#todo-list li.editing .view { + display: none; +} + +#todo-list li .toggle { + text-align: center; + width: 40px; + height: 40px; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +#todo-list li .toggle:after { + font-size: 18px; + content: '✔'; + line-height: 43px; /* 40 + a couple of pixels visual adjustment */ + font-size: 20px; + color: #d9d9d9; + text-shadow: 0 -1px 0 #bfbfbf; +} + +#todo-list li .toggle:checked:after { + color: #85ada7; + text-shadow: 0 1px 0 #669991; + bottom: 1px; + position: relative; +} + +#todo-list li label { + word-break: break-word; + margin: 15px 15px 15px 60px; + display: inline-block; + line-height: 1.2; + -webkit-transition: color 0.4s; + -moz-transition: color 0.4s; + -ms-transition: color 0.4s; + -o-transition: color 0.4s; + transition: color 0.4s; +} + +#todo-list li.completed label { + color: #a9a9a9; + text-decoration: line-through; +} + +#todo-list li .todo-destroy { + display: none; + position: absolute; + top: 0; + right: 10px; + bottom: 0; + width: 40px; + height: 40px; + margin: auto 0; + font-size: 22px; + color: #a88a8a; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -ms-transition: all 0.2s; + -o-transition: all 0.2s; + transition: all 0.2s; +} + +#todo-list li .todo-destroy:hover { + text-shadow: 0 0 1px #000, + 0 0 10px rgba(199, 107, 107, 0.8); + -webkit-transform: scale(1.3); + -moz-transform: scale(1.3); + -ms-transform: scale(1.3); + -o-transform: scale(1.3); + transform: scale(1.3); +} + +#todo-list li .todo-destroy:after { + content: '✖'; +} + +#todo-list li:hover .todo-destroy { + display: block; +} + +#todo-list li .edit { + display: none; +} + +#todo-list li.editing:last-child { + margin-bottom: -1px; +} + +#footer { + color: #777; + padding: 0 15px; + position: absolute; + right: 0; + bottom: -31px; + left: 0; + z-index: 1; + text-align: center; +} + +#footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 31px; + left: 0; + height: 100px; + z-index: -1; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), + 0 0px 0 0px rgba(255, 255, 255, 0.8), + 0 7px 1px -3px rgba(0, 0, 0, 0.3), + 0 42px 0 -6px rgba(255, 255, 255, 0.8), + 0 43px 2px -6px rgba(0, 0, 0, 0.2); +} + +#todo-count { + float: left; + text-align: left; +} + +#filters { + margin: 0; + padding: 0; + list-style: none; + position: absolute; + right: 0; + left: 0; +} + +#filters li { + display: inline; +} + +#filters li a { + color: #83756f; + margin: 2px; + text-decoration: none; +} + +#filters li a.selected { + font-weight: bold; +} + +#clear-completed { + float: right; + line-height: 20px; + text-decoration: none; + background: rgba(0, 0, 0, 0.1); + font-size: 11px; + padding: 0 10px; + position: relative; + border-radius: 3px; + box-shadow: 0 2px 0 rgba(0,0,0,0.2); + +} + +#clear-completed:hover { + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 2px 0 rgba(0,0,0,0.2); +} + +#info { + margin: 65px auto 0; + color: #a6a6a6; + font-size: 12px; + text-shadow: 0 1px 0 rgba(255, 255, 255, 0.7); + text-align: center; +} + +#info a { + color: inherit; +} + +/* + Hack to remove background from Mobile Safari. + Can't use it globally since it destroys checkboxes in Firefox and Opera + */ +@media screen and (-webkit-min-device-pixel-ratio:0) { + #toggle-all, + #todo-list li .toggle { + background: none; + } +} + +#todoapp .content .login { + width: 350px; + margin: 35px auto 15px auto; +} + +#todoapp .content .login h2 { + font-weight: normal; + font-size: 25px; + margin-bottom: 20px; +} + +#todoapp .content .login input { + display: block; + margin-bottom: 10px; + font-size: 20px; + padding: 5px; + width: 340px; + +} + +#todoapp .content .login .signup-form { + margin-top: 25px; + +} + +#todoapp .content .login button { + padding: 5px 10px; + color: white; + font-weight: 400; + background: #eee; + font-size: 15px; + background: #DC4E23; + margin-bottom: 25px; + font-family: inherit; + box-shadow: 0 4px 0 rgba(0,0,0,0.2); + +} + +#todoapp .content .login .error { + border: 1px solid red; + background: #FDEFF0; + padding: 5px 15px; + margin-bottom: 15px; +} + +#todoapp .content #user-info { + padding: 5px 10px; + border-top: 1px solid #ededed; + margin-bottom: 0px; + text-align: right; +} + + +#todoapp .content .mark-all-done { + margin-left: 8px; +} + +#credits { + color: #888; + text-align: center; + padding-top: 25px; +} + +#credits a { + color: #888; +} + diff --git a/images/bg.png b/images/bg.png Binary files differnew file mode 100644 index 0000000..b2a7600 --- /dev/null +++ b/images/bg.png diff --git a/images/destroy.png b/images/destroy.png Binary files differnew file mode 100755 index 0000000..56d7637 --- /dev/null +++ b/images/destroy.png diff --git a/images/spinner.gif b/images/spinner.gif Binary files differnew file mode 100644 index 0000000..5b13367 --- /dev/null +++ b/images/spinner.gif diff --git a/index.html b/index.html new file mode 100755 index 0000000..37d60f2 --- /dev/null +++ b/index.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> + <title>iHomework</title> + <link href='http://fonts.googleapis.com/css?family=Roboto:400,300,500,700,900' rel='stylesheet' type='text/css'> + <link href="css/todos.css" media="all" rel="stylesheet" type="text/css"/> + <script src="http://code.jquery.com/jquery-2.0.3.min.js"></script> + <script src="js/underscore-1.1.6.js"></script> + <script src="http://www.parsecdn.com/js/parse-1.2.13.min.js"></script> + <script src="js/todos.js"></script> + </head> + + <body> + + <!-- Todo App Interface --> + + <div id="todoapp"> + <div class="title"> + <h1>iHomework</h1> + </div> + + <div class="content"> + </div> + </div> + + <script type="text/template" id="login-template"> + <header id="header"></header> + <div class="login"> + <form class="login-form"> + <h2>You know how this works?</h2> + <div class="error" style="display:none"></div> + <input type="text" id="login-username" class="input_login" placeholder="User" /> + <input type="password" id="login-password" class="input_login" placeholder="Password" /> + <button>Login</button> + </form> + + <form class="signup-form"> + <h2>If you are new register here!</h2> + <div class="error" style="display:none"></div> + <input type="text" id="signup-username" class="input_login" placeholder="Choose an username" /> + <input type="password" id="signup-password" class="input_login" placeholder="Choose a password" /> + <button>Registrarse</button> + </form> + </div> + </script> + + <script type="text/template" id="manage-todos-template"> + <div id="user-info"> + Logged as <strong><%= Parse.User.current().get("username") %></strong> (<a href="#" class="log-out">Logout</a>) + </div> + + <div class="section"> + + <header id="header"> + <input id="new-todo" placeholder="What homework do you have?" type="text" /> + </header> + + <div id="main"> + <input id="toggle-all" type="checkbox"> + <label for="toggle-all">Mark all as complete</label> + + <ul id="todo-list"> + <img src='images/spinner.gif' class='spinner' /> + </ul> + </div> + + <div id="todo-stats"></div> + </div> + </script> + + <script type="text/template" id="item-template"> + <li class="<%= done ? 'completed' : '' %>"> + <div class="view"> + <input class="toggle" type="checkbox" <%= done ? 'checked="checked"' : '' %>> + <label class="todo-content"><%= content %></label> + <button class="todo-destroy"></button> + </div> + <input class="edit" value="<%= content %>"> + </li> + </script> + + <script type="text/template" id="stats-template"> + <footer id="footer"> + <span id="todo-count"><strong><%= remaining %></strong> <%= remaining == 1 ? ' task left' : ' tasks left' %></span> + <ul id="filters"> + <li> + <a href="javascript:void(0)" id="all" class="selected">All</a> + </li> + <li> + <a href="javascript:void(0)" id="active">Active</a> + </li> + <li> + <a href="javascript:void(0)" id="completed">Complete</a> + </li> + </ul> + <button id="clear-completed">Clear completed (<%= done %>)</button> + </footer> + </script> + + </body> + +</html> diff --git a/js/todos.js b/js/todos.js new file mode 100755 index 0000000..dbe31c4 --- /dev/null +++ b/js/todos.js @@ -0,0 +1,341 @@ +$(function() { + + Parse.$ = jQuery; + + Parse.initialize("NmUIWDKlcHA583ECNxyUSq2G7tt7R7NHKkWKHVd8", "3BwEtyKq519LhR8YIlxMxUa7artuOPfyQE6A801c"); + + var Todo = Parse.Object.extend("Todo", { + defaults: { + content: "sin deberes...", + done: false + }, + + initialize: function() { + if (!this.get("content")) { + this.set({"content": this.defaults.content}); + } + }, + + toggle: function() { + this.save({done: !this.get("done")}); + } + }); + + var AppState = Parse.Object.extend("AppState", { + defaults: { + filter: "all" + } + }); + + var TodoList = Parse.Collection.extend({ + + model: Todo, + + done: function() { + return this.filter(function(todo){ return todo.get('done'); }); + }, + + remaining: function() { + return this.without.apply(this, this.done()); + }, + + nextOrder: function() { + if (!this.length) return 1; + return this.last().get('order') + 1; + }, + + comparator: function(todo) { + return todo.get('order'); + } + + }); + + var TodoView = Parse.View.extend({ + + tagName: "li", + + template: _.template($('#item-template').html()), + + events: { + "click .toggle" : "toggleDone", + "dblclick label.todo-content" : "edit", + "click .todo-destroy" : "clear", + "keypress .edit" : "updateOnEnter", + "blur .edit" : "close" + }, + + initialize: function() { + _.bindAll(this, 'render', 'close', 'remove'); + this.model.bind('change', this.render); + this.model.bind('destroy', this.remove); + }, + + render: function() { + $(this.el).html(this.template(this.model.toJSON())); + this.input = this.$('.edit'); + return this; + }, + + toggleDone: function() { + this.model.toggle(); + }, + + edit: function() { + $(this.el).addClass("editing"); + this.input.focus(); + }, + + close: function() { + this.model.save({content: this.input.val()}); + $(this.el).removeClass("editing"); + }, + + updateOnEnter: function(e) { + if (e.keyCode == 13) this.close(); + }, + + clear: function() { + this.model.destroy(); + } + + }); + + var ManageTodosView = Parse.View.extend({ + + statsTemplate: _.template($('#stats-template').html()), + + events: { + "keypress #new-todo": "createOnEnter", + "click #clear-completed": "clearCompleted", + "click #toggle-all": "toggleAllComplete", + "click .log-out": "logOut", + "click ul#filters a": "selectFilter" + }, + + el: ".content", + + initialize: function() { + var self = this; + + _.bindAll(this, 'addOne', 'addAll', 'addSome', 'render', 'toggleAllComplete', 'logOut', 'createOnEnter'); + + this.$el.html(_.template($("#manage-todos-template").html())); + + this.input = this.$("#new-todo"); + this.allCheckbox = this.$("#toggle-all")[0]; + + this.todos = new TodoList; + + this.todos.query = new Parse.Query(Todo); + this.todos.query.equalTo("user", Parse.User.current()); + + this.todos.bind('add', this.addOne); + this.todos.bind('reset', this.addAll); + this.todos.bind('all', this.render); + + this.todos.fetch(); + + state.on("change", this.filter, this); + }, + + logOut: function(e) { + Parse.User.logOut(); + new LogInView(); + this.undelegateEvents(); + delete this; + }, + + render: function() { + var done = this.todos.done().length; + var remaining = this.todos.remaining().length; + + this.$('#todo-stats').html(this.statsTemplate({ + total: this.todos.length, + done: done, + remaining: remaining + })); + + this.delegateEvents(); + + this.allCheckbox.checked = !remaining; + }, + + selectFilter: function(e) { + var el = $(e.target); + var filterValue = el.attr("id"); + state.set({filter: filterValue}); + Parse.history.navigate(filterValue); + }, + + filter: function() { + var filterValue = state.get("filter"); + this.$("ul#filters a").removeClass("selected"); + this.$("ul#filters a#" + filterValue).addClass("selected"); + if (filterValue === "all") { + this.addAll(); + } else if (filterValue === "completed") { + this.addSome(function(item) { return item.get('done') }); + } else { + this.addSome(function(item) { return !item.get('done') }); + } + }, + + resetFilters: function() { + this.$("ul#filters a").removeClass("selected"); + this.$("ul#filters a#all").addClass("selected"); + this.addAll(); + }, + + addOne: function(todo) { + var view = new TodoView({model: todo}); + this.$("#todo-list").append(view.render().el); + }, + + addAll: function(collection, filter) { + this.$("#todo-list").html(""); + this.todos.each(this.addOne); + }, + + addSome: function(filter) { + var self = this; + this.$("#todo-list").html(""); + this.todos.chain().filter(filter).each(function(item) { self.addOne(item) }); + }, + + createOnEnter: function(e) { + var self = this; + if (e.keyCode != 13) return; + + this.todos.create({ + content: this.input.val(), + order: this.todos.nextOrder(), + done: false, + user: Parse.User.current(), + ACL: new Parse.ACL(Parse.User.current()) + }); + + this.input.val(''); + this.resetFilters(); + }, + + clearCompleted: function() { + _.each(this.todos.done(), function(todo){ todo.destroy(); }); + return false; + }, + + toggleAllComplete: function () { + var done = this.allCheckbox.checked; + this.todos.each(function (todo) { todo.save({'done': done}); }); + } + }); + + var LogInView = Parse.View.extend({ + events: { + "submit form.login-form": "logIn", + "submit form.signup-form": "signUp" + }, + + el: ".content", + + initialize: function() { + _.bindAll(this, "logIn", "signUp"); + this.render(); + }, + + logIn: function(e) { + var self = this; + var username = this.$("#login-username").val(); + var password = this.$("#login-password").val(); + + Parse.User.logIn(username, password, { + success: function(user) { + new ManageTodosView(); + self.undelegateEvents(); + delete self; + }, + + error: function(user, error) { + self.$(".login-form .error").html("Invalid username or password. Please try again.").show(); + this.$(".login-form button").removeAttr("disabled"); + } + }); + + this.$(".login-form button").attr("disabled", "disabled"); + + return false; + }, + + signUp: function(e) { + var self = this; + var username = this.$("#signup-username").val(); + var password = this.$("#signup-password").val(); + + Parse.User.signUp(username, password, { ACL: new Parse.ACL() }, { + success: function(user) { + new ManageTodosView(); + self.undelegateEvents(); + delete self; + }, + + error: function(user, error) { + self.$(".signup-form .error").html(error.message).show(); + this.$(".signup-form button").removeAttr("disabled"); + } + }); + + this.$(".signup-form button").attr("disabled", "disabled"); + + return false; + }, + + render: function() { + this.$el.html(_.template($("#login-template").html())); + this.delegateEvents(); + } + }); + + var AppView = Parse.View.extend({ + el: $("#todoapp"), + + initialize: function() { + this.render(); + }, + + render: function() { + if (Parse.User.current()) { + new ManageTodosView(); + } else { + new LogInView(); + } + } + }); + + var AppRouter = Parse.Router.extend({ + routes: { + "all": "all", + "active": "active", + "completed": "completed" + }, + + initialize: function(options) { + }, + + all: function() { + state.set({ filter: "all" }); + }, + + active: function() { + state.set({ filter: "active" }); + }, + + completed: function() { + state.set({ filter: "completed" }); + } + }); + + var state = new AppState; + + new AppRouter; + new AppView; + Parse.history.start(); +}); diff --git a/js/underscore-1.1.6.js b/js/underscore-1.1.6.js new file mode 100755 index 0000000..eaba008 --- /dev/null +++ b/js/underscore-1.1.6.js @@ -0,0 +1,807 @@ +// Underscore.js 1.1.6 +// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc. +// Underscore is freely distributable under the MIT license. +// Portions of Underscore are inspired or borrowed from Prototype, +// Oliver Steele's Functional, and John Resig's Micro-Templating. +// For all details and documentation: +// http://documentcloud.github.com/underscore + +(function() { + + // Baseline setup + // -------------- + + // Establish the root object, `window` in the browser, or `global` on the server. + var root = this; + + // Save the previous value of the `_` variable. + var previousUnderscore = root._; + + // Establish the object that gets returned to break out of a loop iteration. + var breaker = {}; + + // Save bytes in the minified (but not gzipped) version: + var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; + + // Create quick reference variables for speed access to core prototypes. + var slice = ArrayProto.slice, + unshift = ArrayProto.unshift, + toString = ObjProto.toString, + hasOwnProperty = ObjProto.hasOwnProperty; + + // All **ECMAScript 5** native function implementations that we hope to use + // are declared here. + var + nativeForEach = ArrayProto.forEach, + nativeMap = ArrayProto.map, + nativeReduce = ArrayProto.reduce, + nativeReduceRight = ArrayProto.reduceRight, + nativeFilter = ArrayProto.filter, + nativeEvery = ArrayProto.every, + nativeSome = ArrayProto.some, + nativeIndexOf = ArrayProto.indexOf, + nativeLastIndexOf = ArrayProto.lastIndexOf, + nativeIsArray = Array.isArray, + nativeKeys = Object.keys, + nativeBind = FuncProto.bind; + + // Create a safe reference to the Underscore object for use below. + var _ = function(obj) { return new wrapper(obj); }; + + // Export the Underscore object for **CommonJS**, with backwards-compatibility + // for the old `require()` API. If we're not in CommonJS, add `_` to the + // global object. + if (typeof module !== 'undefined' && module.exports) { + module.exports = _; + _._ = _; + } else { + root._ = _; + } + + // Current version. + _.VERSION = '1.1.6'; + + // Collection Functions + // -------------------- + + // The cornerstone, an `each` implementation, aka `forEach`. + // Handles objects implementing `forEach`, arrays, and raw objects. + // Delegates to **ECMAScript 5**'s native `forEach` if available. + var each = _.each = _.forEach = function(obj, iterator, context) { + if (obj == null) return; + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else if (_.isNumber(obj.length)) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === breaker) return; + } + } else { + for (var key in obj) { + if (hasOwnProperty.call(obj, key)) { + if (iterator.call(context, obj[key], key, obj) === breaker) return; + } + } + } + }; + + // Return the results of applying the iterator to each element. + // Delegates to **ECMAScript 5**'s native `map` if available. + _.map = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); + each(obj, function(value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + // **Reduce** builds up a single result from a list of values, aka `inject`, + // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. + _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { + var initial = memo !== void 0; + if (obj == null) obj = []; + if (nativeReduce && obj.reduce === nativeReduce) { + if (context) iterator = _.bind(iterator, context); + return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); + } + each(obj, function(value, index, list) { + if (!initial && index === 0) { + memo = value; + initial = true; + } else { + memo = iterator.call(context, memo, value, index, list); + } + }); + if (!initial) throw new TypeError("Reduce of empty array with no initial value"); + return memo; + }; + + // The right-associative version of reduce, also known as `foldr`. + // Delegates to **ECMAScript 5**'s native `reduceRight` if available. + _.reduceRight = _.foldr = function(obj, iterator, memo, context) { + if (obj == null) obj = []; + if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { + if (context) iterator = _.bind(iterator, context); + return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); + } + var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse(); + return _.reduce(reversed, iterator, memo, context); + }; + + // Return the first value which passes a truth test. Aliased as `detect`. + _.find = _.detect = function(obj, iterator, context) { + var result; + any(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) { + result = value; + return true; + } + }); + return result; + }; + + // Return all the elements that pass a truth test. + // Delegates to **ECMAScript 5**'s native `filter` if available. + // Aliased as `select`. + _.filter = _.select = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); + each(obj, function(value, index, list) { + if (iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Return all the elements for which a truth test fails. + _.reject = function(obj, iterator, context) { + var results = []; + if (obj == null) return results; + each(obj, function(value, index, list) { + if (!iterator.call(context, value, index, list)) results[results.length] = value; + }); + return results; + }; + + // Determine whether all of the elements match a truth test. + // Delegates to **ECMAScript 5**'s native `every` if available. + // Aliased as `all`. + _.every = _.all = function(obj, iterator, context) { + var result = true; + if (obj == null) return result; + if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); + each(obj, function(value, index, list) { + if (!(result = result && iterator.call(context, value, index, list))) return breaker; + }); + return result; + }; + + // Determine if at least one element in the object matches a truth test. + // Delegates to **ECMAScript 5**'s native `some` if available. + // Aliased as `any`. + var any = _.some = _.any = function(obj, iterator, context) { + iterator || (iterator = _.identity); + var result = false; + if (obj == null) return result; + if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); + each(obj, function(value, index, list) { + if (result = iterator.call(context, value, index, list)) return breaker; + }); + return result; + }; + + // Determine if a given value is included in the array or object using `===`. + // Aliased as `contains`. + _.include = _.contains = function(obj, target) { + var found = false; + if (obj == null) return found; + if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; + any(obj, function(value) { + if (found = value === target) return true; + }); + return found; + }; + + // Invoke a method (with arguments) on every item in a collection. + _.invoke = function(obj, method) { + var args = slice.call(arguments, 2); + return _.map(obj, function(value) { + return (method.call ? method || value : value[method]).apply(value, args); + }); + }; + + // Convenience version of a common use case of `map`: fetching a property. + _.pluck = function(obj, key) { + return _.map(obj, function(value){ return value[key]; }); + }; + + // Return the maximum element or (element-based computation). + _.max = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.max.apply(Math, obj); + var result = {computed : -Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed >= result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Return the minimum element (or element-based computation). + _.min = function(obj, iterator, context) { + if (!iterator && _.isArray(obj)) return Math.min.apply(Math, obj); + var result = {computed : Infinity}; + each(obj, function(value, index, list) { + var computed = iterator ? iterator.call(context, value, index, list) : value; + computed < result.computed && (result = {value : value, computed : computed}); + }); + return result.value; + }; + + // Sort the object's values by a criterion produced by an iterator. + _.sortBy = function(obj, iterator, context) { + return _.pluck(_.map(obj, function(value, index, list) { + return { + value : value, + criteria : iterator.call(context, value, index, list) + }; + }).sort(function(left, right) { + var a = left.criteria, b = right.criteria; + return a < b ? -1 : a > b ? 1 : 0; + }), 'value'); + }; + + // Use a comparator function to figure out at what index an object should + // be inserted so as to maintain order. Uses binary search. + _.sortedIndex = function(array, obj, iterator) { + iterator || (iterator = _.identity); + var low = 0, high = array.length; + while (low < high) { + var mid = (low + high) >> 1; + iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; + } + return low; + }; + + // Safely convert anything iterable into a real, live array. + _.toArray = function(iterable) { + if (!iterable) return []; + if (iterable.toArray) return iterable.toArray(); + if (_.isArray(iterable)) return iterable; + if (_.isArguments(iterable)) return slice.call(iterable); + return _.values(iterable); + }; + + // Return the number of elements in an object. + _.size = function(obj) { + return _.toArray(obj).length; + }; + + // Array Functions + // --------------- + + // Get the first element of an array. Passing **n** will return the first N + // values in the array. Aliased as `head`. The **guard** check allows it to work + // with `_.map`. + _.first = _.head = function(array, n, guard) { + return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; + }; + + // Returns everything but the first entry of the array. Aliased as `tail`. + // Especially useful on the arguments object. Passing an **index** will return + // the rest of the values in the array from that index onward. The **guard** + // check allows it to work with `_.map`. + _.rest = _.tail = function(array, index, guard) { + return slice.call(array, (index == null) || guard ? 1 : index); + }; + + // Get the last element of an array. + _.last = function(array) { + return array[array.length - 1]; + }; + + // Trim out all falsy values from an array. + _.compact = function(array) { + return _.filter(array, function(value){ return !!value; }); + }; + + // Return a completely flattened version of an array. + _.flatten = function(array) { + return _.reduce(array, function(memo, value) { + if (_.isArray(value)) return memo.concat(_.flatten(value)); + memo[memo.length] = value; + return memo; + }, []); + }; + + // Return a version of the array that does not contain the specified value(s). + _.without = function(array) { + var values = slice.call(arguments, 1); + return _.filter(array, function(value){ return !_.include(values, value); }); + }; + + // Produce a duplicate-free version of the array. If the array has already + // been sorted, you have the option of using a faster algorithm. + // Aliased as `unique`. + _.uniq = _.unique = function(array, isSorted) { + return _.reduce(array, function(memo, el, i) { + if (0 == i || (isSorted === true ? _.last(memo) != el : !_.include(memo, el))) memo[memo.length] = el; + return memo; + }, []); + }; + + // Produce an array that contains every item shared between all the + // passed-in arrays. + _.intersect = function(array) { + var rest = slice.call(arguments, 1); + return _.filter(_.uniq(array), function(item) { + return _.every(rest, function(other) { + return _.indexOf(other, item) >= 0; + }); + }); + }; + + // Zip together multiple lists into a single array -- elements that share + // an index go together. + _.zip = function() { + var args = slice.call(arguments); + var length = _.max(_.pluck(args, 'length')); + var results = new Array(length); + for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); + return results; + }; + + // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), + // we need this function. Return the position of the first occurrence of an + // item in an array, or -1 if the item is not included in the array. + // Delegates to **ECMAScript 5**'s native `indexOf` if available. + // If the array is large and already in sort order, pass `true` + // for **isSorted** to use binary search. + _.indexOf = function(array, item, isSorted) { + if (array == null) return -1; + var i, l; + if (isSorted) { + i = _.sortedIndex(array, item); + return array[i] === item ? i : -1; + } + if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); + for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i; + return -1; + }; + + + // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. + _.lastIndexOf = function(array, item) { + if (array == null) return -1; + if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); + var i = array.length; + while (i--) if (array[i] === item) return i; + return -1; + }; + + // Generate an integer Array containing an arithmetic progression. A port of + // the native Python `range()` function. See + // [the Python documentation](http://docs.python.org/library/functions.html#range). + _.range = function(start, stop, step) { + if (arguments.length <= 1) { + stop = start || 0; + start = 0; + } + step = arguments[2] || 1; + + var len = Math.max(Math.ceil((stop - start) / step), 0); + var idx = 0; + var range = new Array(len); + + while(idx < len) { + range[idx++] = start; + start += step; + } + + return range; + }; + + // Function (ahem) Functions + // ------------------ + + // Create a function bound to a given object (assigning `this`, and arguments, + // optionally). Binding with arguments is also known as `curry`. + // Delegates to **ECMAScript 5**'s native `Function.bind` if available. + // We check for `func.bind` first, to fail fast when `func` is undefined. + _.bind = function(func, obj) { + if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); + var args = slice.call(arguments, 2); + return function() { + return func.apply(obj, args.concat(slice.call(arguments))); + }; + }; + + // Bind all of an object's methods to that object. Useful for ensuring that + // all callbacks defined on an object belong to it. + _.bindAll = function(obj) { + var funcs = slice.call(arguments, 1); + if (funcs.length == 0) funcs = _.functions(obj); + each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); + return obj; + }; + + // Memoize an expensive function by storing its results. + _.memoize = function(func, hasher) { + var memo = {}; + hasher || (hasher = _.identity); + return function() { + var key = hasher.apply(this, arguments); + return hasOwnProperty.call(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); + }; + }; + + // Delays a function for the given number of milliseconds, and then calls + // it with the arguments supplied. + _.delay = function(func, wait) { + var args = slice.call(arguments, 2); + return setTimeout(function(){ return func.apply(func, args); }, wait); + }; + + // Defers a function, scheduling it to run after the current call stack has + // cleared. + _.defer = function(func) { + return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); + }; + + // Internal function used to implement `_.throttle` and `_.debounce`. + var limit = function(func, wait, debounce) { + var timeout; + return function() { + var context = this, args = arguments; + var throttler = function() { + timeout = null; + func.apply(context, args); + }; + if (debounce) clearTimeout(timeout); + if (debounce || !timeout) timeout = setTimeout(throttler, wait); + }; + }; + + // Returns a function, that, when invoked, will only be triggered at most once + // during a given window of time. + _.throttle = function(func, wait) { + return limit(func, wait, false); + }; + + // Returns a function, that, as long as it continues to be invoked, will not + // be triggered. The function will be called after it stops being called for + // N milliseconds. + _.debounce = function(func, wait) { + return limit(func, wait, true); + }; + + // Returns a function that will be executed at most one time, no matter how + // often you call it. Useful for lazy initialization. + _.once = function(func) { + var ran = false, memo; + return function() { + if (ran) return memo; + ran = true; + return memo = func.apply(this, arguments); + }; + }; + + // Returns the first function passed as an argument to the second, + // allowing you to adjust arguments, run code before and after, and + // conditionally execute the original function. + _.wrap = function(func, wrapper) { + return function() { + var args = [func].concat(slice.call(arguments)); + return wrapper.apply(this, args); + }; + }; + + // Returns a function that is the composition of a list of functions, each + // consuming the return value of the function that follows. + _.compose = function() { + var funcs = slice.call(arguments); + return function() { + var args = slice.call(arguments); + for (var i=funcs.length-1; i >= 0; i--) { + args = [funcs[i].apply(this, args)]; + } + return args[0]; + }; + }; + + // Returns a function that will only be executed after being called N times. + _.after = function(times, func) { + return function() { + if (--times < 1) { return func.apply(this, arguments); } + }; + }; + + + // Object Functions + // ---------------- + + // Retrieve the names of an object's properties. + // Delegates to **ECMAScript 5**'s native `Object.keys` + _.keys = nativeKeys || function(obj) { + if (obj !== Object(obj)) throw new TypeError('Invalid object'); + var keys = []; + for (var key in obj) if (hasOwnProperty.call(obj, key)) keys[keys.length] = key; + return keys; + }; + + // Retrieve the values of an object's properties. + _.values = function(obj) { + return _.map(obj, _.identity); + }; + + // Return a sorted list of the function names available on the object. + // Aliased as `methods` + _.functions = _.methods = function(obj) { + return _.filter(_.keys(obj), function(key){ return _.isFunction(obj[key]); }).sort(); + }; + + // Extend a given object with all the properties in passed-in object(s). + _.extend = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (source[prop] !== void 0) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Fill in a given object with default properties. + _.defaults = function(obj) { + each(slice.call(arguments, 1), function(source) { + for (var prop in source) { + if (obj[prop] == null) obj[prop] = source[prop]; + } + }); + return obj; + }; + + // Create a (shallow-cloned) duplicate of an object. + _.clone = function(obj) { + return _.isArray(obj) ? obj.slice() : _.extend({}, obj); + }; + + // Invokes interceptor with the obj, and then returns obj. + // The primary purpose of this method is to "tap into" a method chain, in + // order to perform operations on intermediate results within the chain. + _.tap = function(obj, interceptor) { + interceptor(obj); + return obj; + }; + + // Perform a deep comparison to check if two objects are equal. + _.isEqual = function(a, b) { + // Check object identity. + if (a === b) return true; + // Different types? + var atype = typeof(a), btype = typeof(b); + if (atype != btype) return false; + // Basic equality test (watch out for coercions). + if (a == b) return true; + // One is falsy and the other truthy. + if ((!a && b) || (a && !b)) return false; + // Unwrap any wrapped objects. + if (a._chain) a = a._wrapped; + if (b._chain) b = b._wrapped; + // One of them implements an isEqual()? + if (a.isEqual) return a.isEqual(b); + // Check dates' integer values. + if (_.isDate(a) && _.isDate(b)) return a.getTime() === b.getTime(); + // Both are NaN? + if (_.isNaN(a) && _.isNaN(b)) return false; + // Compare regular expressions. + if (_.isRegExp(a) && _.isRegExp(b)) + return a.source === b.source && + a.global === b.global && + a.ignoreCase === b.ignoreCase && + a.multiline === b.multiline; + // If a is not an object by this point, we can't handle it. + if (atype !== 'object') return false; + // Check for different array lengths before comparing contents. + if (a.length && (a.length !== b.length)) return false; + // Nothing else worked, deep compare the contents. + var aKeys = _.keys(a), bKeys = _.keys(b); + // Different object sizes? + if (aKeys.length != bKeys.length) return false; + // Recursive comparison of contents. + for (var key in a) if (!(key in b) || !_.isEqual(a[key], b[key])) return false; + return true; + }; + + // Is a given array or object empty? + _.isEmpty = function(obj) { + if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; + for (var key in obj) if (hasOwnProperty.call(obj, key)) return false; + return true; + }; + + // Is a given value a DOM element? + _.isElement = function(obj) { + return !!(obj && obj.nodeType == 1); + }; + + // Is a given value an array? + // Delegates to ECMA5's native Array.isArray + _.isArray = nativeIsArray || function(obj) { + return toString.call(obj) === '[object Array]'; + }; + + // Is a given variable an arguments object? + _.isArguments = function(obj) { + return !!(obj && hasOwnProperty.call(obj, 'callee')); + }; + + // Is a given value a function? + _.isFunction = function(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); + }; + + // Is a given value a string? + _.isString = function(obj) { + return !!(obj === '' || (obj && obj.charCodeAt && obj.substr)); + }; + + // Is a given value a number? + _.isNumber = function(obj) { + return !!(obj === 0 || (obj && obj.toExponential && obj.toFixed)); + }; + + // Is the given value `NaN`? `NaN` happens to be the only value in JavaScript + // that does not equal itself. + _.isNaN = function(obj) { + return obj !== obj; + }; + + // Is a given value a boolean? + _.isBoolean = function(obj) { + return obj === true || obj === false; + }; + + // Is a given value a date? + _.isDate = function(obj) { + return !!(obj && obj.getTimezoneOffset && obj.setUTCFullYear); + }; + + // Is the given value a regular expression? + _.isRegExp = function(obj) { + return !!(obj && obj.test && obj.exec && (obj.ignoreCase || obj.ignoreCase === false)); + }; + + // Is a given value equal to null? + _.isNull = function(obj) { + return obj === null; + }; + + // Is a given variable undefined? + _.isUndefined = function(obj) { + return obj === void 0; + }; + + // Utility Functions + // ----------------- + + // Run Underscore.js in *noConflict* mode, returning the `_` variable to its + // previous owner. Returns a reference to the Underscore object. + _.noConflict = function() { + root._ = previousUnderscore; + return this; + }; + + // Keep the identity function around for default iterators. + _.identity = function(value) { + return value; + }; + + // Run a function **n** times. + _.times = function (n, iterator, context) { + for (var i = 0; i < n; i++) iterator.call(context, i); + }; + + // Add your own custom functions to the Underscore object, ensuring that + // they're correctly added to the OOP wrapper as well. + _.mixin = function(obj) { + each(_.functions(obj), function(name){ + addToWrapper(name, _[name] = obj[name]); + }); + }; + + // Generate a unique integer id (unique within the entire client session). + // Useful for temporary DOM ids. + var idCounter = 0; + _.uniqueId = function(prefix) { + var id = idCounter++; + return prefix ? prefix + id : id; + }; + + // 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 + }; + + // 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(str, data) { + var c = _.templateSettings; + var tmpl = 'var __p=[],print=function(){__p.push.apply(__p,arguments);};' + + 'with(obj||{}){__p.push(\'' + + str.replace(/\\/g, '\\\\') + .replace(/'/g, "\\'") + .replace(c.interpolate, function(match, code) { + return "'," + code.replace(/\\'/g, "'") + ",'"; + }) + .replace(c.evaluate || null, function(match, code) { + return "');" + code.replace(/\\'/g, "'") + .replace(/[\r\n\t]/g, ' ') + "__p.push('"; + }) + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\t/g, '\\t') + + "');}return __p.join('');"; + var func = new Function('obj', tmpl); + return data ? func(data) : func; + }; + + // The OOP Wrapper + // --------------- + + // If Underscore is called as a function, it returns a wrapped object that + // can be used OO-style. This wrapper holds altered versions of all the + // underscore functions. Wrapped objects may be chained. + var wrapper = function(obj) { this._wrapped = obj; }; + + // Expose `wrapper.prototype` as `_.prototype` + _.prototype = wrapper.prototype; + + // Helper function to continue chaining intermediate results. + var result = function(obj, chain) { + return chain ? _(obj).chain() : obj; + }; + + // A method to easily add functions to the OOP wrapper. + var addToWrapper = function(name, func) { + wrapper.prototype[name] = function() { + var args = slice.call(arguments); + unshift.call(args, this._wrapped); + return result(func.apply(_, args), this._chain); + }; + }; + + // Add all of the Underscore functions to the wrapper object. + _.mixin(_); + + // Add all mutator Array functions to the wrapper. + each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + method.apply(this._wrapped, arguments); + return result(this._wrapped, this._chain); + }; + }); + + // Add all accessor Array functions to the wrapper. + each(['concat', 'join', 'slice'], function(name) { + var method = ArrayProto[name]; + wrapper.prototype[name] = function() { + return result(method.apply(this._wrapped, arguments), this._chain); + }; + }); + + // Start chaining a wrapped Underscore object. + wrapper.prototype.chain = function() { + this._chain = true; + return this; + }; + + // Extracts the result from a wrapped and chained object. + wrapper.prototype.value = function() { + return this._wrapped; + }; + +})(); |