Permalink
Browse files

pushing the refactored version of the codebase with changes by Alex M…

…acCraw in place
  • Loading branch information...
1 parent 3bce32d commit 3251bfe3c60c3f354c930bac1a59d718ea50a7ac @addyosmani committed Apr 27, 2011
View
5 README.md
@@ -1 +1,4 @@
-A Spine.js demo application that uses the bit.ly API to help shorten URLs and provide a means to archive them offline. This demo was created as part of a Spine introduction that will be up on http://addyosmani.com shortly.
+Update: Many thanks to Spine author Alex MacCraw for heavily refactoring the code written. It now employs some Spine best practices.
+
+A Spine.js demo application that uses the bit.ly API to help shorten URLs and provide a means to archive them offline. This demo was created as part of a Spine introduction that will be up on http://addyosmani.com shortly.
+
View
164 _archive/css/application.css
@@ -0,0 +1,164 @@
+html, body {
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: "Helvetica Neue", helvetica, arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.4em;
+ background: #eeeeee;
+ color: #333333;
+}
+
+#views {
+ width: 520px;
+ margin: 0 auto 40px auto;
+ background: white;
+
+ -moz-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+ -webkit-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+ -o-box-shadow: rgba(0, 0, 0, 0.2) 0 2px 6px 0;
+ box-shadow: rgba(0, 0, 0, 0.2) 0 2px 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;
+}
+
+#tasks {
+ padding: 20px;
+}
+
+#tasks h1 {
+ font-size: 36px;
+ font-weight: bold;
+ text-align: center;
+ padding: 0 0 10px 0;
+}
+
+#tasks input[type="text"] {
+ 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;
+}
+
+#tasks input::-webkit-input-placeholder {
+ font-style: italic;
+}
+
+#tasks .items {
+ margin: 10px 0;
+ list-style: none;
+}
+
+#tasks .item {
+ padding: 15px 20px 15px 0;
+ position: relative;
+ font-size: 24px;
+ border-bottom: 1px solid #cccccc;
+}
+
+#tasks .item.done span {
+ color: #777777;
+ text-decoration: line-through;
+}
+
+#tasks .item .destroy {
+ position: absolute;
+ right: 10px;
+ top: 16px;
+ display: none;
+ cursor: pointer;
+ width: 20px;
+ height: 20px;
+ background: url(../img/destroy.png) no-repeat center center;
+}
+
+#tasks .item:hover .destroy {
+ display: block;
+}
+
+#tasks .item .edit { display: none; }
+#tasks .item.editing .edit { display: block; }
+#tasks .item.editing .view { display: none; }
+#tasks .item .original { clear:both; font-size:11px;}
+#tasks .item .clicks { font-size: 10px; display: none;position: absolute; right: 35px; top: 16px; cursor:pointer; background-color:#ffcccc;}
+#tasks .item:hover .clicks{
+ display:block;
+}
+
+#tasks footer {
+ display: block;
+ margin: 20px -20px -20px -20px;
+ overflow: hidden;
+
+ color: #555555;
+ background: #f4fce8;
+ border-top: 1px solid #ededed;
+ padding: 0 20px;
+ line-height: 36px;
+
+ -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;
+}
+
+#tasks .clear {
+ display: block;
+ float: right;
+ line-height: 20px;
+ text-decoration: none;
+
+ background: rgba(0, 0, 0, 0.1);
+ color: #555555;
+ font-size: 11px;
+ margin-top: 8px;
+ padding: 0 10px 1px;
+
+ -moz-border-radius: 12px;
+ -webkit-border-radius: 12px;
+ -o-border-radius: 12px;
+ border-radius: 12px;
+
+ -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;
+
+ cursor: pointer;
+}
+
+#tasks .clear:hover {
+ 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;
+}
+
+#tasks .clear:active {
+ position: relative;
+ top: 1px;
+}
+
+#tasks .count span {
+ font-weight: bold;
+}
+
+.bitly-summary span{
+ clear:both;
+ display:block;
+}
View
0 img/destroy.png → _archive/img/destroy.png
File renamed without changes
View
242 _archive/index.html
@@ -0,0 +1,242 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>ShorterUrl</title>
+ <script src="js/jquery-1.5.2.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/spine.tmpl.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/spine.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/spine.route.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/json.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/store.min.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/jquery.bitlydfd.0.1.js" type="text/javascript"></script>
+ <script src="js/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
+ <script src="js/spine.controller.manager.js" type="text/javascript"></script>
+
+ <link rel="stylesheet" type="text/css" href="css/application.css"/>
+
+<script type="text/javascript">
+// Create the Task model.
+var Task = Spine.Model.setup("Task", ["name", "done", "original"]);
+
+Spine.Route.setup();
+
+Task.extend(Spine.Model.Local);
+
+Task.extend({
+ active: function(){
+ return(this.select(function(item){ return !item.done; }));
+ },
+
+ done: function(){
+ return(this.select(function(item){ return !!item.done; }));
+ },
+
+ destroyDone: function(){
+ this.done().forEach(function(rec){ rec.destroy() });
+ }
+});
+
+jQuery(function($){
+
+
+ window.Tasks = Spine.Controller.create({
+ tag: "li",
+
+ proxied: ["render", "remove"],
+
+ events: {
+ "change input[type=checkbox]": "toggle",
+ "click .destroy": "destroy",
+ "dblclick .view": "edit",
+ "keypress input[type=text]": "blurOnEnter",
+ "blur input[type=text]": "close",
+ "click .clicks": "viewclicks"
+ },
+
+ elements: {
+ "input[type=text]": "input",
+ ".item": "wrapper"
+ },
+
+ init: function(){
+ this.item.bind("update", this.render);
+ this.item.bind("destroy", this.remove);
+ },
+
+
+ render: function(){
+ var elements = $("#taskTemplate").tmpl(this.item);
+ this.el.html(elements);
+ this.refreshElements();
+ return this;
+ },
+
+ toggle: function(){
+ this.item.done = !this.item.done;
+ this.item.save();
+ },
+
+ destroy: function(){
+ this.item.destroy();
+ },
+
+ viewclicks: function(){
+ $('.items').hide();
+ var elements = $("#clicksTemplate").tmpl(this.item);
+ $('#tasks').append(elements);
+ $('#tasks .clicksView').bitlyDFD({utility:'clicks', shortUrl:this.item.name});
+ this.refreshElements();
+
+ return this;
+ },
+
+ edit: function(){
+ this.wrapper.addClass("editing");
+ this.input.focus();
+ },
+
+ blurOnEnter: function(e) {
+ if (e.keyCode == 13) e.target.blur();
+ },
+
+ close: function(){
+ this.wrapper.removeClass("editing");
+ this.item.updateAttributes({name: this.input.val()});
+ },
+
+ remove: function(){
+ this.el.remove();
+ }
+ });
+
+ window.TaskApp = Spine.Controller.create({
+ el: $("#tasks"),
+
+ proxied: ["addOne", "addAll", "renderCount"],
+
+ events: {
+ "submit form": "create",
+ "click .clear": "clear"
+ },
+
+ elements: {
+ ".items": "items",
+ ".countVal": "count",
+ ".clear": "clear",
+ "form input": "input"
+ },
+
+ init: function(){
+ Task.bind("create", this.addOne);
+ Task.bind("refresh", this.addAll);
+ Task.bind("refresh change", this.renderCount);
+ Task.fetch();
+
+ this.routes({
+ "/ui/clicks/:id": function(id){
+ //console.log("/ui/clicks/", id)
+
+ },
+ "" : function(){
+ console.log('catchall');
+ $('.items').show();
+ $('.clicksView').remove();
+ }
+ });
+
+ },
+
+ addOne: function(task) {
+ var view = Tasks.init({item: task});
+ this.items.append(view.render().el);
+
+ },
+
+ addAll: function() {
+ Task.each(this.addOne);
+ },
+
+ create: function(){
+ var q = this.input;
+ q.bitlyDFD({utility:'shorten',
+ longUrl:this.input.val(),
+ callback:function(s,l){
+ Task.create({name: s, original:l}
+ );
+ }
+ });
+
+ this.input.val("");
+ return false;
+ },
+
+ clear: function(){
+ Task.destroyDone();
+ },
+
+ renderCount: function(){
+ var active = Task.active().length;
+ this.count.text(active);
+ var inactive = Task.done().length;
+ this.clear[inactive ? "show" : "hide"]();
+ }
+ });
+
+ window.App = TaskApp.init();
+
+
+});
+
+</script>
+</head>
+<body>
+
+
+ <script type="text/x-jquery-tmpl" id="taskTemplate">
+ <div class="item {{if done}}done{{/if}}">
+ <div class="view" title="Double click to edit...">
+ <input type="checkbox" {{if done}}checked="checked"{{/if}}>
+
+ <span>${name}</span>
+ <span class="original">${original}</span>
+ <a class="clicks" href="#/ui/clicks/${id}">Click Statistics</a>
+ <a class="destroy"></a>
+ </div>
+
+ <div class="edit">
+ <input type="text" value="${name}">
+ </div>
+ </div>
+ </script>
+
+ <script type="text/x-jquery-tmpl" id="clicksTemplate">
+ <div class="clicksView">
+ <h2>Click Statistics</h2>
+ <span><strong>Short URL: </strong> <a href="${name}">${name}</a> (<a href="#">Edit Mode</a>)</span><br>
+ <span><strong>Original URL: </strong> <a href="${original}">${original}</a></span>
+ <div class="clicksInfo"></div>
+
+ </div>
+ </script>
+
+
+
+
+ <div id="views">
+ <div id="tasks">
+ <h1>ShorterUrl</h1>
+
+ <form>
+ <input class="inputUrl" type="text" placeholder="Enter the URL you would like shortened">
+ </form>
+
+ <div class="items"></div>
+ <footer>
+ <a class="clear">Clear entries</a>
+ </footer>
+ </div>
+ </div>
+
+</body>
+</html>
View
0 js/jquery-1.5.2.min.js → _archive/js/jquery-1.5.2.min.js
File renamed without changes.
View
0 js/jquery.bitlydfd.0.1.js → _archive/js/jquery.bitlydfd.0.1.js
File renamed without changes.
View
0 js/jquery.tmpl.js → _archive/js/jquery.tmpl.js
File renamed without changes.
View
0 js/json.js → _archive/js/json.js
File renamed without changes.
View
0 js/spine.controller.manager.js → _archive/js/spine.controller.manager.js
File renamed without changes.
View
0 js/spine.list.js → _archive/js/spine.list.js
File renamed without changes.
View
0 js/spine.min.js → _archive/js/spine.min.js
File renamed without changes.
View
0 js/spine.model.ajax.js → _archive/js/spine.model.ajax.js
File renamed without changes.
View
0 js/spine.model.local.js → _archive/js/spine.model.local.js
File renamed without changes.
View
0 js/spine.route.js → _archive/js/spine.route.js
File renamed without changes.
View
0 js/spine.tmpl.js → _archive/js/spine.tmpl.js
File renamed without changes.
View
0 js/store.min.js → _archive/js/store.min.js
File renamed without changes.
View
146 app/application.js
@@ -0,0 +1,146 @@
+var exports = this;
+
+jQuery(function($){
+
+ $.fn.toggleDisplay = function(bool){
+ if ( typeof bool == "undefined" ) {
+ bool = !$(this).filter(":first:visible")[0];
+ }
+ return $(this)[bool ? "show" : "hide"]();
+ };
+
+ exports.Urls = Spine.Controller.create({
+ events: {
+ "click .destroy": "destroy",
+ "click .toggleStats": "toggleStats"
+ },
+
+ proxied: ["render", "remove"],
+
+ template: function(items){
+ return $("#urlTemplate").tmpl(items);
+ },
+
+ init: function(){
+ this.item.bind("update", this.render);
+ this.item.bind("destroy", this.remove);
+ },
+
+ render: function(){
+ this.el.html(this.template(this.item.reload()));
+ return this;
+ },
+
+ toggleStats: function(){
+ this.navigate("/stats", this.item.id, true);
+ },
+
+ remove: function(){
+ this.el.remove();
+ },
+
+ destroy: function(){
+ this.item.destroy();
+ }
+ });
+
+ exports.UrlsList = Spine.Controller.create({
+ elements: {
+ ".items": "items",
+ "form": "form",
+ "input": "input"
+ },
+
+ events: {
+ "submit form": "create",
+ },
+
+ proxied: ["render", "addAll", "addOne"],
+
+ init: function(){
+ Url.bind("create", this.addOne);
+ Url.bind("refresh", this.addAll);
+ },
+
+ addOne: function(url){
+ var view = Urls.init({item: url});
+ this.items.append(view.render().el);
+ },
+
+ addAll: function(){
+ Url.each(this.addOne);
+ },
+
+ create: function(e){
+ e.preventDefault();
+ var value = this.input.val();
+
+ if (value)
+ Url.create({long_url: value});
+
+ this.input.val("");
+ this.input.focus();
+ }
+ });
+
+ exports.Stats = Spine.Controller.create({
+ events: {
+ "click .back": "back"
+ },
+
+ proxied: ["change", "render"],
+
+ init: function(){
+ Url.bind("update", this.render);
+ },
+
+ template: function(items){
+ return $("#statsTemplate").tmpl(items);
+ },
+
+ render: function(){
+ if ( !this.item ) return;
+ this.el.html(this.template(this.item.reload()));
+ },
+
+ change: function(item){
+ this.item = item;
+ this.navigate("/stats", item.id);
+ this.item.fetchStats();
+ this.render();
+ this.active();
+ },
+
+ back: function(){
+ this.navigate("/list", true);
+ }
+ })
+
+ exports.UrlApp = Spine.Controller.create({
+ el: $("body"),
+
+ elements: {
+ "#urls": "urlsEl",
+ "#stats": "statsEl"
+ },
+
+ init: function(){
+ this.list = UrlsList.init({el: this.urlsEl});
+ this.stats = Stats.init({el: this.statsEl});
+
+ this.manager = Spine.Controller.Manager.init();
+ this.manager.addAll(this.list, this.stats);
+
+ this.routes({
+ "": function(){ this.list.active() },
+ "/list": function(){ this.list.active() },
+ "/stats/:id": function(id){ this.stats.change(Url.find(id)) }
+ });
+
+ Url.fetch();
+ Spine.Route.setup();
+ }
+ });
+
+ exports.App = UrlApp.init();
+});
View
31 app/models/url.js
@@ -0,0 +1,31 @@
+var Url = Spine.Model.setup("Url", ["short_url", "long_url", "stats"]);
+
+Url.extend(Spine.Model.Local);
+
+Url.include({
+ validate: function(){
+ if ( !this.long_url )
+ return "long_url required"
+
+ if ( !this.long_url.match(/:\/\//))
+ this.long_url = "http://" + this.long_url
+ },
+
+ fetchUrl: function(){
+ if ( !this.short_url )
+ $.bitly(this.long_url, this.proxy(function(result){
+ this.updateAttributes({short_url: result});
+ }));
+ },
+
+ fetchStats: function(){
+ if ( !this.short_url ) return;
+ $.bitly.stats(this.short_url, this.proxy(function(result){
+ this.updateAttributes({stats: result});
+ }));
+ }
+});
+
+Url.bind("create", function(rec){
+ rec.fetchUrl();
+});
View
122 css/application.css
@@ -11,6 +11,20 @@ body {
color: #333333;
}
+a {
+ color: #261A3B;
+ text-decoration: underline;
+ cursor: pointer;
+}
+
+
+h1 {
+ font-size: 36px;
+ font-weight: bold;
+ text-align: center;
+ padding: 0 0 10px 0;
+}
+
#views {
width: 520px;
margin: 0 auto 40px auto;
@@ -27,18 +41,15 @@ body {
border-radius: 0 0 5px 5px;
}
-#tasks {
- padding: 20px;
+#views > *:not(.active) {
+ display: none;
}
-#tasks h1 {
- font-size: 36px;
- font-weight: bold;
- text-align: center;
- padding: 0 0 10px 0;
+#urls, #stats {
+ padding: 20px;
}
-#tasks input[type="text"] {
+#urls input[type="text"] {
width: 466px;
font-size: 24px;
font-family: inherit;
@@ -54,111 +65,52 @@ body {
box-shadow: rgba(0, 0, 0, 0.2) 0 1px 2px 0 inset;
}
-#tasks input::-webkit-input-placeholder {
+#urls input::-webkit-input-placeholder {
font-style: italic;
}
-#tasks .items {
+#urls .items {
margin: 10px 0;
list-style: none;
}
-#tasks .item {
+#urls .item {
padding: 15px 20px 15px 0;
position: relative;
font-size: 24px;
border-bottom: 1px solid #cccccc;
}
-#tasks .item.done span {
- color: #777777;
- text-decoration: line-through;
-}
-
-#tasks .item .destroy {
+#urls .item .destroy {
position: absolute;
right: 10px;
top: 16px;
display: none;
cursor: pointer;
width: 20px;
height: 20px;
- background: url(../img/destroy.png) no-repeat center center;
-}
-
-#tasks .item:hover .destroy {
- display: block;
-}
-
-#tasks .item .edit { display: none; }
-#tasks .item.editing .edit { display: block; }
-#tasks .item.editing .view { display: none; }
-#tasks .item .original { clear:both; font-size:11px;}
-#tasks .item .clicks { font-size: 10px; display: none;position: absolute; right: 35px; top: 16px; cursor:pointer; background-color:#ffcccc;}
-#tasks .item:hover .clicks{
- display:block;
+ background: url(../images/destroy.png) no-repeat center center;
}
-#tasks footer {
+#urls .item:hover .destroy {
display: block;
- margin: 20px -20px -20px -20px;
- overflow: hidden;
-
- color: #555555;
- background: #f4fce8;
- border-top: 1px solid #ededed;
- padding: 0 20px;
- line-height: 36px;
-
- -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;
}
-#tasks .clear {
- display: block;
- float: right;
- line-height: 20px;
- text-decoration: none;
-
- background: rgba(0, 0, 0, 0.1);
- color: #555555;
- font-size: 11px;
- margin-top: 8px;
- padding: 0 10px 1px;
-
- -moz-border-radius: 12px;
- -webkit-border-radius: 12px;
- -o-border-radius: 12px;
- border-radius: 12px;
-
- -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;
-
+#urls .item .toggleStats {
+ position: absolute;
+ right: 40px;
+ top: 16px;
+ display: none;
cursor: pointer;
+ width: 20px;
+ height: 20px;
+ background: url(../images/destroy.png) no-repeat center center;
}
-#tasks .clear:hover {
- 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;
-}
-
-#tasks .clear:active {
- position: relative;
- top: 1px;
-}
-
-#tasks .count span {
- font-weight: bold;
+#urls .item:hover .toggleStats {
+ display: block;
}
-.bitly-summary span{
- clear:both;
- display:block;
+#urls .item .stats {
+ display: none;
}
View
BIN images/destroy.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
297 index.html
@@ -1,242 +1,79 @@
<!DOCTYPE html>
<html>
-<head>
- <title>ShorterUrl</title>
- <script src="js/jquery-1.5.2.min.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/spine.tmpl.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/spine.min.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/spine.route.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/json.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/store.min.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/jquery.bitlydfd.0.1.js" type="text/javascript"></script>
- <script src="js/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
- <script src="js/spine.controller.manager.js" type="text/javascript"></script>
+<head>
+ <link rel="stylesheet" href="css/application.css" type="text/css" charset="utf-8">
- <link rel="stylesheet" type="text/css" href="css/application.css"/>
+ <script src="lib/json2.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/jquery.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/jquery.tmpl.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/jquery.bitly.js" type="text/javascript" charset="utf-8"></script>
-<script type="text/javascript">
-// Create the Task model.
-var Task = Spine.Model.setup("Task", ["name", "done", "original"]);
+ <script src="lib/spine.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/spine.model.local.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/spine.controller.manager.js" type="text/javascript" charset="utf-8"></script>
+ <script src="lib/spine.route.js" type="text/javascript" charset="utf-8"></script>
-Spine.Route.setup();
-
-Task.extend(Spine.Model.Local);
-
-Task.extend({
- active: function(){
- return(this.select(function(item){ return !item.done; }));
- },
-
- done: function(){
- return(this.select(function(item){ return !!item.done; }));
- },
+ <script src="app/models/url.js" type="text/javascript" charset="utf-8"></script>
+ <script src="app/application.js" type="text/javascript" charset="utf-8"></script>
- destroyDone: function(){
- this.done().forEach(function(rec){ rec.destroy() });
- }
-});
-
-jQuery(function($){
-
-
- window.Tasks = Spine.Controller.create({
- tag: "li",
-
- proxied: ["render", "remove"],
-
- events: {
- "change input[type=checkbox]": "toggle",
- "click .destroy": "destroy",
- "dblclick .view": "edit",
- "keypress input[type=text]": "blurOnEnter",
- "blur input[type=text]": "close",
- "click .clicks": "viewclicks"
- },
-
- elements: {
- "input[type=text]": "input",
- ".item": "wrapper"
- },
-
- init: function(){
- this.item.bind("update", this.render);
- this.item.bind("destroy", this.remove);
- },
-
-
- render: function(){
- var elements = $("#taskTemplate").tmpl(this.item);
- this.el.html(elements);
- this.refreshElements();
- return this;
- },
-
- toggle: function(){
- this.item.done = !this.item.done;
- this.item.save();
- },
-
- destroy: function(){
- this.item.destroy();
- },
-
- viewclicks: function(){
- $('.items').hide();
- var elements = $("#clicksTemplate").tmpl(this.item);
- $('#tasks').append(elements);
- $('#tasks .clicksView').bitlyDFD({utility:'clicks', shortUrl:this.item.name});
- this.refreshElements();
-
- return this;
- },
-
- edit: function(){
- this.wrapper.addClass("editing");
- this.input.focus();
- },
-
- blurOnEnter: function(e) {
- if (e.keyCode == 13) e.target.blur();
- },
-
- close: function(){
- this.wrapper.removeClass("editing");
- this.item.updateAttributes({name: this.input.val()});
- },
-
- remove: function(){
- this.el.remove();
- }
- });
+ <script type="text/x-jquery-tmpl" id="urlTemplate">
+ <div class="item">
+ <div class="show">
+ <span class="short">
+ ${long_url}
+ </span>
+
+ <span class="long">
+ {{if short_url}}
+ <a href="${short_url}">${short_url}</a>
+ {{else}}
+ Generating...
+ {{/if}}
+ </span>
+
+ <a class="toggleStats"></a>
+ <a class="destroy"></a>
+ </div>
+ </div>
+ </script>
- window.TaskApp = Spine.Controller.create({
- el: $("#tasks"),
-
- proxied: ["addOne", "addAll", "renderCount"],
-
- events: {
- "submit form": "create",
- "click .clear": "clear"
- },
-
- elements: {
- ".items": "items",
- ".countVal": "count",
- ".clear": "clear",
- "form input": "input"
- },
-
- init: function(){
- Task.bind("create", this.addOne);
- Task.bind("refresh", this.addAll);
- Task.bind("refresh change", this.renderCount);
- Task.fetch();
-
- this.routes({
- "/ui/clicks/:id": function(id){
- //console.log("/ui/clicks/", id)
-
- },
- "" : function(){
- console.log('catchall');
- $('.items').show();
- $('.clicksView').remove();
- }
- });
-
- },
+ <script type="text/x-jquery-tmpl" id="statsTemplate">
+ <div class="stats">
+ <a class="back">Back</a>
- addOne: function(task) {
- var view = Tasks.init({item: task});
- this.items.append(view.render().el);
+ <h1>${long_url}</h1>
- },
-
- addAll: function() {
- Task.each(this.addOne);
- },
-
- create: function(){
- var q = this.input;
- q.bitlyDFD({utility:'shorten',
- longUrl:this.input.val(),
- callback:function(s,l){
- Task.create({name: s, original:l}
- );
- }
- });
-
- this.input.val("");
- return false;
- },
-
- clear: function(){
- Task.destroyDone();
- },
-
- renderCount: function(){
- var active = Task.active().length;
- this.count.text(active);
- var inactive = Task.done().length;
- this.clear[inactive ? "show" : "hide"]();
- }
- });
-
- window.App = TaskApp.init();
-
-
-});
-
-</script>
+ <h2 class="short">
+ {{if short_url}}
+ <a href="${short_url}">${short_url}</a>
+ {{else}}
+ Generating...
+ {{/if}}
+ </h2>
+
+ {{if stats}}
+ <p>Global clicks: ${stats.global_clicks}</p>
+ <p>User clicks: ${stats.user_clicks}</p>
+ {{else}}
+ Fetching...
+ {{/if}}
+ </div>
+ </script>
</head>
<body>
-
-
- <script type="text/x-jquery-tmpl" id="taskTemplate">
- <div class="item {{if done}}done{{/if}}">
- <div class="view" title="Double click to edit...">
- <input type="checkbox" {{if done}}checked="checked"{{/if}}>
-
- <span>${name}</span>
- <span class="original">${original}</span>
- <a class="clicks" href="#/ui/clicks/${id}">Click Statistics</a>
- <a class="destroy"></a>
- </div>
-
- <div class="edit">
- <input type="text" value="${name}">
- </div>
- </div>
- </script>
-
- <script type="text/x-jquery-tmpl" id="clicksTemplate">
- <div class="clicksView">
- <h2>Click Statistics</h2>
- <span><strong>Short URL: </strong> <a href="${name}">${name}</a> (<a href="#">Edit Mode</a>)</span><br>
- <span><strong>Original URL: </strong> <a href="${original}">${original}</a></span>
- <div class="clicksInfo"></div>
-
- </div>
- </script>
-
-
-
-
- <div id="views">
- <div id="tasks">
- <h1>ShorterUrl</h1>
-
- <form>
- <input class="inputUrl" type="text" placeholder="Enter the URL you would like shortened">
- </form>
-
- <div class="items"></div>
- <footer>
- <a class="clear">Clear entries</a>
- </footer>
- </div>
- </div>
-
+ <div id="views">
+ <div id="urls">
+ <h1>Bitly Generator</h1>
+
+ <form>
+ <input type="text" placeholder="Enter a URL">
+ </form>
+
+ <div class="items"></div>
+ </div>
+
+ <div id="stats">
+ </div>
+ </div>
</body>
-</html>
+</html>
View
32 lib/jquery.bitly.js
@@ -0,0 +1,32 @@
+(function($){
+
+ var defaults = {
+ version: "3.0",
+ login: "legacye",
+ apiKey: "R_32f60d09cccde1f266bcba8c242bfb5a",
+ history: "0",
+ format: "json"
+ };
+
+ $.bitly = function( url, callback, params ) {
+ if ( !url || !callback ) throw("url and callback required");
+
+ var params = $.extend( defaults, params );
+ params.longUrl = url;
+
+ return $.getJSON("http://api.bit.ly/shorten?callback=?", params, function(data, status, xhr){
+ callback(data.results[params.longUrl].shortUrl, data.results[params.longUrl], data);
+ });
+ };
+
+ $.bitly.stats = function( url, callback, params ) {
+ if ( !url || !callback ) throw("url and callback required");
+
+ var params = $.extend( defaults, params );
+ params.shortUrl = url;
+
+ return $.getJSON("http://api.bitly.com/v3/clicks?callback=?", params, function(data, status, xhr){
+ callback(data.data.clicks[0], data);
+ });
+ };
+})(jQuery);
View
8,316 lib/jquery.js
8,316 additions, 0 deletions not shown because the diff is too large. Please use a local Git client to view these changes.
View
503 lib/jquery.tmpl.js
@@ -0,0 +1,503 @@
+/*!
+ * jQuery Templates Plugin
+ * http://github.com/jquery/jquery-tmpl
+ *
+ * Copyright Software Freedom Conservancy, Inc.
+ * Dual licensed under the MIT or GPL Version 2 licenses.
+ * http://jquery.org/license
+ */
+(function( jQuery, undefined ){
+ var oldManip = jQuery.fn.domManip, tmplItmAtt = "_tmplitem", htmlExpr = /^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,
+ newTmplItems = {}, wrappedItems = {}, appendToTmplItems, topTmplItem = { key: 0, data: {} }, itemKey = 0, cloneIndex = 0, stack = [];
+
+ function newTmplItem( options, parentItem, fn, data ) {
+ // Returns a template item data structure for a new rendered instance of a template (a 'template item').
+ // The content field is a hierarchical array of strings and nested items (to be
+ // removed and replaced by nodes field of dom elements, once inserted in DOM).
+ var newItem = {
+ data: data || (parentItem ? parentItem.data : {}),
+ _wrap: parentItem ? parentItem._wrap : null,
+ tmpl: null,
+ parent: parentItem || null,
+ nodes: [],
+ calls: tiCalls,
+ nest: tiNest,
+ wrap: tiWrap,
+ html: tiHtml,
+ update: tiUpdate
+ };
+ if ( options ) {
+ jQuery.extend( newItem, options, { nodes: [], parent: parentItem } );
+ }
+ if ( fn ) {
+ // Build the hierarchical content to be used during insertion into DOM
+ newItem.tmpl = fn;
+ newItem._ctnt = newItem._ctnt || newItem.tmpl( jQuery, newItem );
+ newItem.key = ++itemKey;
+ // Keep track of new template item, until it is stored as jQuery Data on DOM element
+ (stack.length ? wrappedItems : newTmplItems)[itemKey] = newItem;
+ }
+ return newItem;
+ }
+
+ // Override appendTo etc., in order to provide support for targeting multiple elements. (This code would disappear if integrated in jquery core).
+ jQuery.each({
+ appendTo: "append",
+ prependTo: "prepend",
+ insertBefore: "before",
+ insertAfter: "after",
+ replaceAll: "replaceWith"
+ }, function( name, original ) {
+ jQuery.fn[ name ] = function( selector ) {
+ var ret = [], insert = jQuery( selector ), elems, i, l, tmplItems,
+ parent = this.length === 1 && this[0].parentNode;
+
+ appendToTmplItems = newTmplItems || {};
+ if ( parent && parent.nodeType === 11 && parent.childNodes.length === 1 && insert.length === 1 ) {
+ insert[ original ]( this[0] );
+ ret = this;
+ } else {
+ for ( i = 0, l = insert.length; i < l; i++ ) {
+ cloneIndex = i;
+ elems = (i > 0 ? this.clone(true) : this).get();
+ jQuery.fn[ original ].apply( jQuery(insert[i]), elems );
+ ret = ret.concat( elems );
+ }
+ cloneIndex = 0;
+ ret = this.pushStack( ret, name, insert.selector );
+ }
+ tmplItems = appendToTmplItems;
+ appendToTmplItems = null;
+ jQuery.tmpl.complete( tmplItems );
+ return ret;
+ };
+ });
+
+ jQuery.fn.extend({
+ // Use first wrapped element as template markup.
+ // Return wrapped set of template items, obtained by rendering template against data.
+ tmpl: function( data, options, parentItem ) {
+ return jQuery.tmpl( this[0], data, options, parentItem );
+ },
+
+ // Find which rendered template item the first wrapped DOM element belongs to
+ tmplItem: function() {
+ return jQuery.tmplItem( this[0] );
+ },
+
+ tmplElement: function() {
+ return jQuery.tmplElement( this[0] );
+ },
+
+ // Consider the first wrapped element as a template declaration, and get the compiled template or store it as a named template.
+ template: function( name ) {
+ return jQuery.template( name, this[0] );
+ },
+
+ domManip: function( args, table, callback, options ) {
+ // This appears to be a bug in the appendTo, etc. implementation
+ // it should be doing .call() instead of .apply(). See #6227
+ if ( args[0] && args[0].nodeType ) {
+ var dmArgs = jQuery.makeArray( arguments ), argsLength = args.length, i = 0, tmplItem;
+ while ( i < argsLength && !(tmplItem = jQuery.data( args[i++], "tmplItem" ))) {}
+ if ( argsLength > 1 ) {
+ dmArgs[0] = [jQuery.makeArray( args )];
+ }
+ if ( tmplItem && cloneIndex ) {
+ dmArgs[2] = function( fragClone ) {
+ // Handler called by oldManip when rendered template has been inserted into DOM.
+ jQuery.tmpl.afterManip( this, fragClone, callback );
+ };
+ }
+ oldManip.apply( this, dmArgs );
+ } else {
+ oldManip.apply( this, arguments );
+ }
+ cloneIndex = 0;
+ if ( !appendToTmplItems ) {
+ jQuery.tmpl.complete( newTmplItems );
+ }
+ return this;
+ }
+ });
+
+ jQuery.extend({
+ // Return wrapped set of template items, obtained by rendering template against data.
+ tmpl: function( tmpl, data, options, parentItem ) {
+ var ret, topLevel = !parentItem;
+ if ( topLevel ) {
+ // This is a top-level tmpl call (not from a nested template using {{tmpl}})
+ parentItem = topTmplItem;
+ if ( typeof tmpl != "function" )
+ tmpl = jQuery.template[tmpl] || jQuery.template( null, tmpl );
+ wrappedItems = {}; // Any wrapped items will be rebuilt, since this is top level
+ } else if ( !tmpl ) {
+ // The template item is already associated with DOM - this is a refresh.
+ // Re-evaluate rendered template for the parentItem
+ tmpl = parentItem.tmpl;
+ newTmplItems[parentItem.key] = parentItem;
+ parentItem.nodes = [];
+ if ( parentItem.wrapped ) {
+ updateWrapped( parentItem, parentItem.wrapped );
+ }
+ // Rebuild, without creating a new template item
+ return jQuery( build( parentItem, null, parentItem.tmpl( jQuery, parentItem ) ));
+ }
+ if ( !tmpl ) {
+ return []; // Could throw...
+ }
+ if ( typeof data === "function" ) {
+ data = data.call( parentItem || {} );
+ }
+ if ( options && options.wrapped ) {
+ updateWrapped( options, options.wrapped );
+ }
+ ret = jQuery.isArray( data ) ?
+ jQuery.map( data, function( dataItem ) {
+ return dataItem ? newTmplItem( options, parentItem, tmpl, dataItem ) : null;
+ }) :
+ [ newTmplItem( options, parentItem, tmpl, data ) ];
+ return topLevel ? jQuery( build( parentItem, null, ret ) ) : ret;
+ },
+
+ // Return rendered template item for an element.
+ tmplItem: function( elem ) {
+ var tmplItem;
+ if ( elem instanceof jQuery ) {
+ elem = elem[0];
+ }
+ while ( elem && elem.nodeType === 1 && !(tmplItem = jQuery.data( elem, "tmplItem" )) && (elem = elem.parentNode) ) {}
+ return tmplItem || topTmplItem;
+ },
+
+ tmplElement: function( elem ) {
+ var tmplItem;
+ if ( elem instanceof jQuery ) {
+ elem = elem[0];
+ }
+ while ( elem && elem.nodeType === 1 && !jQuery.data( elem, "tmplItem" ) && (elem = elem.parentNode) ) {}
+ return elem;
+ },
+
+ // Set:
+ // Use $.template( name, tmpl ) to cache a named template,
+ // where tmpl is a template string, a script element or a jQuery instance wrapping a script element, etc.
+ // Use $( "selector" ).template( name ) to provide access by name to a script block template declaration.
+
+ // Get:
+ // Use $.template( name ) to access a cached template.
+ // Also $( selectorToScriptBlock ).template(), or $.template( null, templateString )
+ // will return the compiled template, without adding a name reference.
+ // If templateString includes at least one HTML tag, $.template( templateString ) is equivalent
+ // to $.template( null, templateString )
+ template: function( name, tmpl ) {
+ if (tmpl) {
+ // Compile template and associate with name
+ if ( typeof tmpl === "string" ) {
+ // This is an HTML string being passed directly in.
+ tmpl = buildTmplFn( tmpl )
+ } else if ( tmpl instanceof jQuery ) {
+ tmpl = tmpl[0] || {};
+ }
+ if ( tmpl.nodeType ) {
+ // If this is a template block, use cached copy, or generate tmpl function and cache.
+ tmpl = jQuery.data( tmpl, "tmpl" ) || jQuery.data( tmpl, "tmpl", buildTmplFn( tmpl.innerHTML ));
+ }
+ return typeof name === "string" ? (jQuery.template[name] = tmpl) : tmpl;
+ }
+ // Return named compiled template
+ return name ? (typeof name !== "string" ? jQuery.template( null, name ):
+ (jQuery.template[name] ||
+ // If not in map, treat as a selector. (If integrated with core, use quickExpr.exec)
+ jQuery.template( null, htmlExpr.test( name ) ? name : jQuery( name )))) : null;
+ },
+
+ encode: function( text ) {
+ // Do HTML encoding replacing < > & and ' and " by corresponding entities.
+ return ("" + text).split("<").join("&lt;").split(">").join("&gt;").split('"').join("&#34;").split("'").join("&#39;");
+ }
+ });
+
+ jQuery.extend( jQuery.tmpl, {
+ tag: {
+ "tmpl": {
+ _default: { $2: "null" },
+ open: "if($notnull_1){_=_.concat($item.nest($1,$2));}"
+ // tmpl target parameter can be of type function, so use $1, not $1a (so not auto detection of functions)
+ // This means that {{tmpl foo}} treats foo as a template (which IS a function).
+ // Explicit parens can be used if foo is a function that returns a template: {{tmpl foo()}}.
+ },
+ "wrap": {
+ _default: { $2: "null" },
+ open: "$item.calls(_,$1,$2);_=[];",
+ close: "call=$item.calls();_=call._.concat($item.wrap(call,_));"
+ },
+ "each": {
+ _default: { $2: "$index, $value" },
+ open: "if($notnull_1){$.each($1a,function($2){with(this){",
+ close: "}});}"
+ },
+ "if": {
+ open: "if(($notnull_1) && $1a){",
+ close: "}"
+ },
+ "else": {
+ _default: { $1: "true" },
+ open: "}else if(($notnull_1) && $1a){"
+ },
+ "html": {
+ // Unecoded expression evaluation.
+ open: "if($notnull_1){_.push($1a);}"
+ },
+ "=": {
+ // Encoded expression evaluation. Abbreviated form is ${}.
+ _default: { $1: "$data" },
+ open: "if($notnull_1){_.push($.encode($1a));}"
+ },
+ "!": {
+ // Comment tag. Skipped by parser
+ open: ""
+ }
+ },
+
+ // This stub can be overridden, e.g. in jquery.tmplPlus for providing rendered events
+ complete: function( items ) {
+ newTmplItems = {};
+ },
+
+ // Call this from code which overrides domManip, or equivalent
+ // Manage cloning/storing template items etc.
+ afterManip: function afterManip( elem, fragClone, callback ) {
+ // Provides cloned fragment ready for fixup prior to and after insertion into DOM
+ var content = fragClone.nodeType === 11 ?
+ jQuery.makeArray(fragClone.childNodes) :
+ fragClone.nodeType === 1 ? [fragClone] : [];
+
+ // Return fragment to original caller (e.g. append) for DOM insertion
+ callback.call( elem, fragClone );
+
+ // Fragment has been inserted:- Add inserted nodes to tmplItem data structure. Replace inserted element annotations by jQuery.data.
+ storeTmplItems( content );
+ cloneIndex++;
+ }
+ });
+
+ //========================== Private helper functions, used by code above ==========================
+
+ function build( tmplItem, nested, content ) {
+ // Convert hierarchical content into flat string array
+ // and finally return array of fragments ready for DOM insertion
+ var frag, ret = content ? jQuery.map( content, function( item ) {
+ return (typeof item === "string") ?
+ // Insert template item annotations, to be converted to jQuery.data( "tmplItem" ) when elems are inserted into DOM.
+ (tmplItem.key ? item.replace( /(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g, "$1 " + tmplItmAtt + "=\"" + tmplItem.key + "\" $2" ) : item) :
+ // This is a child template item. Build nested template.
+ build( item, tmplItem, item._ctnt );
+ }) :
+ // If content is not defined, insert tmplItem directly. Not a template item. May be a string, or a string array, e.g. from {{html $item.html()}}.
+ tmplItem;
+ if ( nested ) {
+ return ret;
+ }
+
+ // top-level template
+ ret = ret.join("");
+
+ // Support templates which have initial or final text nodes, or consist only of text
+ // Also support HTML entities within the HTML markup.
+ ret.replace( /^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/, function( all, before, middle, after) {
+ frag = jQuery( middle ).get();
+
+ storeTmplItems( frag );
+ if ( before ) {
+ frag = unencode( before ).concat(frag);
+ }
+ if ( after ) {
+ frag = frag.concat(unencode( after ));
+ }
+ });
+ return frag ? frag : unencode( ret );
+ }
+
+ function unencode( text ) {
+ // Use createElement, since createTextNode will not render HTML entities correctly
+ var el = document.createElement( "div" );
+ el.innerHTML = text;
+ return jQuery.makeArray(el.childNodes);
+ }
+
+ // Generate a reusable function that will serve to render a template against data
+ function buildTmplFn( markup ) {
+ return new Function("jQuery","$item",
+ "var $=jQuery,call,_=[],$data=$item.data;" +
+
+ // Introduce the data as local variables using with(){}
+ "with($data){_.push('" +
+
+ // Convert the template into pure JavaScript
+ jQuery.trim(markup)
+ .replace( /([\\'])/g, "\\$1" )
+ .replace( /[\r\t\n]/g, " " )
+ .replace( /\$\{([^\}]*)\}/g, "{{= $1}}" )
+ .replace( /\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,
+ function( all, slash, type, fnargs, target, parens, args ) {
+ var tag = jQuery.tmpl.tag[ type ], def, expr, exprAutoFnDetect;
+ if ( !tag ) {
+ throw "Template command not found: " + type;
+ }
+ def = tag._default || [];
+ if ( parens && !/\w$/.test(target)) {
+ target += parens;
+ parens = "";
+ }
+ if ( target ) {
+ target = unescape( target );
+ args = args ? ("," + unescape( args ) + ")") : (parens ? ")" : "");
+ // Support for target being things like a.toLowerCase();
+ // In that case don't call with template item as 'this' pointer. Just evaluate...
+ expr = parens ? (target.indexOf(".") > -1 ? target + parens : ("(" + target + ").call($data" + args)) : target;
+ exprAutoFnDetect = parens ? expr : "(typeof(" + target + ")==='function'?(" + target + ").call($item):(" + target + "))";
+ } else {
+ exprAutoFnDetect = expr = def.$1 || "null";
+ }
+ fnargs = unescape( fnargs );
+ return "');" +
+ tag[ slash ? "close" : "open" ]
+ .split( "$notnull_1" ).join( target ? "typeof(" + target + ")!=='undefined' && (" + target + ")!=null" : "true" )
+ .split( "$1a" ).join( exprAutoFnDetect )
+ .split( "$1" ).join( expr )
+ .split( "$2" ).join( fnargs ?
+ fnargs.replace( /\s*([^\(]+)\s*(\((.*?)\))?/g, function( all, name, parens, params ) {
+ params = params ? ("," + params + ")") : (parens ? ")" : "");
+ return params ? ("(" + name + ").call($item" + params) : all;
+ })
+ : (def.$2||"")
+ ) +
+ "_.push('";
+ }) +
+ "');}return _;"
+ );
+ }
+ function updateWrapped( options, wrapped ) {
+ // Build the wrapped content.
+ options._wrap = build( options, true,
+ // Suport imperative scenario in which options.wrapped can be set to a selector or an HTML string.
+ jQuery.isArray( wrapped ) ? wrapped : [htmlExpr.test( wrapped ) ? wrapped : jQuery( wrapped ).html()]
+ ).join("");
+ }
+
+ function unescape( args ) {
+ return args ? args.replace( /\\'/g, "'").replace(/\\\\/g, "\\" ) : null;
+ }
+ function outerHtml( elem ) {
+ var div = document.createElement("div");
+ div.appendChild( elem.cloneNode(true) );
+ return div.innerHTML;
+ }
+
+ // Store template items in jQuery.data(), ensuring a unique tmplItem data data structure for each rendered template instance.
+ function storeTmplItems( content ) {
+ var keySuffix = "_" + cloneIndex, elem, elems, newClonedItems = {}, i, l, m;
+ for ( i = 0, l = content.length; i < l; i++ ) {
+ if ( (elem = content[i]).nodeType !== 1 ) {
+ continue;
+ }
+ elems = elem.getElementsByTagName("*");
+ for ( m = elems.length - 1; m >= 0; m-- ) {
+ processItemKey( elems[m] );
+ }
+ processItemKey( elem );
+ }
+ function processItemKey( el ) {
+ var pntKey, pntNode = el, pntItem, tmplItem, key;
+ // Ensure that each rendered template inserted into the DOM has its own template item,
+ if ( (key = el.getAttribute( tmplItmAtt ))) {
+ while ( pntNode.parentNode && (pntNode = pntNode.parentNode).nodeType === 1 && !(pntKey = pntNode.getAttribute( tmplItmAtt ))) { }
+ if ( pntKey !== key ) {
+ // The next ancestor with a _tmplitem expando is on a different key than this one.
+ // So this is a top-level element within this template item
+ // Set pntNode to the key of the parentNode, or to 0 if pntNode.parentNode is null, or pntNode is a fragment.
+ pntNode = pntNode.parentNode ? (pntNode.nodeType === 11 ? 0 : (pntNode.getAttribute( tmplItmAtt ) || 0)) : 0;
+ if ( !(tmplItem = newTmplItems[key]) ) {
+ // The item is for wrapped content, and was copied from the temporary parent wrappedItem.
+ tmplItem = wrappedItems[key];
+ tmplItem = newTmplItem( tmplItem, newTmplItems[pntNode]||wrappedItems[pntNode], null, true );
+ tmplItem.key = ++itemKey;
+ newTmplItems[itemKey] = tmplItem;
+ }
+ if ( cloneIndex ) {
+ cloneTmplItem( key );
+ }
+ }
+ el.removeAttribute( tmplItmAtt );
+ } else if ( cloneIndex && (tmplItem = jQuery.data( el, "tmplItem" )) ) {
+ // This was a rendered element, cloned during append or appendTo etc.
+ // TmplItem stored in jQuery data has already been cloned in cloneCopyEvent. We must replace it with a fresh cloned tmplItem.
+ cloneTmplItem( tmplItem.key );
+ newTmplItems[tmplItem.key] = tmplItem;
+ pntNode = jQuery.data( el.parentNode, "tmplItem" );
+ pntNode = pntNode ? pntNode.key : 0;
+ }
+ if ( tmplItem ) {
+ pntItem = tmplItem;
+ // Find the template item of the parent element.
+ // (Using !=, not !==, since pntItem.key is number, and pntNode may be a string)
+ while ( pntItem && pntItem.key != pntNode ) {
+ // Add this element as a top-level node for this rendered template item, as well as for any
+ // ancestor items between this item and the item of its parent element
+ pntItem.nodes.push( el );
+ pntItem = pntItem.parent;
+ }
+ // Delete content built during rendering - reduce API surface area and memory use, and avoid exposing of stale data after rendering...
+ delete tmplItem._ctnt;
+ delete tmplItem._wrap;
+ // Store template item as jQuery data on the element
+ jQuery.data( el, "tmplItem", tmplItem );
+ }
+ function cloneTmplItem( key ) {
+ key = key + keySuffix;
+ tmplItem = newClonedItems[key] =
+ (newClonedItems[key] || newTmplItem( tmplItem, newTmplItems[tmplItem.parent.key + keySuffix] || tmplItem.parent, null, true ));
+ }
+ }
+ }
+
+ //---- Helper functions for template item ----
+
+ function tiCalls( content, tmpl, data, options ) {
+ if ( !content ) {
+ return stack.pop();
+ }
+ stack.push({ _: content, tmpl: tmpl, item:this, data: data, options: options });
+ }
+
+ function tiNest( tmpl, data, options ) {
+ // nested template, using {{tmpl}} tag
+ return jQuery.tmpl( jQuery.template( tmpl ), data, options, this );
+ }
+
+ function tiWrap( call, wrapped ) {
+ // nested template, using {{wrap}} tag
+ var options = call.options || {};
+ options.wrapped = wrapped;
+ // Apply the template, which may incorporate wrapped content,
+ return jQuery.tmpl( jQuery.template( call.tmpl ), call.data, options, call.item );
+ }
+
+ function tiHtml( filter, textOnly ) {
+ var wrapped = this._wrap;
+ return jQuery.map(
+ jQuery( jQuery.isArray( wrapped ) ? wrapped.join("") : wrapped ).filter( filter || "*" ),
+ function(e) {
+ return textOnly ?
+ e.innerText || e.textContent :
+ e.outerHTML || outerHtml(e);
+ });
+ }
+
+ function tiUpdate() {
+ var coll = this.nodes;
+ jQuery.tmpl( null, null, null, this).insertBefore( coll[0] );
+ jQuery( coll ).remove();
+ }
+})( jQuery );
View
481 lib/json2.js
@@ -0,0 +1,481 @@
+/*
+ http://www.JSON.org/json2.js
+ 2009-09-29
+
+ Public Domain.
+
+ NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
+
+ See http://www.JSON.org/js.html
+
+
+ This code should be minified before deployment.
+ See http://javascript.crockford.com/jsmin.html
+
+ USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
+ NOT CONTROL.
+
+
+ This file creates a global JSON object containing two methods: stringify
+ and parse.
+
+ JSON.stringify(value, replacer, space)
+ value any JavaScript value, usually an object or array.
+
+ replacer an optional parameter that determines how object
+ values are stringified for objects. It can be a
+ function or an array of strings.
+
+ space an optional parameter that specifies the indentation
+ of nested structures. If it is omitted, the text will
+ be packed without extra whitespace. If it is a number,
+ it will specify the number of spaces to indent at each
+ level. If it is a string (such as '\t' or '&nbsp;'),
+ it contains the characters used to indent at each level.
+
+ This method produces a JSON text from a JavaScript value.
+
+ When an object value is found, if the object contains a toJSON
+ method, its toJSON method will be called and the result will be
+ stringified. A toJSON method does not serialize: it returns the
+ value represented by the name/value pair that should be serialized,
+ or undefined if nothing should be serialized. The toJSON method
+ will be passed the key associated with the value, and this will be
+ bound to the value
+
+ For example, this would serialize Dates as ISO strings.
+
+ Date.prototype.toJSON = function (key) {
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ return this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z';
+ };
+
+ You can provide an optional replacer method. It will be passed the
+ key and value of each member, with this bound to the containing
+ object. The value that is returned from your method will be
+ serialized. If your method returns undefined, then the member will
+ be excluded from the serialization.
+
+ If the replacer parameter is an array of strings, then it will be
+ used to select the members to be serialized. It filters the results
+ such that only members with keys listed in the replacer array are
+ stringified.
+
+ Values that do not have JSON representations, such as undefined or
+ functions, will not be serialized. Such values in objects will be
+ dropped; in arrays they will be replaced with null. You can use
+ a replacer function to replace those with JSON values.
+ JSON.stringify(undefined) returns undefined.
+
+ The optional space parameter produces a stringification of the
+ value that is filled with line breaks and indentation to make it
+ easier to read.
+
+ If the space parameter is a non-empty string, then that string will
+ be used for indentation. If the space parameter is a number, then
+ the indentation will be that many spaces.
+
+ Example:
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}]);
+ // text is '["e",{"pluribus":"unum"}]'
+
+
+ text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t');
+ // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]'
+
+ text = JSON.stringify([new Date()], function (key, value) {
+ return this[key] instanceof Date ?
+ 'Date(' + this[key] + ')' : value;
+ });
+ // text is '["Date(---current time---)"]'
+
+
+ JSON.parse(text, reviver)
+ This method parses a JSON text to produce an object or array.
+ It can throw a SyntaxError exception.
+
+ The optional reviver parameter is a function that can filter and
+ transform the results. It receives each of the keys and values,
+ and its return value is used instead of the original value.
+ If it returns what it received, then the structure is not modified.
+ If it returns undefined then the member is deleted.
+
+ Example:
+
+ // Parse the text. Values that look like ISO date strings will
+ // be converted to Date objects.
+
+ myData = JSON.parse(text, function (key, value) {
+ var a;
+ if (typeof value === 'string') {
+ a =
+/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value);
+ if (a) {
+ return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4],
+ +a[5], +a[6]));
+ }
+ }
+ return value;
+ });
+
+ myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) {
+ var d;
+ if (typeof value === 'string' &&
+ value.slice(0, 5) === 'Date(' &&
+ value.slice(-1) === ')') {
+ d = new Date(value.slice(5, -1));
+ if (d) {
+ return d;
+ }
+ }
+ return value;
+ });
+
+
+ This is a reference implementation. You are free to copy, modify, or
+ redistribute.
+*/
+
+/*jslint evil: true, strict: false */
+
+/*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply,
+ call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours,
+ getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join,
+ lastIndex, length, parse, prototype, push, replace, slice, stringify,
+ test, toJSON, toString, valueOf
+*/
+
+
+// Create a JSON object only if one does not already exist. We create the
+// methods in a closure to avoid creating global variables.
+
+if (!this.JSON) {
+ this.JSON = {};
+}
+
+(function () {
+
+ function f(n) {
+ // Format integers to have at least two digits.
+ return n < 10 ? '0' + n : n;
+ }
+
+ if (typeof Date.prototype.toJSON !== 'function') {
+
+ Date.prototype.toJSON = function (key) {
+
+ return isFinite(this.valueOf()) ?
+ this.getUTCFullYear() + '-' +
+ f(this.getUTCMonth() + 1) + '-' +
+ f(this.getUTCDate()) + 'T' +
+ f(this.getUTCHours()) + ':' +
+ f(this.getUTCMinutes()) + ':' +
+ f(this.getUTCSeconds()) + 'Z' : null;
+ };
+
+ String.prototype.toJSON =
+ Number.prototype.toJSON =
+ Boolean.prototype.toJSON = function (key) {
+ return this.valueOf();
+ };
+ }
+
+ var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
+ gap,
+ indent,
+ meta = { // table of character substitutions
+ '\b': '\\b',
+ '\t': '\\t',
+ '\n': '\\n',
+ '\f': '\\f',
+ '\r': '\\r',
+ '"' : '\\"',
+ '\\': '\\\\'
+ },
+ rep;
+
+
+ function quote(string) {
+
+// If the string contains no control characters, no quote characters, and no
+// backslash characters, then we can safely slap some quotes around it.
+// Otherwise we must also replace the offending characters with safe escape
+// sequences.
+
+ escapable.lastIndex = 0;
+ return escapable.test(string) ?
+ '"' + string.replace(escapable, function (a) {
+ var c = meta[a];
+ return typeof c === 'string' ? c :
+ '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ }) + '"' :
+ '"' + string + '"';
+ }
+
+
+ function str(key, holder) {
+
+// Produce a string from holder[key].
+
+ var i, // The loop counter.
+ k, // The member key.
+ v, // The member value.
+ length,
+ mind = gap,
+ partial,
+ value = holder[key];
+
+// If the value has a toJSON method, call it to obtain a replacement value.
+
+ if (value && typeof value === 'object' &&
+ typeof value.toJSON === 'function') {
+ value = value.toJSON(key);
+ }
+
+// If we were called with a replacer function, then call the replacer to
+// obtain a replacement value.
+
+ if (typeof rep === 'function') {
+ value = rep.call(holder, key, value);
+ }
+
+// What happens next depends on the value's type.
+
+ switch (typeof value) {
+ case 'string':
+ return quote(value);
+
+ case 'number':
+
+// JSON numbers must be finite. Encode non-finite numbers as null.
+
+ return isFinite(value) ? String(value) : 'null';
+
+ case 'boolean':
+ case 'null':
+
+// If the value is a boolean or null, convert it to a string. Note:
+// typeof null does not produce 'null'. The case is included here in
+// the remote chance that this gets fixed someday.
+
+ return String(value);
+
+// If the type is 'object', we might be dealing with an object or an array or
+// null.
+
+ case 'object':
+
+// Due to a specification blunder in ECMAScript, typeof null is 'object',
+// so watch out for that case.
+
+ if (!value) {
+ return 'null';
+ }
+
+// Make an array to hold the partial results of stringifying this object value.
+
+ gap += indent;
+ partial = [];
+
+// Is the value an array?
+
+ if (Object.prototype.toString.apply(value) === '[object Array]') {
+
+// The value is an array. Stringify every element. Use null as a placeholder
+// for non-JSON values.
+
+ length = value.length;
+ for (i = 0; i < length; i += 1) {
+ partial[i] = str(i, value) || 'null';
+ }
+
+// Join all of the elements together, separated with commas, and wrap them in
+// brackets.
+
+ v = partial.length === 0 ? '[]' :
+ gap ? '[\n' + gap +
+ partial.join(',\n' + gap) + '\n' +
+ mind + ']' :
+ '[' + partial.join(',') + ']';
+ gap = mind;
+ return v;
+ }
+
+// If the replacer is an array, use it to select the members to be stringified.
+
+ if (rep && typeof rep === 'object') {
+ length = rep.length;
+ for (i = 0; i < length; i += 1) {
+ k = rep[i];
+ if (typeof k === 'string') {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ } else {
+
+// Otherwise, iterate through all of the keys in the object.
+
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = str(k, value);
+ if (v) {
+ partial.push(quote(k) + (gap ? ': ' : ':') + v);
+ }
+ }
+ }
+ }
+
+// Join all of the member texts together, separated with commas,
+// and wrap them in braces.
+
+ v = partial.length === 0 ? '{}' :
+ gap ? '{\n' + gap + partial.join(',\n' + gap) + '\n' +
+ mind + '}' : '{' + partial.join(',') + '}';
+ gap = mind;
+ return v;
+ }
+ }
+
+// If the JSON object does not yet have a stringify method, give it one.
+
+ if (typeof JSON.stringify !== 'function') {
+ JSON.stringify = function (value, replacer, space) {
+
+// The stringify method takes a value and an optional replacer, and an optional
+// space parameter, and returns a JSON text. The replacer can be a function
+// that can replace values, or an array of strings that will select the keys.
+// A default replacer method can be provided. Use of the space parameter can
+// produce text that is more easily readable.
+
+ var i;
+ gap = '';
+ indent = '';
+
+// If the space parameter is a number, make an indent string containing that
+// many spaces.
+
+ if (typeof space === 'number') {
+ for (i = 0; i < space; i += 1) {
+ indent += ' ';
+ }
+
+// If the space parameter is a string, it will be used as the indent string.
+
+ } else if (typeof space === 'string') {
+ indent = space;
+ }
+
+// If there is a replacer, it must be a function or an array.
+// Otherwise, throw an error.
+
+ rep = replacer;
+ if (replacer && typeof replacer !== 'function' &&
+ (typeof replacer !== 'object' ||
+ typeof replacer.length !== 'number')) {
+ throw new Error('JSON.stringify');
+ }
+
+// Make a fake root object containing our value under the key of ''.
+// Return the result of stringifying the value.
+
+ return str('', {'': value});
+ };
+ }
+
+
+// If the JSON object does not yet have a parse method, give it one.
+
+ if (typeof JSON.parse !== 'function') {
+ JSON.parse = function (text, reviver) {
+
+// The parse method takes a text and an optional reviver function, and returns
+// a JavaScript value if the text is a valid JSON text.
+
+ var j;
+
+ function walk(holder, key) {
+
+// The walk method is used to recursively walk the resulting structure so
+// that modifications can be made.
+
+ var k, v, value = holder[key];
+ if (value && typeof value === 'object') {
+ for (k in value) {
+ if (Object.hasOwnProperty.call(value, k)) {
+ v = walk(value, k);
+ if (v !== undefined) {
+ value[k] = v;
+ } else {
+ delete value[k];
+ }
+ }
+ }
+ }
+ return reviver.call(holder, key, value);
+ }
+
+
+// Parsing happens in four stages. In the first stage, we replace certain
+// Unicode characters with escape sequences. JavaScript handles many characters
+// incorrectly, either silently deleting them, or treating them as line endings.
+
+ cx.lastIndex = 0;
+ if (cx.test(text)) {
+ text = text.replace(cx, function (a) {
+ return '\\u' +
+ ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
+ });
+ }
+
+// In the second stage, we run the text against regular expressions that look
+// for non-JSON patterns. We are especially concerned with '()' and 'new'
+// because they can cause invocation, and '=' because it can cause mutation.
+// But just to be safe, we want to reject all unexpected forms.
+
+// We split the second stage into 4 regexp operations in order to work around
+// crippling inefficiencies in IE's and Safari's regexp engines. First we
+// replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
+// replace all simple value tokens with ']' characters. Third, we delete all
+// open brackets that follow a colon or comma or that begin the text. Finally,
+// we look to see that the remaining characters are only whitespace or ']' or
+// ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.
+
+ if (/^[\],:{}\s]*$/.
+test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@').
+replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']').
+replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {
+
+// In the third stage we use the eval function to compile the text into a
+// JavaScript structure. The '{' operator is subject to a syntactic ambiguity
+// in JavaScript: it can begin a block or an object literal. We wrap the text
+// in parens to eliminate the ambiguity.
+
+ j = eval('(' + text + ')');
+
+// In the optional fourth stage, we recursively walk the new structure, passing
+// each name/value pair to a reviver function for possible transformation.
+
+ return typeof reviver === 'function' ?
+ walk({'': j}, '') : j;
+ }
+
+// If the text is not JSON parseable, then a SyntaxError is thrown.
+
+ throw new SyntaxError('JSON.parse');
+ };
+ }
+}());
View
49 lib/spine.controller.manager.js
<
@@ -0,0 +1,49 @@
+(function(Spine, $){
+
+var Manager = Spine.Controller.Manager = Spine.Class.create();
+Manager.include(Spine.Events);
+
+Manager.include({
+ addAll: function(){
+ var args = Spine.makeArray(arguments);
+ for (var i=0; i < args.length; i++) this.add(args[i]);
+ },
+
+ add: function(controller){
+ if ( !controller ) throw("Controller required");
+
+ this.bind("change", function(current){
+ if (controller == current)
+ controller.activate();
+ else
+ controller.deactivate();
+ });
+
+ controller.active(this.proxy(function(){
+ this.trigger("change", controller);
+ }));
+ }