diff --git a/.gitignore b/.gitignore index bfd6222..9c2f2cd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ *.iml -.idea/ .ipr .iws *~ @@ -9,9 +8,8 @@ *.bak .DS_Store Thumbs.db -.svn/ *.swp .nojekyll .project -.settings/ node_modules +.*/ diff --git a/README.md b/README.md index c323028..a7831d1 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,25 @@ -Examples for SeaJS -=== +# Examples for Sea.js - + -__注意__ -如果你想构建示例,需要使用 [Gruntjs](http://gruntjs.com/) 与 [spm](https://github.com/spmjs/spm/wiki)。 \ No newline at end of file +How to Build +------------ + +First, you should install `spm` and `spm-build`: + +``` +$ npm install spm -g +$ npm install spm-build -g +``` + +Then, build it: + +``` +$ cd static/hello +$ make build +$ make deploy +``` + +Visit for detail information. + diff --git a/hello.html b/app/hello.html similarity index 70% rename from hello.html rename to app/hello.html index 811f503..b3c94eb 100644 --- a/hello.html +++ b/app/hello.html @@ -2,8 +2,8 @@ -Hello SeaJS - +Hello Sea.js +
@@ -20,9 +20,26 @@ S
- + + \ No newline at end of file diff --git a/app/lucky.html b/app/lucky.html new file mode 100644 index 0000000..bb2a56a --- /dev/null +++ b/app/lucky.html @@ -0,0 +1,41 @@ + + + +Lucky Ball + + + + + + +
+
    +
+
    +
+
+ + + + + + \ No newline at end of file diff --git a/todo.html b/app/todo.html similarity index 74% rename from todo.html rename to app/todo.html index e376f14..7de8b99 100644 --- a/todo.html +++ b/app/todo.html @@ -4,7 +4,7 @@ Todo - +
@@ -50,6 +50,29 @@

todos

<% } %> - + + + + diff --git a/index.html b/index.html index 9e883f1..0017891 100644 --- a/index.html +++ b/index.html @@ -2,17 +2,14 @@ -Examples for SeaJS +Examples for Sea.js @@ -20,9 +17,9 @@
   seajs
     `-- examples
-          | -- hello.html
-          | -- lucky.html
-          ` -- todo.html
+          | -- Hello Sea.js
+          | -- Lucky Ball
+          ` -- Todo
 
\ No newline at end of file diff --git a/lucky.html b/lucky.html deleted file mode 100644 index 5f3a433..0000000 --- a/lucky.html +++ /dev/null @@ -1,23 +0,0 @@ - - - -Lucky Ball - - - - - - -
-
    -
-
    -
-
- - - - - \ No newline at end of file diff --git a/sea-modules/examples/hello/1.0.0/main-debug.js b/sea-modules/examples/hello/1.0.0/main-debug.js new file mode 100644 index 0000000..e43e508 --- /dev/null +++ b/sea-modules/examples/hello/1.0.0/main-debug.js @@ -0,0 +1,54 @@ +define("examples/hello/1.0.0/main-debug", [ "./spinning-debug", "jquery-debug" ], function(require) { + var Spinning = require("./spinning-debug"); + var s = new Spinning("#container"); + s.render(); +}); + +define("examples/hello/1.0.0/spinning-debug", [ "jquery-debug" ], function(require, exports, module) { + var $ = require("jquery-debug"); + function Spinning(container) { + this.container = $(container); + this.icons = this.container.children(); + this.spinnings = []; + } + module.exports = Spinning; + Spinning.prototype.render = function() { + this._init(); + this.container.css("background", "none"); + this.icons.show(); + this._spin(); + }; + Spinning.prototype._init = function() { + var spinnings = this.spinnings; + $(this.icons).each(function(n) { + var startDeg = random(360); + var node = $(this); + var timer; + node.css({ + top: random(40), + left: n * 50 + random(10), + zIndex: 1e3 + }).hover(function() { + node.fadeTo(250, 1).css("zIndex", 1001).css("transform", "rotate(0deg)"); + }, function() { + node.fadeTo(250, .6).css("zIndex", 1e3); + timer && clearTimeout(timer); + timer = setTimeout(spin, Math.ceil(random(1e4))); + }); + function spin() { + node.css("transform", "rotate(" + startDeg + "deg)"); + } + spinnings[n] = spin; + }); + return this; + }; + Spinning.prototype._spin = function() { + $(this.spinnings).each(function(i, fn) { + setTimeout(fn, Math.ceil(random(3e3))); + }); + return this; + }; + function random(x) { + return Math.random() * x; + } +}); diff --git a/sea-modules/examples/hello/1.0.0/main.js b/sea-modules/examples/hello/1.0.0/main.js new file mode 100644 index 0000000..aa136cd --- /dev/null +++ b/sea-modules/examples/hello/1.0.0/main.js @@ -0,0 +1 @@ +define("examples/hello/1.0.0/main",["./spinning","jquery"],function(a){var b=a("./spinning"),c=new b("#container");c.render()}),define("examples/hello/1.0.0/spinning",["jquery"],function(a,b,c){function d(a){this.container=f(a),this.icons=this.container.children(),this.spinnings=[]}function e(a){return Math.random()*a}var f=a("jquery");c.exports=d,d.prototype.render=function(){this._init(),this.container.css("background","none"),this.icons.show(),this._spin()},d.prototype._init=function(){var a=this.spinnings;return f(this.icons).each(function(b){function c(){h.css("transform","rotate("+g+"deg)")}var d,g=e(360),h=f(this);h.css({top:e(40),left:50*b+e(10),zIndex:1e3}).hover(function(){h.fadeTo(250,1).css("zIndex",1001).css("transform","rotate(0deg)")},function(){h.fadeTo(250,.6).css("zIndex",1e3),d&&clearTimeout(d),d=setTimeout(c,Math.ceil(e(1e4)))}),a[b]=c}),this},d.prototype._spin=function(){return f(this.spinnings).each(function(a,b){setTimeout(b,Math.ceil(e(3e3)))}),this}}); diff --git a/sea-modules/examples/hello/1.0.0/style-debug.css b/sea-modules/examples/hello/1.0.0/style-debug.css new file mode 100644 index 0000000..725ba69 --- /dev/null +++ b/sea-modules/examples/hello/1.0.0/style-debug.css @@ -0,0 +1,36 @@ +/*! define examples/hello/1.0.0/style-debug.css */ +html, body, div, span, h1, h2, h3, h4, p, img, strong, ol, ul, li { + margin: 0; + padding: 0; + border: 0; + font-size: 100%; + vertical-align: baseline; + background: transparent; +} + +body { + background: #F4F4F4; +} + +#container { + position: relative; + margin: 40px 120px; + background: url(https://a248.e.akamai.net/assets.github.com/images/spinners/octocat-spinner-32.gif) no-repeat; + min-height: 32px; +} + +#container img { + position: absolute; + cursor: pointer; + opacity: .6; + display: none; + + -webkit-transition-property: -webkit-transform; + -moz-transition-property: -moz-transform; + -o-transition-property: -o-transform; + transition-property: transform; + -webkit-transition-duration: 0.8s; + -moz-transition-duration: 0.8s; + -o-transition-duration: 0.8s; + transition-duration: 0.8s; +} diff --git a/sea-modules/examples/hello/1.0.0/style.css b/sea-modules/examples/hello/1.0.0/style.css new file mode 100644 index 0000000..e0930b4 --- /dev/null +++ b/sea-modules/examples/hello/1.0.0/style.css @@ -0,0 +1 @@ +html,body,div,span,h1,h2,h3,h4,p,img,strong,ol,ul,li{margin:0;padding:0;border:0;font-size:100%;vertical-align:baseline;background:transparent}body{background:#F4F4F4}#container{position:relative;margin:40px 120px;background:url(https://a248.e.akamai.net/assets.github.com/images/spinners/octocat-spinner-32.gif) no-repeat;min-height:32px}#container img{position:absolute;cursor:pointer;opacity:.6;display:none;-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;transition-property:transform;-webkit-transition-duration:.8s;-moz-transition-duration:.8s;-o-transition-duration:.8s;transition-duration:.8s} diff --git a/sea-modules/examples/lucky/1.0.0/main-debug.js b/sea-modules/examples/lucky/1.0.0/main-debug.js new file mode 100644 index 0000000..0027c16 --- /dev/null +++ b/sea-modules/examples/lucky/1.0.0/main-debug.js @@ -0,0 +1,294 @@ +if (document.attachEvent) { + alert("这个例子不支持 Old IE 哦"); +} + +define("examples/lucky/1.0.0/main-debug", [ "./data-debug", "./lucky-debug", "jquery-debug", "jquery-easing-debug", "./user-debug" ], function(require) { + var data = require("./data-debug"); + var lucky = require("./lucky-debug"); + lucky.init(data); +}); + +define("examples/lucky/1.0.0/data-debug", [], [ "马云", "马化腾", "李彦宏", "周鸿祎", "玉伯", "沉鱼", "丹侠", "贯高", "邵帅", "陆辉", "南伯", "崔护", "偏右", "远尘", "臻儿", "左宜", "张初尘", "初成", "琼羽", "异草", "闭月", "周爱民", "陈皓", "winter", "army", "hax", "老赵", "sofish", "罗龙浩" ]); + +define("examples/lucky/1.0.0/lucky-debug", [ "jquery-debug", "jquery-easing-debug", "examples/lucky/1.0.0/user-debug" ], function(require, exports, module) { + var $ = require("jquery-debug"); + require("jquery-easing-debug"); + var User = require("examples/lucky/1.0.0/user-debug"); + var HIT_SPEED = 100; + var RIGIDITY = 4; + module.exports = { + users: [], + init: function(data) { + $("#container").css("background", "none"); + this.data = data; + this.users = data.map(function(name) { + return new User(name, data[name]); + }); + this._bindUI(); + }, + _bindUI: function() { + var that = this; + // bind button + var trigger = document.querySelector("#go"); + trigger.innerHTML = trigger.getAttribute("data-text-start"); + trigger.addEventListener("click", go, false); + function go() { + if (trigger.getAttribute("data-action") === "start") { + trigger.setAttribute("data-action", "stop"); + trigger.innerHTML = trigger.getAttribute("data-text-stop"); + that.start(); + } else { + trigger.setAttribute("data-action", "start"); + trigger.innerHTML = trigger.getAttribute("data-text-start"); + that.stop(); + } + } + // bind #lucky-balls + $("#lucky-balls").on("click", "li", function(e) { + var el = $(e.target); + var name = el.text(); + that.addItem(name); + that.hit(); + el.remove(); + }); + // bind #balls + $("#balls").on("click", "li", function(e) { + var el = $(e.target); + var name = el.text(); + for (var i = 0; i < that.users.length; i++) { + var user = that.users[i]; + if (user.name === name) { + that.moveLucky(); + if (that.luckyUser !== user) { + that.setLucky(user); + } + break; + } + } + }); + // bind keydown + document.addEventListener("keydown", function(ev) { + if (ev.keyCode == "32") { + go(); + } else if (ev.keyCode == "27") { + that.moveLucky(); + $("#lucky-balls li").eq(0).click(); + } + }, false); + }, + start: function() { + this.timer && clearTimeout(this.timer); + this.moveLucky(); + this.users.forEach(function(user) { + user.start(); + }); + }, + stop: function() { + var users = this.users; + var z = 0, lucky = users[0]; + users.forEach(function(user) { + user.stop(); + if (z < user.zIndex) { + lucky = user; + z = user.zIndex; + } + }); + lucky.bang(); + this.hit(); + this.luckyUser = lucky; + }, + removeItem: function(item) { + for (var i = 0; i < this.users.length; i++) { + var user = this.users[i]; + if (user === item) { + this.users.splice(i, 1); + } + } + }, + addItem: function(name) { + this.users.push(new User(name)); + }, + moveLucky: function() { + var luckyUser = this.luckyUser; + if (luckyUser) { + luckyUser.el[0].style.cssText = ""; + luckyUser.el.prependTo("#lucky-balls"); + this.removeItem(luckyUser); + this.luckyUser = null; + } + }, + setLucky: function(item) { + this.users.forEach(function(user) { + user.stop(); + }); + this.luckyUser = item; + item.bang(); + this.hit(); + }, + hit: function() { + var that = this; + var hitCount = 0; + var users = this.users; + users.forEach(function(user) { + user.beginHit(); + }); + for (var i = 0; i < users.length; i++) { + for (var j = i + 1; j < users.length; j++) { + if (isOverlap(users[i], users[j])) { + hit(users[i], users[j]); + hitCount++; + } + } + } + users.forEach(function(user) { + user.hitMove(); + }); + if (hitCount > 0) { + this.timer = setTimeout(function() { + that.hit(); + }, HIT_SPEED); + } + } + }; + // Helpers + function getOffset(a, b) { + return Math.sqrt((a.x - b.x) * (a.x - b.x) + (a.y - b.y) * (a.y - b.y)); + } + function isOverlap(a, b) { + return getOffset(a, b) <= (a.width + b.width) / 2; + } + function hit(a, b) { + var yOffset = b.y - a.y; + var xOffset = b.x - a.x; + var offset = getOffset(a, b); + var power = Math.ceil(((a.width + b.width) / 2 - offset) / RIGIDITY); + var yStep = yOffset > 0 ? Math.ceil(power * yOffset / offset) : Math.floor(power * yOffset / offset); + var xStep = xOffset > 0 ? Math.ceil(power * xOffset / offset) : Math.floor(power * xOffset / offset); + if (a.lucky) { + b._xMove += xStep * 2; + b._yMove += yStep * 2; + } else if (b.lucky) { + a._xMove += xStep * -2; + a._yMove += yStep * -2; + } else { + a._yMove += -1 * yStep; + b._yMove += yStep; + a._xMove += -1 * xStep; + b._xMove += xStep; + } + } +}); + +define("examples/lucky/1.0.0/user-debug", [ "jquery-debug" ], function(require, exports, module) { + var $ = require("jquery-debug"); + var CANVAS_HEIGHT = 500; + var CANVAS_WIDTH = 900; + var BALL_WIDTH = 60; + var BALL_HEIGHT = 60; + var LUCKY_BALL_WIDTH = 200; + var LUCKY_BALL_HEIGHT = 200; + var MAX_ZINDEX = 100; + var DURATION_MIN = 100; + var DURATION_MAX = 500; + var ZOOM_DURATION = 500; + function User(name, options) { + this.name = name; + this.options = options || {}; + this.el = null; + this.width = 0; + this.height = 0; + this.left = 0; + this.top = 0; + this.x = 0; + this.y = 0; + this.moving = false; + this.lucky = false; + this.createEl(); + this.move(); + } + module.exports = User; + User.prototype.createEl = function() { + this.el = $("
  • " + this.name + "
  • ").appendTo("#balls"); + this.width = this.el.width(); + this.height = this.el.height(); + }; + User.prototype.move = function(callback) { + this.left = r(0, CANVAS_WIDTH - this.width); + this.top = r(0, CANVAS_HEIGHT - this.height); + this.zIndex = r(0, MAX_ZINDEX); + this.reflow(callback); + }; + User.prototype.reflow = function(callback, direct) { + this.x = this.left + this.width / 2; + this.y = this.top + this.height / 2; + this.el[0].style.zIndex = this.zIndex; + if (direct) { + this.el[0].style.left = this.left; + this.el[0].style.top = this.top; + } else { + this.el.animate({ + left: this.left, + top: this.top + }, r(DURATION_MIN, DURATION_MAX), "easeOutBack", callback); + } + }; + User.prototype.start = function() { + this.reset(); + this.moving = true; + this.autoMove(); + }; + User.prototype.reset = function() { + this.el.stop(true, true); + this.lucky = false; + this.el[0].className = ""; + this.el[0].style.width = BALL_WIDTH + "px"; + this.el[0].style.height = BALL_HEIGHT + "px"; + this.width = this.el.width(); + this.height = this.el.height(); + this._maxTop = CANVAS_HEIGHT - this.height; + this._maxLeft = CANVAS_WIDTH - this.width; + }; + User.prototype.autoMove = function() { + var that = this; + if (this.moving) { + this.move(function() { + that.autoMove(); + }); + } + }; + User.prototype.stop = function() { + this.el.stop(true, true); + this.moving = false; + }; + User.prototype.bang = function() { + this.lucky = true; + this.el[0].className = "selected"; + this.width = LUCKY_BALL_WIDTH; + this.height = LUCKY_BALL_HEIGHT; + this.left = (CANVAS_WIDTH - this.width) / 2; + this.top = (CANVAS_HEIGHT - this.height) / 2; + this.el.animate({ + left: this.left, + top: this.top, + width: this.width, + height: this.height + }, ZOOM_DURATION); + }; + User.prototype.beginHit = function() { + this._xMove = 0; + this._yMove = 0; + }; + User.prototype.hitMove = function() { + this.left += this._xMove; + this.top += this._yMove; + this.top = this.top < 0 ? 0 : this.top > this._maxTop ? this._maxTop : this.top; + this.left = this.left < 0 ? 0 : this.left > this._maxLeft ? this._maxLeft : this.left; + this.reflow(null, false); + }; + // Helpers + function r(from, to) { + from = from || 0; + to = to || 1; + return Math.floor(Math.random() * (to - from + 1) + from); + } +}); diff --git a/sea-modules/examples/lucky/1.0.0/main.js b/sea-modules/examples/lucky/1.0.0/main.js new file mode 100644 index 0000000..aa2c61e --- /dev/null +++ b/sea-modules/examples/lucky/1.0.0/main.js @@ -0,0 +1 @@ +document.attachEvent&&alert("这个例子不支持 Old IE 哦"),define("examples/lucky/1.0.0/main",["./data","./lucky","jquery","jquery-easing","./user"],function(a){var b=a("./data"),c=a("./lucky");c.init(b)}),define("examples/lucky/1.0.0/data",[],["马云","马化腾","李彦宏","周鸿祎","玉伯","沉鱼","丹侠","贯高","邵帅","陆辉","南伯","崔护","偏右","远尘","臻儿","左宜","张初尘","初成","琼羽","异草","闭月","周爱民","陈皓","winter","army","hax","老赵","sofish","罗龙浩"]),define("examples/lucky/1.0.0/lucky",["jquery","jquery-easing","examples/lucky/1.0.0/user"],function(a,b,c){function d(a,b){return Math.sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y))}function e(a,b){return d(a,b)<=(a.width+b.width)/2}function f(a,b){var c=b.y-a.y,e=b.x-a.x,f=d(a,b),g=Math.ceil(((a.width+b.width)/2-f)/j),h=c>0?Math.ceil(g*c/f):Math.floor(g*c/f),i=e>0?Math.ceil(g*e/f):Math.floor(g*e/f);a.lucky?(b._xMove+=2*i,b._yMove+=2*h):b.lucky?(a._xMove+=-2*i,a._yMove+=-2*h):(a._yMove+=-1*h,b._yMove+=h,a._xMove+=-1*i,b._xMove+=i)}var g=a("jquery");a("jquery-easing");var h=a("examples/lucky/1.0.0/user"),i=100,j=4;c.exports={users:[],init:function(a){g("#container").css("background","none"),this.data=a,this.users=a.map(function(b){return new h(b,a[b])}),this._bindUI()},_bindUI:function(){function a(){"start"===c.getAttribute("data-action")?(c.setAttribute("data-action","stop"),c.innerHTML=c.getAttribute("data-text-stop"),b.start()):(c.setAttribute("data-action","start"),c.innerHTML=c.getAttribute("data-text-start"),b.stop())}var b=this,c=document.querySelector("#go");c.innerHTML=c.getAttribute("data-text-start"),c.addEventListener("click",a,!1),g("#lucky-balls").on("click","li",function(a){var c=g(a.target),d=c.text();b.addItem(d),b.hit(),c.remove()}),g("#balls").on("click","li",function(a){for(var c=g(a.target),d=c.text(),e=0;e0&&(this.timer=setTimeout(function(){a.hit()},i))}}}),define("examples/lucky/1.0.0/user",["jquery"],function(a,b,c){function d(a,b){this.name=a,this.options=b||{},this.el=null,this.width=0,this.height=0,this.left=0,this.top=0,this.x=0,this.y=0,this.moving=!1,this.lucky=!1,this.createEl(),this.move()}function e(a,b){return a=a||0,b=b||1,Math.floor(Math.random()*(b-a+1)+a)}var f=a("jquery"),g=500,h=900,i=60,j=60,k=200,l=200,m=100,n=100,o=500,p=500;c.exports=d,d.prototype.createEl=function(){this.el=f("
  • "+this.name+"
  • ").appendTo("#balls"),this.width=this.el.width(),this.height=this.el.height()},d.prototype.move=function(a){this.left=e(0,h-this.width),this.top=e(0,g-this.height),this.zIndex=e(0,m),this.reflow(a)},d.prototype.reflow=function(a,b){this.x=this.left+this.width/2,this.y=this.top+this.height/2,this.el[0].style.zIndex=this.zIndex,b?(this.el[0].style.left=this.left,this.el[0].style.top=this.top):this.el.animate({left:this.left,top:this.top},e(n,o),"easeOutBack",a)},d.prototype.start=function(){this.reset(),this.moving=!0,this.autoMove()},d.prototype.reset=function(){this.el.stop(!0,!0),this.lucky=!1,this.el[0].className="",this.el[0].style.width=i+"px",this.el[0].style.height=j+"px",this.width=this.el.width(),this.height=this.el.height(),this._maxTop=g-this.height,this._maxLeft=h-this.width},d.prototype.autoMove=function(){var a=this;this.moving&&this.move(function(){a.autoMove()})},d.prototype.stop=function(){this.el.stop(!0,!0),this.moving=!1},d.prototype.bang=function(){this.lucky=!0,this.el[0].className="selected",this.width=k,this.height=l,this.left=(h-this.width)/2,this.top=(g-this.height)/2,this.el.animate({left:this.left,top:this.top,width:this.width,height:this.height},p)},d.prototype.beginHit=function(){this._xMove=0,this._yMove=0},d.prototype.hitMove=function(){this.left+=this._xMove,this.top+=this._yMove,this.top=this.top<0?0:this.top>this._maxTop?this._maxTop:this.top,this.left=this.left<0?0:this.left>this._maxLeft?this._maxLeft:this.left,this.reflow(null,!1)}}); diff --git a/sea-modules/examples/lucky/1.0.0/style-debug.css b/sea-modules/examples/lucky/1.0.0/style-debug.css new file mode 100644 index 0000000..d234a26 --- /dev/null +++ b/sea-modules/examples/lucky/1.0.0/style-debug.css @@ -0,0 +1,86 @@ +/*! define examples/lucky/1.0.0/style-debug.css */ +body { + font: 20px "Hiragino Sans GB", sans-serif; + background: #eee; +} + +ul li { + list-style: none; +} + +#container { + width: 900px; + margin: 10px auto; + position: relative; + background: url(https://a248.e.akamai.net/assets.github.com/images/spinners/octocat-spinner-32.gif) no-repeat 45% 25%; +} + +#balls { + padding: 0; + margin: 0; + list-style: none; + height: 500px; +} + +#balls li, +#lucky-balls li { + position: absolute; + left: 420px; + top: 470px; + padding: 0; + width: 60px; + height: 60px; + overflow: hidden; + font-size: 16px; + background: #333; + color: #ddd; + opacity: .5; + text-align: center; + line-height: 60px; + border-radius: 30px; + word-break: keep-all; + cursor: pointer; +} + +#go { + width: 200px; + height: 50px; + display: block; + margin: 0 auto; + font-size: 22px; + border: none; + background: none; + color: #999; +} + +#balls li.selected { + color: #fff; + background: #f60; + font-size: 50px; + height: 200px; + width: 200px; + line-height: 200px; + border-radius: 100px; + -webkit-transition: 0.3s ease-in; + -webkit-transition-property: width, height, font; + z-index: 100; +} + +#lucky-balls { + padding: 20px; + margin: 10px 0; + list-style: none; +} + +#lucky-balls li { + position: relative; + left: auto !important; + top: auto !important; + width: 60px !important; + height: 60px !important; + float: left; + margin: 5px 10px; + font-weight: bold; + color: #fff; + background: #f60; +} diff --git a/sea-modules/examples/lucky/1.0.0/style.css b/sea-modules/examples/lucky/1.0.0/style.css new file mode 100644 index 0000000..81b3f05 --- /dev/null +++ b/sea-modules/examples/lucky/1.0.0/style.css @@ -0,0 +1 @@ +body{font:20px "Hiragino Sans GB",sans-serif;background:#eee}ul li{list-style:none}#container{width:900px;margin:10px auto;position:relative;background:url(https://a248.e.akamai.net/assets.github.com/images/spinners/octocat-spinner-32.gif) no-repeat 45% 25%}#balls{padding:0;margin:0;list-style:none;height:500px}#balls li,#lucky-balls li{position:absolute;left:420px;top:470px;padding:0;width:60px;height:60px;overflow:hidden;font-size:16px;background:#333;color:#ddd;opacity:.5;text-align:center;line-height:60px;border-radius:30px;word-break:keep-all;cursor:pointer}#go{width:200px;height:50px;display:block;margin:0 auto;font-size:22px;border:0;background:0;color:#999}#balls li.selected{color:#fff;background:#f60;font-size:50px;height:200px;width:200px;line-height:200px;border-radius:100px;-webkit-transition:.3s ease-in;-webkit-transition-property:width,height,font;z-index:100}#lucky-balls{padding:20px;margin:10px 0;list-style:none}#lucky-balls li{position:relative;left:auto!important;top:auto!important;width:60px!important;height:60px!important;float:left;margin:5px 10px;font-weight:700;color:#fff;background:#f60} diff --git a/sea-modules/examples/todo/1.0.0/base-debug.css b/sea-modules/examples/todo/1.0.0/base-debug.css new file mode 100644 index 0000000..52351b0 --- /dev/null +++ b/sea-modules/examples/todo/1.0.0/base-debug.css @@ -0,0 +1,415 @@ +/*! define examples/todo/1.0.0/base-debug.css */ +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; + /*-moz-appearance: none;*/ + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +body { + font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; + line-height: 1.4em; + background: #eaeaea url('bg.png'); + 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: 130px 0 40px 0; + border: 1px solid #ccc; + position: relative; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.2), + 0 25px 50px 0 rgba(0, 0, 0, 0.15); +} + +#todoapp:before { + content: ''; + border-left: 1px solid #f5d6d6; + border-right: 1px solid #f5d6d6; + width: 2px; + position: absolute; + top: 0; + left: 40px; + height: 100%; +} + +#todoapp input::-webkit-input-placeholder { + font-style: italic; +} + +#todoapp input:-moz-placeholder { + font-style: italic; + color: #a9a9a9; +} + +#todoapp h1 { + position: absolute; + top: -120px; + width: 100%; + font-size: 70px; + font-weight: bold; + text-align: center; + color: #b3b3b3; + color: rgba(255, 255, 255, 0.3); + text-shadow: -1px -1px rgba(0, 0, 0, 0.2); + -webkit-text-rendering: optimizeLegibility; + -moz-text-rendering: optimizeLegibility; + -ms-text-rendering: optimizeLegibility; + -o-text-rendering: optimizeLegibility; + text-rendering: optimizeLegibility; +} + +#header { + padding-top: 15px; + border-radius: inherit; +} + +#header:before { + content: ''; + position: absolute; + top: 0; + right: 0; + left: 0; + height: 15px; + z-index: 2; + border-bottom: 1px solid #6c615c; + background: #8d7d77; + background: -webkit-gradient(linear, left top, left bottom, from(rgba(132, 110, 100, 0.8)),to(rgba(101, 84, 76, 0.8))); + background: -webkit-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: -moz-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: -o-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: -ms-linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + background: linear-gradient(top, rgba(132, 110, 100, 0.8), rgba(101, 84, 76, 0.8)); + filter: progid:DXImageTransform.Microsoft.gradient(GradientType=0,StartColorStr='#9d8b83', EndColorStr='#847670'); + 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: -42px; + left: -4px; + width: 40px; + text-align: center; + border: none; /* Mobile Safari */ +} + +#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; +} + +#todo-list li { + 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; + /* auto, since non-WebKit browsers doesn't support input styling */ + height: auto; + position: absolute; + top: 0; + bottom: 0; + margin: auto 0; + border: none; /* Mobile Safari */ + -webkit-appearance: none; + /*-moz-appearance: none;*/ + -ms-appearance: none; + -o-appearance: none; + appearance: none; +} + +#todo-list li .toggle:after { + 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; + padding: 15px; + margin-left: 45px; + display: 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 .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 .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 .destroy:after { + content: '✖'; +} + +#todo-list li:hover .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; + height: 20px; + z-index: 1; + text-align: center; +} + +#footer:before { + content: ''; + position: absolute; + right: 0; + bottom: 31px; + left: 0; + height: 50px; + z-index: -1; + box-shadow: 0 1px 1px rgba(0, 0, 0, 0.3), + 0 6px 0 -3px rgba(255, 255, 255, 0.8), + 0 7px 1px -3px rgba(0, 0, 0, 0.3), + 0 43px 0 -6px rgba(255, 255, 255, 0.8), + 0 44px 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; + position: relative; + line-height: 20px; + text-decoration: none; + background: rgba(0, 0, 0, 0.1); + font-size: 11px; + padding: 0 10px; + border-radius: 3px; + box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.2); +} + +#clear-completed:hover { + background: rgba(0, 0, 0, 0.15); + box-shadow: 0 -1px 0 0 rgba(0, 0, 0, 0.3); +} + +#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; + } + + #todo-list li .toggle { + height: 40px; + } + + #toggle-all { + top: -56px; + left: -15px; + width: 65px; + height: 41px; + -webkit-transform: rotate(90deg); + transform: rotate(90deg); + -webkit-appearance: none; + appearance: none; + } +} + +.hidden{ + display:none; +} diff --git a/sea-modules/examples/todo/1.0.0/base.css b/sea-modules/examples/todo/1.0.0/base.css new file mode 100644 index 0000000..a17d904 --- /dev/null +++ b/sea-modules/examples/todo/1.0.0/base.css @@ -0,0 +1 @@ +html,body{margin:0;padding:0}button{margin:0;padding:0;border:0;background:0;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 'Helvetica Neue',Helvetica,Arial,sans-serif;line-height:1.4em;background:#eaeaea url(bg.png);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,.9);margin:130px 0 40px;border:1px solid #ccc;position:relative;border-top-left-radius:2px;border-top-right-radius:2px;box-shadow:0 2px 6px 0 rgba(0,0,0,.2),0 25px 50px 0 rgba(0,0,0,.15)}#todoapp:before{content:'';border-left:1px solid #f5d6d6;border-right:1px solid #f5d6d6;width:2px;position:absolute;top:0;left:40px;height:100%}#todoapp input::-webkit-input-placeholder{font-style:italic}#todoapp input:-moz-placeholder{font-style:italic;color:#a9a9a9}#todoapp h1{position:absolute;top:-120px;width:100%;font-size:70px;font-weight:700;text-align:center;color:#b3b3b3;color:rgba(255,255,255,.3);text-shadow:-1px -1px rgba(0,0,0,.2);-webkit-text-rendering:optimizeLegibility;-moz-text-rendering:optimizeLegibility;-ms-text-rendering:optimizeLegibility;-o-text-rendering:optimizeLegibility;text-rendering:optimizeLegibility}#header{padding-top:15px;border-radius:inherit}#header:before{content:'';position:absolute;top:0;right:0;left:0;height:15px;z-index:2;border-bottom:1px solid #6c615c;background:#8d7d77;background:-webkit-gradient(linear,left top,left bottom,from(rgba(132,110,100,.8)),to(rgba(101,84,76,.8)));background:-webkit-linear-gradient(top,rgba(132,110,100,.8),rgba(101,84,76,.8));background:-moz-linear-gradient(top,rgba(132,110,100,.8),rgba(101,84,76,.8));background:-o-linear-gradient(top,rgba(132,110,100,.8),rgba(101,84,76,.8));background:-ms-linear-gradient(top,rgba(132,110,100,.8),rgba(101,84,76,.8));background:linear-gradient(top,rgba(132,110,100,.8),rgba(101,84,76,.8));filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0, StartColorStr='#9d8b83', EndColorStr='#847670');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:0;color:inherit;padding:6px;border:1px solid #999;box-shadow:inset 0 -1px 5px 0 rgba(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:0;background:rgba(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:-42px;left:-4px;width:40px;text-align:center;border:0}#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}#todo-list li{position:relative;font-size:24px;border-bottom:1px dotted #ccc}#todo-list li:last-child{border-bottom:0}#todo-list li.editing{border-bottom:0;padding:0}#todo-list li.editing .edit{display:block;width:506px;padding:13px 17px 12px;margin:0 0 0 43px}#todo-list li.editing .view{display:none}#todo-list li .toggle{text-align:center;width:40px;height:auto;position:absolute;top:0;bottom:0;margin:auto 0;border:0;-webkit-appearance:none;-ms-appearance:none;-o-appearance:none;appearance:none}#todo-list li .toggle:after{content:'✔';line-height:43px;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;padding:15px;margin-left:45px;display:block;line-height:1.2;-webkit-transition:color .4s;-moz-transition:color .4s;-ms-transition:color .4s;-o-transition:color .4s;transition:color .4s}#todo-list li.completed label{color:#a9a9a9;text-decoration:line-through}#todo-list li .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 .2s;-moz-transition:all .2s;-ms-transition:all .2s;-o-transition:all .2s;transition:all .2s}#todo-list li .destroy:hover{text-shadow:0 0 1px #000,0 0 10px rgba(199,107,107,.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 .destroy:after{content:'✖'}#todo-list li:hover .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;height:20px;z-index:1;text-align:center}#footer:before{content:'';position:absolute;right:0;bottom:31px;left:0;height:50px;z-index:-1;box-shadow:0 1px 1px rgba(0,0,0,.3),0 6px 0 -3px rgba(255,255,255,.8),0 7px 1px -3px rgba(0,0,0,.3),0 43px 0 -6px rgba(255,255,255,.8),0 44px 2px -6px rgba(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:700}#clear-completed{float:right;position:relative;line-height:20px;text-decoration:none;background:rgba(0,0,0,.1);font-size:11px;padding:0 10px;border-radius:3px;box-shadow:0 -1px 0 0 rgba(0,0,0,.2)}#clear-completed:hover{background:rgba(0,0,0,.15);box-shadow:0 -1px 0 0 rgba(0,0,0,.3)}#info{margin:65px auto 0;color:#a6a6a6;font-size:12px;text-shadow:0 1px 0 rgba(255,255,255,.7);text-align:center}#info a{color:inherit}@media screen and (-webkit-min-device-pixel-ratio:0){#toggle-all,#todo-list li .toggle{background:0}#todo-list li .toggle{height:40px}#toggle-all{top:-56px;left:-15px;width:65px;height:41px;-webkit-transform:rotate(90deg);transform:rotate(90deg);-webkit-appearance:none;appearance:none}}.hidden{display:none} diff --git a/static/todo/bg.png b/sea-modules/examples/todo/1.0.0/bg.png similarity index 100% rename from static/todo/bg.png rename to sea-modules/examples/todo/1.0.0/bg.png diff --git a/sea-modules/examples/todo/1.0.0/main-debug.js b/sea-modules/examples/todo/1.0.0/main-debug.js new file mode 100644 index 0000000..190f77f --- /dev/null +++ b/sea-modules/examples/todo/1.0.0/main-debug.js @@ -0,0 +1,388 @@ +define("examples/todo/1.0.0/main-debug", [ "backbone-debug", "./views/app-debug", "$-debug", "underscore-debug", "./views/todos-debug", "./common-debug", "./collections/todos-debug", "./vendor/backbone.localStorage-debug", "./models/todo-debug", "./routers/router-debug" ], function(require) { + var Backbone = require("backbone-debug"); + var app = require("./views/app-debug"); + var Workspace = require("./routers/router-debug"); + new Workspace(); + Backbone.history.start(); + new app(); +}); + +define("examples/todo/1.0.0/views/app-debug", [ "$-debug", "underscore-debug", "backbone-debug", "examples/todo/1.0.0/views/todos-debug", "examples/todo/1.0.0/common-debug", "examples/todo/1.0.0/collections/todos-debug", "examples/todo/1.0.0/models/todo-debug" ], function(require, exports, module) { + var Backbone, TodoView, todos, common, AppView; + var $ = require("$-debug"); + var _ = require("underscore-debug"); + Backbone = require("backbone-debug"); + TodoView = require("examples/todo/1.0.0/views/todos-debug"); + todos = require("examples/todo/1.0.0/collections/todos-debug"); + common = require("examples/todo/1.0.0/common-debug"); + AppView = Backbone.View.extend({ + el: "#todoapp", + statsTemplate: _.template($("#stats-template").html()), + events: { + "keypress #new-todo": "createOnEnter", + "click #clear-completed": "clearCompleted", + "click #toggle-all": "toggleAllComplete" + }, + initialize: function() { + this.allCheckbox = this.$("#toggle-all")[0]; + this.$input = this.$("#new-todo"); + this.$footer = this.$("#footer"); + this.$main = this.$("#main"); + this.listenTo(todos, "add", this.addOne); + this.listenTo(todos, "reset", this.addAll); + this.listenTo(todos, "change:completed", this.filterOne); + this.listenTo(todos, "filter", this.filterAll); + this.listenTo(todos, "all", this.render); + todos.fetch(); + }, + render: function() { + var completed = todos.completed().length; + var remaining = todos.remaining().length; + if (todos.length) { + this.$main.show(); + this.$footer.show(); + this.$footer.html(this.statsTemplate({ + completed: completed, + remaining: remaining + })); + this.$("#filters li a").removeClass("selected").filter('[href="#/' + (common.TodoFilter || "") + '"]').addClass("selected"); + } else { + this.$main.hide(); + this.$footer.hide(); + } + this.allCheckbox.checked = !remaining; + }, + addOne: function(todo) { + var view = new TodoView({ + model: todo + }); + $("#todo-list").append(view.render().el); + }, + addAll: function() { + this.$("#todo-list").html(""); + todos.each(this.addOne, this); + }, + filterOne: function(todo) { + todo.trigger("visible"); + }, + filterAll: function() { + todos.each(this.filterOne, this); + }, + newAttributes: function() { + return { + title: this.$input.val().trim(), + order: todos.nextOrder(), + completed: false + }; + }, + createOnEnter: function(e) { + if (e.which !== common.ENTER_KEY || !this.$input.val().trim()) { + return; + } + todos.create(this.newAttributes()); + this.$input.val(""); + }, + clearCompleted: function() { + _.invoke(todos.completed(), "destroy"); + return false; + }, + toggleAllComplete: function() { + var completed = this.allCheckbox.checked; + todos.each(function(todo) { + todo.save({ + completed: completed + }); + }); + } + }); + module.exports = AppView; +}); + +define("examples/todo/1.0.0/views/todos-debug", [ "backbone-debug", "examples/todo/1.0.0/common-debug", "$-debug", "underscore-debug" ], function(require, exports, module) { + var Backbone, common, TodoView; + Backbone = require("backbone-debug"); + common = require("examples/todo/1.0.0/common-debug"); + var $ = require("$-debug"); + var _ = require("underscore-debug"); + TodoView = Backbone.View.extend({ + tagName: "li", + template: _.template($("#item-template").html()), + events: { + "click .toggle": "toggleCompleted", + "dblclick label": "edit", + "click .destroy": "clear", + "keypress .edit": "updateOnEnter", + "blur .edit": "close" + }, + initialize: function() { + this.listenTo(this.model, "change", this.render); + this.listenTo(this.model, "destroy", this.remove); + this.listenTo(this.model, "visible", this.toggleVisible); + }, + render: function() { + this.$el.html(this.template(this.model.toJSON())); + this.$el.toggleClass("completed", this.model.get("completed")); + this.toggleVisible(); + this.$input = this.$(".edit"); + return this; + }, + toggleVisible: function() { + this.$el.toggleClass("hidden", this.isHidden()); + }, + isHidden: function() { + var isCompleted = this.model.get("completed"); + return; + // hidden cases only + !isCompleted && common.TodoFilter === "completed" || isCompleted && common.TodoFilter === "active"; + }, + toggleCompleted: function() { + this.model.toggle(); + }, + edit: function() { + this.$el.addClass("editing"); + this.$input.focus(); + }, + close: function() { + var value = this.$input.val().trim(); + if (value) { + this.model.save({ + title: value + }); + } else { + this.clear(); + } + this.$el.removeClass("editing"); + }, + updateOnEnter: function(e) { + if (e.which === common.ENTER_KEY) { + this.close(); + } + }, + clear: function() { + this.model.destroy(); + } + }); + module.exports = TodoView; +}); + +define("examples/todo/1.0.0/common-debug", [], { + // Which filter are we using? + TodoFilter: "", + // empty, active, completed + // What is the enter key constant? + ENTER_KEY: 13 +}); + +define("examples/todo/1.0.0/collections/todos-debug", [ "backbone-debug", "$-debug", "underscore-debug", "examples/todo/1.0.0/models/todo-debug" ], function(require, exports, module) { + var Backbone, TodoModel, TodosCollection; + Backbone = require("backbone-debug"); + require("examples/todo/1.0.0/vendor/backbone.localStorage-debug"); + var $ = require("$-debug"); + var _ = require("underscore-debug"); + TodoModel = require("examples/todo/1.0.0/models/todo-debug"); + TodosCollection = Backbone.Collection.extend({ + model: TodoModel, + localStorage: new Backbone.LocalStorage("todos-backbone"), + completed: function() { + return this.filter(function(todo) { + return todo.get("completed"); + }); + }, + remaining: function() { + return this.without.apply(this, this.completed()); + }, + nextOrder: function() { + if (!this.length) { + return 1; + } + return this.last().get("order") + 1; + }, + comparator: function(todo) { + return todo.get("order"); + } + }); + module.exports = new TodosCollection(); +}); + +/** + * Backbone localStorage Adapter + * Version 1.1.0 + * + * https://github.com/jeromegn/Backbone.localStorage + */ +define("examples/todo/1.0.0/vendor/backbone.localStorage-debug", [ "underscore-debug", "$-debug", "backbone-debug" ], function(require) { + var _ = require("underscore-debug"); + var $ = require("$-debug"); + var Backbone = require("backbone-debug"); + // A simple module to replace `Backbone.sync` with *localStorage*-based + // persistence. Models are given GUIDS, and saved into a JSON object. Simple + // as that. + // Hold reference to Underscore.js and Backbone.js in the closure in order + // to make things work even if they are removed from the global namespace + // Generate four random hex digits. + function S4() { + return ((1 + Math.random()) * 65536 | 0).toString(16).substring(1); + } + // Generate a pseudo-GUID by concatenating random hexadecimal. + function guid() { + return S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4(); + } + // Our Store is represented by a single JS object in *localStorage*. Create it + // with a meaningful name, like the name you'd give a table. + // window.Store is deprectated, use Backbone.LocalStorage instead + Backbone.LocalStorage = window.Store = function(name) { + this.name = name; + var store = this.localStorage().getItem(this.name); + this.records = store && store.split(",") || []; + }; + _.extend(Backbone.LocalStorage.prototype, { + // Save the current state of the **Store** to *localStorage*. + save: function() { + this.localStorage().setItem(this.name, this.records.join(",")); + }, + // Add a model, giving it a (hopefully)-unique GUID, if it doesn't already + // have an id of it's own. + create: function(model) { + if (!model.id) { + model.id = guid(); + model.set(model.idAttribute, model.id); + } + this.localStorage().setItem(this.name + "-" + model.id, JSON.stringify(model)); + this.records.push(model.id.toString()); + this.save(); + return this.find(model); + }, + // Update a model by replacing its copy in `this.data`. + update: function(model) { + this.localStorage().setItem(this.name + "-" + model.id, JSON.stringify(model)); + if (!_.include(this.records, model.id.toString())) this.records.push(model.id.toString()); + this.save(); + return this.find(model); + }, + // Retrieve a model from `this.data` by id. + find: function(model) { + return this.jsonData(this.localStorage().getItem(this.name + "-" + model.id)); + }, + // Return the array of all models currently in storage. + findAll: function() { + return _(this.records).chain().map(function(id) { + return this.jsonData(this.localStorage().getItem(this.name + "-" + id)); + }, this).compact().value(); + }, + // Delete a model from `this.data`, returning it. + destroy: function(model) { + if (model.isNew()) return false; + this.localStorage().removeItem(this.name + "-" + model.id); + this.records = _.reject(this.records, function(id) { + return id === model.id.toString(); + }); + this.save(); + return model; + }, + localStorage: function() { + return localStorage; + }, + // fix for "illegal access" error on Android when JSON.parse is passed null + jsonData: function(data) { + return data && JSON.parse(data); + } + }); + // localSync delegate to the model or collection's + // *localStorage* property, which should be an instance of `Store`. + // window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead + Backbone.LocalStorage.sync = window.Store.sync = Backbone.localSync = function(method, model, options) { + var store = model.localStorage || model.collection.localStorage; + var resp, errorMessage, syncDfd = $.Deferred && $.Deferred(); + //If $ is having Deferred - use it. + try { + switch (method) { + case "read": + resp = model.id != undefined ? store.find(model) : store.findAll(); + break; + + case "create": + resp = store.create(model); + break; + + case "update": + resp = store.update(model); + break; + + case "delete": + resp = store.destroy(model); + break; + } + } catch (error) { + if (error.code === DOMException.QUOTA_EXCEEDED_ERR && window.localStorage.length === 0) errorMessage = "Private browsing is unsupported"; else errorMessage = error.message; + } + if (resp) { + if (options && options.success) if (Backbone.VERSION === "0.9.10") { + options.success(model, resp, options); + } else { + options.success(resp); + } + if (syncDfd) syncDfd.resolve(resp); + } else { + errorMessage = errorMessage ? errorMessage : "Record Not Found"; + if (options && options.error) if (Backbone.VERSION === "0.9.10") { + options.error(model, errorMessage, options); + } else { + options.error(errorMessage); + } + if (syncDfd) syncDfd.reject(errorMessage); + } + // add compatibility with $.ajax + // always execute callback for success and error + if (options && options.complete) options.complete(resp); + return syncDfd && syncDfd.promise(); + }; + Backbone.ajaxSync = Backbone.sync; + Backbone.getSyncMethod = function(model) { + if (model.localStorage || model.collection && model.collection.localStorage) { + return Backbone.localSync; + } + return Backbone.ajaxSync; + }; + // Override 'Backbone.sync' to default to localSync, + // the original 'Backbone.sync' is still available in 'Backbone.ajaxSync' + Backbone.sync = function(method, model, options) { + return Backbone.getSyncMethod(model).apply(this, [ method, model, options ]); + }; + return Backbone.LocalStorage; +}); + +define("examples/todo/1.0.0/models/todo-debug", [ "backbone-debug" ], function(require, exports, module) { + var Backbone = require("backbone-debug"); + var TodoModel = Backbone.Model.extend({ + defaults: { + title: "", + completed: false + }, + toggle: function() { + this.save({ + completed: !this.get("completed") + }); + } + }); + module.exports = TodoModel; +}); + +define("examples/todo/1.0.0/routers/router-debug", [ "backbone-debug", "examples/todo/1.0.0/collections/todos-debug", "$-debug", "underscore-debug", "examples/todo/1.0.0/models/todo-debug", "examples/todo/1.0.0/common-debug" ], function(require, exports, module) { + var Backbone, Workspace, todos, common; + Backbone = require("backbone-debug"); + todos = require("examples/todo/1.0.0/collections/todos-debug"); + common = require("examples/todo/1.0.0/common-debug"); + Workspace = Backbone.Router.extend({ + routes: { + "*filter": "setFilter" + }, + setFilter: function(param) { + // Set the current filter to be used + common.TodoFilter = param && param.trim() || ""; + // Trigger a collection filter event, causing hiding/unhiding + // of Todo view items + todos.trigger("filter"); + } + }); + module.exports = Workspace; +}); diff --git a/sea-modules/examples/todo/1.0.0/main.js b/sea-modules/examples/todo/1.0.0/main.js new file mode 100644 index 0000000..4e39297 --- /dev/null +++ b/sea-modules/examples/todo/1.0.0/main.js @@ -0,0 +1 @@ +define("examples/todo/1.0.0/main",["backbone","./views/app","$","underscore","./views/todos","./common","./collections/todos","./vendor/backbone.localStorage","./models/todo","./routers/router"],function(a){var b=a("backbone"),c=a("./views/app"),d=a("./routers/router");new d,b.history.start(),new c}),define("examples/todo/1.0.0/views/app",["$","underscore","backbone","examples/todo/1.0.0/views/todos","examples/todo/1.0.0/common","examples/todo/1.0.0/collections/todos","examples/todo/1.0.0/models/todo"],function(a,b,c){var d,e,f,g,h,i=a("$"),j=a("underscore");d=a("backbone"),e=a("examples/todo/1.0.0/views/todos"),f=a("examples/todo/1.0.0/collections/todos"),g=a("examples/todo/1.0.0/common"),h=d.View.extend({el:"#todoapp",statsTemplate:j.template(i("#stats-template").html()),events:{"keypress #new-todo":"createOnEnter","click #clear-completed":"clearCompleted","click #toggle-all":"toggleAllComplete"},initialize:function(){this.allCheckbox=this.$("#toggle-all")[0],this.$input=this.$("#new-todo"),this.$footer=this.$("#footer"),this.$main=this.$("#main"),this.listenTo(f,"add",this.addOne),this.listenTo(f,"reset",this.addAll),this.listenTo(f,"change:completed",this.filterOne),this.listenTo(f,"filter",this.filterAll),this.listenTo(f,"all",this.render),f.fetch()},render:function(){var a=f.completed().length,b=f.remaining().length;f.length?(this.$main.show(),this.$footer.show(),this.$footer.html(this.statsTemplate({completed:a,remaining:b})),this.$("#filters li a").removeClass("selected").filter('[href="#/'+(g.TodoFilter||"")+'"]').addClass("selected")):(this.$main.hide(),this.$footer.hide()),this.allCheckbox.checked=!b},addOne:function(a){var b=new e({model:a});i("#todo-list").append(b.render().el)},addAll:function(){this.$("#todo-list").html(""),f.each(this.addOne,this)},filterOne:function(a){a.trigger("visible")},filterAll:function(){f.each(this.filterOne,this)},newAttributes:function(){return{title:this.$input.val().trim(),order:f.nextOrder(),completed:!1}},createOnEnter:function(a){a.which===g.ENTER_KEY&&this.$input.val().trim()&&(f.create(this.newAttributes()),this.$input.val(""))},clearCompleted:function(){return j.invoke(f.completed(),"destroy"),!1},toggleAllComplete:function(){var a=this.allCheckbox.checked;f.each(function(b){b.save({completed:a})})}}),c.exports=h}),define("examples/todo/1.0.0/views/todos",["backbone","examples/todo/1.0.0/common","$","underscore"],function(a,b,c){var d,e,f;d=a("backbone"),e=a("examples/todo/1.0.0/common");var g=a("$"),h=a("underscore");f=d.View.extend({tagName:"li",template:h.template(g("#item-template").html()),events:{"click .toggle":"toggleCompleted","dblclick label":"edit","click .destroy":"clear","keypress .edit":"updateOnEnter","blur .edit":"close"},initialize:function(){this.listenTo(this.model,"change",this.render),this.listenTo(this.model,"destroy",this.remove),this.listenTo(this.model,"visible",this.toggleVisible)},render:function(){return this.$el.html(this.template(this.model.toJSON())),this.$el.toggleClass("completed",this.model.get("completed")),this.toggleVisible(),this.$input=this.$(".edit"),this},toggleVisible:function(){this.$el.toggleClass("hidden",this.isHidden())},isHidden:function(){{this.model.get("completed")}},toggleCompleted:function(){this.model.toggle()},edit:function(){this.$el.addClass("editing"),this.$input.focus()},close:function(){var a=this.$input.val().trim();a?this.model.save({title:a}):this.clear(),this.$el.removeClass("editing")},updateOnEnter:function(a){a.which===e.ENTER_KEY&&this.close()},clear:function(){this.model.destroy()}}),c.exports=f}),define("examples/todo/1.0.0/common",[],{TodoFilter:"",ENTER_KEY:13}),define("examples/todo/1.0.0/collections/todos",["backbone","$","underscore","examples/todo/1.0.0/models/todo"],function(a,b,c){var d,e,f;d=a("backbone"),a("examples/todo/1.0.0/vendor/backbone.localStorage"),a("$"),a("underscore"),e=a("examples/todo/1.0.0/models/todo"),f=d.Collection.extend({model:e,localStorage:new d.LocalStorage("todos-backbone"),completed:function(){return this.filter(function(a){return a.get("completed")})},remaining:function(){return this.without.apply(this,this.completed())},nextOrder:function(){return this.length?this.last().get("order")+1:1},comparator:function(a){return a.get("order")}}),c.exports=new f}),define("examples/todo/1.0.0/vendor/backbone.localStorage",["underscore","$","backbone"],function(a){function b(){return(0|65536*(1+Math.random())).toString(16).substring(1)}function c(){return b()+b()+"-"+b()+"-"+b()+"-"+b()+"-"+b()+b()+b()}var d=a("underscore"),e=a("$"),f=a("backbone");return f.LocalStorage=window.Store=function(a){this.name=a;var b=this.localStorage().getItem(this.name);this.records=b&&b.split(",")||[]},d.extend(f.LocalStorage.prototype,{save:function(){this.localStorage().setItem(this.name,this.records.join(","))},create:function(a){return a.id||(a.id=c(),a.set(a.idAttribute,a.id)),this.localStorage().setItem(this.name+"-"+a.id,JSON.stringify(a)),this.records.push(a.id.toString()),this.save(),this.find(a)},update:function(a){return this.localStorage().setItem(this.name+"-"+a.id,JSON.stringify(a)),d.include(this.records,a.id.toString())||this.records.push(a.id.toString()),this.save(),this.find(a)},find:function(a){return this.jsonData(this.localStorage().getItem(this.name+"-"+a.id))},findAll:function(){return d(this.records).chain().map(function(a){return this.jsonData(this.localStorage().getItem(this.name+"-"+a))},this).compact().value()},destroy:function(a){return a.isNew()?!1:(this.localStorage().removeItem(this.name+"-"+a.id),this.records=d.reject(this.records,function(b){return b===a.id.toString()}),this.save(),a)},localStorage:function(){return localStorage},jsonData:function(a){return a&&JSON.parse(a)}}),f.LocalStorage.sync=window.Store.sync=f.localSync=function(a,b,c){var d,g,h=b.localStorage||b.collection.localStorage,i=e.Deferred&&e.Deferred();try{switch(a){case"read":d=void 0!=b.id?h.find(b):h.findAll();break;case"create":d=h.create(b);break;case"update":d=h.update(b);break;case"delete":d=h.destroy(b)}}catch(j){g=j.code===DOMException.QUOTA_EXCEEDED_ERR&&0===window.localStorage.length?"Private browsing is unsupported":j.message}return d?(c&&c.success&&("0.9.10"===f.VERSION?c.success(b,d,c):c.success(d)),i&&i.resolve(d)):(g=g?g:"Record Not Found",c&&c.error&&("0.9.10"===f.VERSION?c.error(b,g,c):c.error(g)),i&&i.reject(g)),c&&c.complete&&c.complete(d),i&&i.promise()},f.ajaxSync=f.sync,f.getSyncMethod=function(a){return a.localStorage||a.collection&&a.collection.localStorage?f.localSync:f.ajaxSync},f.sync=function(a,b,c){return f.getSyncMethod(b).apply(this,[a,b,c])},f.LocalStorage}),define("examples/todo/1.0.0/models/todo",["backbone"],function(a,b,c){var d=a("backbone"),e=d.Model.extend({defaults:{title:"",completed:!1},toggle:function(){this.save({completed:!this.get("completed")})}});c.exports=e}),define("examples/todo/1.0.0/routers/router",["backbone","examples/todo/1.0.0/collections/todos","$","underscore","examples/todo/1.0.0/models/todo","examples/todo/1.0.0/common"],function(a,b,c){var d,e,f,g;d=a("backbone"),f=a("examples/todo/1.0.0/collections/todos"),g=a("examples/todo/1.0.0/common"),e=d.Router.extend({routes:{"*filter":"setFilter"},setFilter:function(a){g.TodoFilter=a&&a.trim()||"",f.trigger("filter")}}),c.exports=e}); diff --git a/sea-modules/gallery/backbone/1.0.0/backbone-debug.js b/sea-modules/gallery/backbone/1.0.0/backbone-debug.js new file mode 100644 index 0000000..80e0392 --- /dev/null +++ b/sea-modules/gallery/backbone/1.0.0/backbone-debug.js @@ -0,0 +1,1404 @@ +define("gallery/backbone/1.0.0/backbone-debug", [ "gallery/underscore/1.4.4/underscore", "$" ], function(require, exports) { + var previousUnderscore = this._; + var previousJQuery = this.jQuery; + this._ = require("gallery/underscore/1.4.4/underscore"); + this.jQuery = require("$"); + // Backbone.js 1.0.0 + // (c) 2010-2013 Jeremy Ashkenas, DocumentCloud Inc. + // Backbone may be freely distributed under the MIT license. + // For all details and documentation: + // http://backbonejs.org + (function() { + // Initial Setup + // ------------- + // Save a reference to the global object (`window` in the browser, `exports` + // on the server). + var root = this; + // Save the previous value of the `Backbone` variable, so that it can be + // restored later on, if `noConflict` is used. + var previousBackbone = root.Backbone; + // Create local references to array methods we'll want to use later. + var array = []; + var push = array.push; + var slice = array.slice; + var splice = array.splice; + // The top-level namespace. All public Backbone classes and modules will + // be attached to this. Exported for both the browser and the server. + var Backbone; + if (typeof exports !== "undefined") { + Backbone = exports; + } else { + Backbone = root.Backbone = {}; + } + // Current version of the library. Keep in sync with `package.json`. + Backbone.VERSION = "1.0.0"; + // Require Underscore, if we're on the server, and it's not already present. + var _ = root._; + if (!_ && typeof require !== "undefined") _ = require("gallery/underscore/1.4.4/underscore"); + // For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns + // the `$` variable. + Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$; + // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable + // to its previous owner. Returns a reference to this Backbone object. + Backbone.noConflict = function() { + root.Backbone = previousBackbone; + return this; + }; + // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option + // will fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and + // set a `X-Http-Method-Override` header. + Backbone.emulateHTTP = false; + // Turn on `emulateJSON` to support legacy servers that can't deal with direct + // `application/json` requests ... will encode the body as + // `application/x-www-form-urlencoded` instead and will send the model in a + // form param named `model`. + Backbone.emulateJSON = false; + // Backbone.Events + // --------------- + // A module that can be mixed in to *any object* in order to provide it with + // custom events. You may bind with `on` or remove with `off` callback + // functions to an event; `trigger`-ing an event fires all callbacks in + // succession. + // + // var object = {}; + // _.extend(object, Backbone.Events); + // object.on('expand', function(){ alert('expanded'); }); + // object.trigger('expand'); + // + var Events = Backbone.Events = { + // Bind an event to a `callback` function. Passing `"all"` will bind + // the callback to all events fired. + on: function(name, callback, context) { + if (!eventsApi(this, "on", name, [ callback, context ]) || !callback) return this; + this._events || (this._events = {}); + var events = this._events[name] || (this._events[name] = []); + events.push({ + callback: callback, + context: context, + ctx: context || this + }); + return this; + }, + // Bind an event to only be triggered a single time. After the first time + // the callback is invoked, it will be removed. + once: function(name, callback, context) { + if (!eventsApi(this, "once", name, [ callback, context ]) || !callback) return this; + var self = this; + var once = _.once(function() { + self.off(name, once); + callback.apply(this, arguments); + }); + once._callback = callback; + return this.on(name, once, context); + }, + // Remove one or many callbacks. If `context` is null, removes all + // callbacks with that function. If `callback` is null, removes all + // callbacks for the event. If `name` is null, removes all bound + // callbacks for all events. + off: function(name, callback, context) { + var retain, ev, events, names, i, l, j, k; + if (!this._events || !eventsApi(this, "off", name, [ callback, context ])) return this; + if (!name && !callback && !context) { + this._events = {}; + return this; + } + names = name ? [ name ] : _.keys(this._events); + for (i = 0, l = names.length; i < l; i++) { + name = names[i]; + if (events = this._events[name]) { + this._events[name] = retain = []; + if (callback || context) { + for (j = 0, k = events.length; j < k; j++) { + ev = events[j]; + if (callback && callback !== ev.callback && callback !== ev.callback._callback || context && context !== ev.context) { + retain.push(ev); + } + } + } + if (!retain.length) delete this._events[name]; + } + } + return this; + }, + // Trigger one or many events, firing all bound callbacks. Callbacks are + // passed the same arguments as `trigger` is, apart from the event name + // (unless you're listening on `"all"`, which will cause your callback to + // receive the true name of the event as the first argument). + trigger: function(name) { + if (!this._events) return this; + var args = slice.call(arguments, 1); + if (!eventsApi(this, "trigger", name, args)) return this; + var events = this._events[name]; + var allEvents = this._events.all; + if (events) triggerEvents(events, args); + if (allEvents) triggerEvents(allEvents, arguments); + return this; + }, + // Tell this object to stop listening to either specific events ... or + // to every object it's currently listening to. + stopListening: function(obj, name, callback) { + var listeners = this._listeners; + if (!listeners) return this; + var deleteListener = !name && !callback; + if (typeof name === "object") callback = this; + if (obj) (listeners = {})[obj._listenerId] = obj; + for (var id in listeners) { + listeners[id].off(name, callback, this); + if (deleteListener) delete this._listeners[id]; + } + return this; + } + }; + // Regular expression used to split event strings. + var eventSplitter = /\s+/; + // Implement fancy features of the Events API such as multiple event + // names `"change blur"` and jQuery-style event maps `{change: action}` + // in terms of the existing API. + var eventsApi = function(obj, action, name, rest) { + if (!name) return true; + // Handle event maps. + if (typeof name === "object") { + for (var key in name) { + obj[action].apply(obj, [ key, name[key] ].concat(rest)); + } + return false; + } + // Handle space separated event names. + if (eventSplitter.test(name)) { + var names = name.split(eventSplitter); + for (var i = 0, l = names.length; i < l; i++) { + obj[action].apply(obj, [ names[i] ].concat(rest)); + } + return false; + } + return true; + }; + // A difficult-to-believe, but optimized internal dispatch function for + // triggering events. Tries to keep the usual cases speedy (most internal + // Backbone events have 3 arguments). + var triggerEvents = function(events, args) { + var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; + switch (args.length) { + case 0: + while (++i < l) (ev = events[i]).callback.call(ev.ctx); + return; + + case 1: + while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); + return; + + case 2: + while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); + return; + + case 3: + while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); + return; + + default: + while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); + } + }; + var listenMethods = { + listenTo: "on", + listenToOnce: "once" + }; + // Inversion-of-control versions of `on` and `once`. Tell *this* object to + // listen to an event in another object ... keeping track of what it's + // listening to. + _.each(listenMethods, function(implementation, method) { + Events[method] = function(obj, name, callback) { + var listeners = this._listeners || (this._listeners = {}); + var id = obj._listenerId || (obj._listenerId = _.uniqueId("l")); + listeners[id] = obj; + if (typeof name === "object") callback = this; + obj[implementation](name, callback, this); + return this; + }; + }); + // Aliases for backwards compatibility. + Events.bind = Events.on; + Events.unbind = Events.off; + // Allow the `Backbone` object to serve as a global event bus, for folks who + // want global "pubsub" in a convenient place. + _.extend(Backbone, Events); + // Backbone.Model + // -------------- + // Backbone **Models** are the basic data object in the framework -- + // frequently representing a row in a table in a database on your server. + // A discrete chunk of data and a bunch of useful, related methods for + // performing computations and transformations on that data. + // Create a new model with the specified attributes. A client id (`cid`) + // is automatically generated and assigned for you. + var Model = Backbone.Model = function(attributes, options) { + var defaults; + var attrs = attributes || {}; + options || (options = {}); + this.cid = _.uniqueId("c"); + this.attributes = {}; + _.extend(this, _.pick(options, modelOptions)); + if (options.parse) attrs = this.parse(attrs, options) || {}; + if (defaults = _.result(this, "defaults")) { + attrs = _.defaults({}, attrs, defaults); + } + this.set(attrs, options); + this.changed = {}; + this.initialize.apply(this, arguments); + }; + // A list of options to be attached directly to the model, if provided. + var modelOptions = [ "url", "urlRoot", "collection" ]; + // Attach all inheritable methods to the Model prototype. + _.extend(Model.prototype, Events, { + // A hash of attributes whose current and previous value differ. + changed: null, + // The value returned during the last failed validation. + validationError: null, + // The default name for the JSON `id` attribute is `"id"`. MongoDB and + // CouchDB users may want to set this to `"_id"`. + idAttribute: "id", + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function() {}, + // Return a copy of the model's `attributes` object. + toJSON: function(options) { + return _.clone(this.attributes); + }, + // Proxy `Backbone.sync` by default -- but override this if you need + // custom syncing semantics for *this* particular model. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + // Get the value of an attribute. + get: function(attr) { + return this.attributes[attr]; + }, + // Get the HTML-escaped value of an attribute. + escape: function(attr) { + return _.escape(this.get(attr)); + }, + // Returns `true` if the attribute contains a value that is not null + // or undefined. + has: function(attr) { + return this.get(attr) != null; + }, + // Set a hash of model attributes on the object, firing `"change"`. This is + // the core primitive operation of a model, updating the data and notifying + // anyone who needs to know about the change in state. The heart of the beast. + set: function(key, val, options) { + var attr, attrs, unset, changes, silent, changing, prev, current; + if (key == null) return this; + // Handle both `"key", value` and `{key: value}` -style arguments. + if (typeof key === "object") { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + options || (options = {}); + // Run validation. + if (!this._validate(attrs, options)) return false; + // Extract attributes and options. + unset = options.unset; + silent = options.silent; + changes = []; + changing = this._changing; + this._changing = true; + if (!changing) { + this._previousAttributes = _.clone(this.attributes); + this.changed = {}; + } + current = this.attributes, prev = this._previousAttributes; + // Check for changes of `id`. + if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; + // For each `set` attribute, update or delete the current value. + for (attr in attrs) { + val = attrs[attr]; + if (!_.isEqual(current[attr], val)) changes.push(attr); + if (!_.isEqual(prev[attr], val)) { + this.changed[attr] = val; + } else { + delete this.changed[attr]; + } + unset ? delete current[attr] : current[attr] = val; + } + // Trigger all relevant attribute changes. + if (!silent) { + if (changes.length) this._pending = true; + for (var i = 0, l = changes.length; i < l; i++) { + this.trigger("change:" + changes[i], this, current[changes[i]], options); + } + } + // You might be wondering why there's a `while` loop here. Changes can + // be recursively nested within `"change"` events. + if (changing) return this; + if (!silent) { + while (this._pending) { + this._pending = false; + this.trigger("change", this, options); + } + } + this._pending = false; + this._changing = false; + return this; + }, + // Remove an attribute from the model, firing `"change"`. `unset` is a noop + // if the attribute doesn't exist. + unset: function(attr, options) { + return this.set(attr, void 0, _.extend({}, options, { + unset: true + })); + }, + // Clear all attributes on the model, firing `"change"`. + clear: function(options) { + var attrs = {}; + for (var key in this.attributes) attrs[key] = void 0; + return this.set(attrs, _.extend({}, options, { + unset: true + })); + }, + // Determine if the model has changed since the last `"change"` event. + // If you specify an attribute name, determine if that attribute has changed. + hasChanged: function(attr) { + if (attr == null) return !_.isEmpty(this.changed); + return _.has(this.changed, attr); + }, + // Return an object containing all the attributes that have changed, or + // false if there are no changed attributes. Useful for determining what + // parts of a view need to be updated and/or what attributes need to be + // persisted to the server. Unset attributes will be set to undefined. + // You can also pass an attributes object to diff against the model, + // determining if there *would be* a change. + changedAttributes: function(diff) { + if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; + var val, changed = false; + var old = this._changing ? this._previousAttributes : this.attributes; + for (var attr in diff) { + if (_.isEqual(old[attr], val = diff[attr])) continue; + (changed || (changed = {}))[attr] = val; + } + return changed; + }, + // Get the previous value of an attribute, recorded at the time the last + // `"change"` event was fired. + previous: function(attr) { + if (attr == null || !this._previousAttributes) return null; + return this._previousAttributes[attr]; + }, + // Get all of the attributes of the model at the time of the previous + // `"change"` event. + previousAttributes: function() { + return _.clone(this._previousAttributes); + }, + // Fetch the model from the server. If the server's representation of the + // model differs from its current attributes, they will be overridden, + // triggering a `"change"` event. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + if (!model.set(model.parse(resp, options), options)) return false; + if (success) success(model, resp, options); + model.trigger("sync", model, resp, options); + }; + wrapError(this, options); + return this.sync("read", this, options); + }, + // Set a hash of model attributes, and sync the model to the server. + // If the server returns an attributes hash that differs, the model's + // state will be `set` again. + save: function(key, val, options) { + var attrs, method, xhr, attributes = this.attributes; + // Handle both `"key", value` and `{key: value}` -style arguments. + if (key == null || typeof key === "object") { + attrs = key; + options = val; + } else { + (attrs = {})[key] = val; + } + // If we're not waiting and attributes exist, save acts as `set(attr).save(null, opts)`. + if (attrs && (!options || !options.wait) && !this.set(attrs, options)) return false; + options = _.extend({ + validate: true + }, options); + // Do not persist invalid models. + if (!this._validate(attrs, options)) return false; + // Set temporary attributes if `{wait: true}`. + if (attrs && options.wait) { + this.attributes = _.extend({}, attributes, attrs); + } + // After a successful server-side save, the client is (optionally) + // updated with the server-side state. + if (options.parse === void 0) options.parse = true; + var model = this; + var success = options.success; + options.success = function(resp) { + // Ensure attributes are restored during synchronous saves. + model.attributes = attributes; + var serverAttrs = model.parse(resp, options); + if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); + if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { + return false; + } + if (success) success(model, resp, options); + model.trigger("sync", model, resp, options); + }; + wrapError(this, options); + method = this.isNew() ? "create" : options.patch ? "patch" : "update"; + if (method === "patch") options.attrs = attrs; + xhr = this.sync(method, this, options); + // Restore attributes. + if (attrs && options.wait) this.attributes = attributes; + return xhr; + }, + // Destroy this model on the server if it was already persisted. + // Optimistically removes the model from its collection, if it has one. + // If `wait: true` is passed, waits for the server to respond before removal. + destroy: function(options) { + options = options ? _.clone(options) : {}; + var model = this; + var success = options.success; + var destroy = function() { + model.trigger("destroy", model, model.collection, options); + }; + options.success = function(resp) { + if (options.wait || model.isNew()) destroy(); + if (success) success(model, resp, options); + if (!model.isNew()) model.trigger("sync", model, resp, options); + }; + if (this.isNew()) { + options.success(); + return false; + } + wrapError(this, options); + var xhr = this.sync("delete", this, options); + if (!options.wait) destroy(); + return xhr; + }, + // Default URL for the model's representation on the server -- if you're + // using Backbone's restful methods, override this to change the endpoint + // that will be called. + url: function() { + var base = _.result(this, "urlRoot") || _.result(this.collection, "url") || urlError(); + if (this.isNew()) return base; + return base + (base.charAt(base.length - 1) === "/" ? "" : "/") + encodeURIComponent(this.id); + }, + // **parse** converts a response into the hash of attributes to be `set` on + // the model. The default implementation is just to pass the response along. + parse: function(resp, options) { + return resp; + }, + // Create a new model with identical attributes to this one. + clone: function() { + return new this.constructor(this.attributes); + }, + // A model is new if it has never been saved to the server, and lacks an id. + isNew: function() { + return this.id == null; + }, + // Check if the model is currently in a valid state. + isValid: function(options) { + return this._validate({}, _.extend(options || {}, { + validate: true + })); + }, + // Run validation against the next complete set of model attributes, + // returning `true` if all is well. Otherwise, fire an `"invalid"` event. + _validate: function(attrs, options) { + if (!options.validate || !this.validate) return true; + attrs = _.extend({}, this.attributes, attrs); + var error = this.validationError = this.validate(attrs, options) || null; + if (!error) return true; + this.trigger("invalid", this, error, _.extend(options || {}, { + validationError: error + })); + return false; + } + }); + // Underscore methods that we want to implement on the Model. + var modelMethods = [ "keys", "values", "pairs", "invert", "pick", "omit" ]; + // Mix in each Underscore method as a proxy to `Model#attributes`. + _.each(modelMethods, function(method) { + Model.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.attributes); + return _[method].apply(_, args); + }; + }); + // Backbone.Collection + // ------------------- + // If models tend to represent a single row of data, a Backbone Collection is + // more analagous to a table full of data ... or a small slice or page of that + // table, or a collection of rows that belong together for a particular reason + // -- all of the messages in this particular folder, all of the documents + // belonging to this particular author, and so on. Collections maintain + // indexes of their models, both in order, and for lookup by `id`. + // Create a new **Collection**, perhaps to contain a specific type of `model`. + // If a `comparator` is specified, the Collection will maintain + // its models in sort order, as they're added and removed. + var Collection = Backbone.Collection = function(models, options) { + options || (options = {}); + if (options.url) this.url = options.url; + if (options.model) this.model = options.model; + if (options.comparator !== void 0) this.comparator = options.comparator; + this._reset(); + this.initialize.apply(this, arguments); + if (models) this.reset(models, _.extend({ + silent: true + }, options)); + }; + // Default options for `Collection#set`. + var setOptions = { + add: true, + remove: true, + merge: true + }; + var addOptions = { + add: true, + merge: false, + remove: false + }; + // Define the Collection's inheritable methods. + _.extend(Collection.prototype, Events, { + // The default model for a collection is just a **Backbone.Model**. + // This should be overridden in most cases. + model: Model, + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function() {}, + // The JSON representation of a Collection is an array of the + // models' attributes. + toJSON: function(options) { + return this.map(function(model) { + return model.toJSON(options); + }); + }, + // Proxy `Backbone.sync` by default. + sync: function() { + return Backbone.sync.apply(this, arguments); + }, + // Add a model, or list of models to the set. + add: function(models, options) { + return this.set(models, _.defaults(options || {}, addOptions)); + }, + // Remove a model, or a list of models from the set. + remove: function(models, options) { + models = _.isArray(models) ? models.slice() : [ models ]; + options || (options = {}); + var i, l, index, model; + for (i = 0, l = models.length; i < l; i++) { + model = this.get(models[i]); + if (!model) continue; + delete this._byId[model.id]; + delete this._byId[model.cid]; + index = this.indexOf(model); + this.models.splice(index, 1); + this.length--; + if (!options.silent) { + options.index = index; + model.trigger("remove", model, this, options); + } + this._removeReference(model); + } + return this; + }, + // Update a collection by `set`-ing a new list of models, adding new ones, + // removing models that are no longer present, and merging models that + // already exist in the collection, as necessary. Similar to **Model#set**, + // the core operation for updating the data contained by the collection. + set: function(models, options) { + options = _.defaults(options || {}, setOptions); + if (options.parse) models = this.parse(models, options); + if (!_.isArray(models)) models = models ? [ models ] : []; + var i, l, model, attrs, existing, sort; + var at = options.at; + var sortable = this.comparator && at == null && options.sort !== false; + var sortAttr = _.isString(this.comparator) ? this.comparator : null; + var toAdd = [], toRemove = [], modelMap = {}; + // Turn bare objects into model references, and prevent invalid models + // from being added. + for (i = 0, l = models.length; i < l; i++) { + if (!(model = this._prepareModel(models[i], options))) continue; + // If a duplicate is found, prevent it from being added and + // optionally merge it into the existing model. + if (existing = this.get(model)) { + if (options.remove) modelMap[existing.cid] = true; + if (options.merge) { + existing.set(model.attributes, options); + if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; + } + } else if (options.add) { + toAdd.push(model); + // Listen to added models' events, and index models for lookup by + // `id` and by `cid`. + model.on("all", this._onModelEvent, this); + this._byId[model.cid] = model; + if (model.id != null) this._byId[model.id] = model; + } + } + // Remove nonexistent models if appropriate. + if (options.remove) { + for (i = 0, l = this.length; i < l; ++i) { + if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); + } + if (toRemove.length) this.remove(toRemove, options); + } + // See if sorting is needed, update `length` and splice in new models. + if (toAdd.length) { + if (sortable) sort = true; + this.length += toAdd.length; + if (at != null) { + splice.apply(this.models, [ at, 0 ].concat(toAdd)); + } else { + push.apply(this.models, toAdd); + } + } + // Silently sort the collection if appropriate. + if (sort) this.sort({ + silent: true + }); + if (options.silent) return this; + // Trigger `add` events. + for (i = 0, l = toAdd.length; i < l; i++) { + (model = toAdd[i]).trigger("add", model, this, options); + } + // Trigger `sort` if the collection was sorted. + if (sort) this.trigger("sort", this, options); + return this; + }, + // When you have more items than you want to add or remove individually, + // you can reset the entire set with a new list of models, without firing + // any granular `add` or `remove` events. Fires `reset` when finished. + // Useful for bulk operations and optimizations. + reset: function(models, options) { + options || (options = {}); + for (var i = 0, l = this.models.length; i < l; i++) { + this._removeReference(this.models[i]); + } + options.previousModels = this.models; + this._reset(); + this.add(models, _.extend({ + silent: true + }, options)); + if (!options.silent) this.trigger("reset", this, options); + return this; + }, + // Add a model to the end of the collection. + push: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({ + at: this.length + }, options)); + return model; + }, + // Remove a model from the end of the collection. + pop: function(options) { + var model = this.at(this.length - 1); + this.remove(model, options); + return model; + }, + // Add a model to the beginning of the collection. + unshift: function(model, options) { + model = this._prepareModel(model, options); + this.add(model, _.extend({ + at: 0 + }, options)); + return model; + }, + // Remove a model from the beginning of the collection. + shift: function(options) { + var model = this.at(0); + this.remove(model, options); + return model; + }, + // Slice out a sub-array of models from the collection. + slice: function(begin, end) { + return this.models.slice(begin, end); + }, + // Get a model from the set by id. + get: function(obj) { + if (obj == null) return void 0; + return this._byId[obj.id != null ? obj.id : obj.cid || obj]; + }, + // Get the model at the given index. + at: function(index) { + return this.models[index]; + }, + // Return models with matching attributes. Useful for simple cases of + // `filter`. + where: function(attrs, first) { + if (_.isEmpty(attrs)) return first ? void 0 : []; + return this[first ? "find" : "filter"](function(model) { + for (var key in attrs) { + if (attrs[key] !== model.get(key)) return false; + } + return true; + }); + }, + // Return the first model with matching attributes. Useful for simple cases + // of `find`. + findWhere: function(attrs) { + return this.where(attrs, true); + }, + // Force the collection to re-sort itself. You don't need to call this under + // normal circumstances, as the set will maintain sort order as each item + // is added. + sort: function(options) { + if (!this.comparator) throw new Error("Cannot sort a set without a comparator"); + options || (options = {}); + // Run sort based on type of `comparator`. + if (_.isString(this.comparator) || this.comparator.length === 1) { + this.models = this.sortBy(this.comparator, this); + } else { + this.models.sort(_.bind(this.comparator, this)); + } + if (!options.silent) this.trigger("sort", this, options); + return this; + }, + // Figure out the smallest index at which a model should be inserted so as + // to maintain order. + sortedIndex: function(model, value, context) { + value || (value = this.comparator); + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _.sortedIndex(this.models, model, iterator, context); + }, + // Pluck an attribute from each model in the collection. + pluck: function(attr) { + return _.invoke(this.models, "get", attr); + }, + // Fetch the default set of models for this collection, resetting the + // collection when they arrive. If `reset: true` is passed, the response + // data will be passed through the `reset` method instead of `set`. + fetch: function(options) { + options = options ? _.clone(options) : {}; + if (options.parse === void 0) options.parse = true; + var success = options.success; + var collection = this; + options.success = function(resp) { + var method = options.reset ? "reset" : "set"; + collection[method](resp, options); + if (success) success(collection, resp, options); + collection.trigger("sync", collection, resp, options); + }; + wrapError(this, options); + return this.sync("read", this, options); + }, + // Create a new instance of a model in this collection. Add the model to the + // collection immediately, unless `wait: true` is passed, in which case we + // wait for the server to agree. + create: function(model, options) { + options = options ? _.clone(options) : {}; + if (!(model = this._prepareModel(model, options))) return false; + if (!options.wait) this.add(model, options); + var collection = this; + var success = options.success; + options.success = function(resp) { + if (options.wait) collection.add(model, options); + if (success) success(model, resp, options); + }; + model.save(null, options); + return model; + }, + // **parse** converts a response into a list of models to be added to the + // collection. The default implementation is just to pass it through. + parse: function(resp, options) { + return resp; + }, + // Create a new collection with an identical list of models as this one. + clone: function() { + return new this.constructor(this.models); + }, + // Private method to reset all internal state. Called when the collection + // is first initialized or reset. + _reset: function() { + this.length = 0; + this.models = []; + this._byId = {}; + }, + // Prepare a hash of attributes (or other model) to be added to this + // collection. + _prepareModel: function(attrs, options) { + if (attrs instanceof Model) { + if (!attrs.collection) attrs.collection = this; + return attrs; + } + options || (options = {}); + options.collection = this; + var model = new this.model(attrs, options); + if (!model._validate(attrs, options)) { + this.trigger("invalid", this, attrs, options); + return false; + } + return model; + }, + // Internal method to sever a model's ties to a collection. + _removeReference: function(model) { + if (this === model.collection) delete model.collection; + model.off("all", this._onModelEvent, this); + }, + // Internal method called every time a model in the set fires an event. + // Sets need to update their indexes when models change ids. All other + // events simply proxy through. "add" and "remove" events that originate + // in other collections are ignored. + _onModelEvent: function(event, model, collection, options) { + if ((event === "add" || event === "remove") && collection !== this) return; + if (event === "destroy") this.remove(model, options); + if (model && event === "change:" + model.idAttribute) { + delete this._byId[model.previous(model.idAttribute)]; + if (model.id != null) this._byId[model.id] = model; + } + this.trigger.apply(this, arguments); + } + }); + // Underscore methods that we want to implement on the Collection. + // 90% of the core usefulness of Backbone Collections is actually implemented + // right here: + var methods = [ "forEach", "each", "map", "collect", "reduce", "foldl", "inject", "reduceRight", "foldr", "find", "detect", "filter", "select", "reject", "every", "all", "some", "any", "include", "contains", "invoke", "max", "min", "toArray", "size", "first", "head", "take", "initial", "rest", "tail", "drop", "last", "without", "indexOf", "shuffle", "lastIndexOf", "isEmpty", "chain" ]; + // Mix in each Underscore method as a proxy to `Collection#models`. + _.each(methods, function(method) { + Collection.prototype[method] = function() { + var args = slice.call(arguments); + args.unshift(this.models); + return _[method].apply(_, args); + }; + }); + // Underscore methods that take a property name as an argument. + var attributeMethods = [ "groupBy", "countBy", "sortBy" ]; + // Use attributes instead of properties. + _.each(attributeMethods, function(method) { + Collection.prototype[method] = function(value, context) { + var iterator = _.isFunction(value) ? value : function(model) { + return model.get(value); + }; + return _[method](this.models, iterator, context); + }; + }); + // Backbone.View + // ------------- + // Backbone Views are almost more convention than they are actual code. A View + // is simply a JavaScript object that represents a logical chunk of UI in the + // DOM. This might be a single item, an entire list, a sidebar or panel, or + // even the surrounding frame which wraps your whole app. Defining a chunk of + // UI as a **View** allows you to define your DOM events declaratively, without + // having to worry about render order ... and makes it easy for the view to + // react to specific changes in the state of your models. + // Creating a Backbone.View creates its initial element outside of the DOM, + // if an existing element is not provided... + var View = Backbone.View = function(options) { + this.cid = _.uniqueId("view"); + this._configure(options || {}); + this._ensureElement(); + this.initialize.apply(this, arguments); + this.delegateEvents(); + }; + // Cached regex to split keys for `delegate`. + var delegateEventSplitter = /^(\S+)\s*(.*)$/; + // List of view options to be merged as properties. + var viewOptions = [ "model", "collection", "el", "id", "attributes", "className", "tagName", "events" ]; + // Set up all inheritable **Backbone.View** properties and methods. + _.extend(View.prototype, Events, { + // The default `tagName` of a View's element is `"div"`. + tagName: "div", + // jQuery delegate for element lookup, scoped to DOM elements within the + // current view. This should be prefered to global lookups where possible. + $: function(selector) { + return this.$el.find(selector); + }, + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function() {}, + // **render** is the core function that your view should override, in order + // to populate its element (`this.el`), with the appropriate HTML. The + // convention is for **render** to always return `this`. + render: function() { + return this; + }, + // Remove this view by taking the element out of the DOM, and removing any + // applicable Backbone.Events listeners. + remove: function() { + this.$el.remove(); + this.stopListening(); + return this; + }, + // Change the view's element (`this.el` property), including event + // re-delegation. + setElement: function(element, delegate) { + if (this.$el) this.undelegateEvents(); + this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); + this.el = this.$el[0]; + if (delegate !== false) this.delegateEvents(); + return this; + }, + // Set callbacks, where `this.events` is a hash of + // + // *{"event selector": "callback"}* + // + // { + // 'mousedown .title': 'edit', + // 'click .button': 'save' + // 'click .open': function(e) { ... } + // } + // + // pairs. Callbacks will be bound to the view, with `this` set properly. + // Uses event delegation for efficiency. + // Omitting the selector binds the event to `this.el`. + // This only works for delegate-able events: not `focus`, `blur`, and + // not `change`, `submit`, and `reset` in Internet Explorer. + delegateEvents: function(events) { + if (!(events || (events = _.result(this, "events")))) return this; + this.undelegateEvents(); + for (var key in events) { + var method = events[key]; + if (!_.isFunction(method)) method = this[events[key]]; + if (!method) continue; + var match = key.match(delegateEventSplitter); + var eventName = match[1], selector = match[2]; + method = _.bind(method, this); + eventName += ".delegateEvents" + this.cid; + if (selector === "") { + this.$el.on(eventName, method); + } else { + this.$el.on(eventName, selector, method); + } + } + return this; + }, + // Clears all callbacks previously bound to the view with `delegateEvents`. + // You usually don't need to use this, but may wish to if you have multiple + // Backbone views attached to the same DOM element. + undelegateEvents: function() { + this.$el.off(".delegateEvents" + this.cid); + return this; + }, + // Performs the initial configuration of a View with a set of options. + // Keys with special meaning *(e.g. model, collection, id, className)* are + // attached directly to the view. See `viewOptions` for an exhaustive + // list. + _configure: function(options) { + if (this.options) options = _.extend({}, _.result(this, "options"), options); + _.extend(this, _.pick(options, viewOptions)); + this.options = options; + }, + // Ensure that the View has a DOM element to render into. + // If `this.el` is a string, pass it through `$()`, take the first + // matching element, and re-assign it to `el`. Otherwise, create + // an element from the `id`, `className` and `tagName` properties. + _ensureElement: function() { + if (!this.el) { + var attrs = _.extend({}, _.result(this, "attributes")); + if (this.id) attrs.id = _.result(this, "id"); + if (this.className) attrs["class"] = _.result(this, "className"); + var $el = Backbone.$("<" + _.result(this, "tagName") + ">").attr(attrs); + this.setElement($el, false); + } else { + this.setElement(_.result(this, "el"), false); + } + } + }); + // Backbone.sync + // ------------- + // Override this function to change the manner in which Backbone persists + // models to the server. You will be passed the type of request, and the + // model in question. By default, makes a RESTful Ajax request + // to the model's `url()`. Some possible customizations could be: + // + // * Use `setTimeout` to batch rapid-fire updates into a single request. + // * Send up the models as XML instead of JSON. + // * Persist models via WebSockets instead of Ajax. + // + // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests + // as `POST`, with a `_method` parameter containing the true HTTP method, + // as well as all requests with the body as `application/x-www-form-urlencoded` + // instead of `application/json` with the model in a param named `model`. + // Useful when interfacing with server-side languages like **PHP** that make + // it difficult to read the body of `PUT` requests. + Backbone.sync = function(method, model, options) { + var type = methodMap[method]; + // Default options, unless specified. + _.defaults(options || (options = {}), { + emulateHTTP: Backbone.emulateHTTP, + emulateJSON: Backbone.emulateJSON + }); + // Default JSON-request options. + var params = { + type: type, + dataType: "json" + }; + // Ensure that we have a URL. + if (!options.url) { + params.url = _.result(model, "url") || urlError(); + } + // Ensure that we have the appropriate request data. + if (options.data == null && model && (method === "create" || method === "update" || method === "patch")) { + params.contentType = "application/json"; + params.data = JSON.stringify(options.attrs || model.toJSON(options)); + } + // For older servers, emulate JSON by encoding the request into an HTML-form. + if (options.emulateJSON) { + params.contentType = "application/x-www-form-urlencoded"; + params.data = params.data ? { + model: params.data + } : {}; + } + // For older servers, emulate HTTP by mimicking the HTTP method with `_method` + // And an `X-HTTP-Method-Override` header. + if (options.emulateHTTP && (type === "PUT" || type === "DELETE" || type === "PATCH")) { + params.type = "POST"; + if (options.emulateJSON) params.data._method = type; + var beforeSend = options.beforeSend; + options.beforeSend = function(xhr) { + xhr.setRequestHeader("X-HTTP-Method-Override", type); + if (beforeSend) return beforeSend.apply(this, arguments); + }; + } + // Don't process data on a non-GET request. + if (params.type !== "GET" && !options.emulateJSON) { + params.processData = false; + } + // If we're sending a `PATCH` request, and we're in an old Internet Explorer + // that still has ActiveX enabled by default, override jQuery to use that + // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. + if (params.type === "PATCH" && window.ActiveXObject && !(window.external && window.external.msActiveXFilteringEnabled)) { + params.xhr = function() { + return new ActiveXObject("Microsoft.XMLHTTP"); + }; + } + // Make the request, allowing the user to override any Ajax options. + var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); + model.trigger("request", model, xhr, options); + return xhr; + }; + // Map from CRUD to HTTP for our default `Backbone.sync` implementation. + var methodMap = { + create: "POST", + update: "PUT", + patch: "PATCH", + "delete": "DELETE", + read: "GET" + }; + // Set the default implementation of `Backbone.ajax` to proxy through to `$`. + // Override this if you'd like to use a different library. + Backbone.ajax = function() { + return Backbone.$.ajax.apply(Backbone.$, arguments); + }; + // Backbone.Router + // --------------- + // Routers map faux-URLs to actions, and fire events when routes are + // matched. Creating a new one sets its `routes` hash, if not set statically. + var Router = Backbone.Router = function(options) { + options || (options = {}); + if (options.routes) this.routes = options.routes; + this._bindRoutes(); + this.initialize.apply(this, arguments); + }; + // Cached regular expressions for matching named param parts and splatted + // parts of route strings. + var optionalParam = /\((.*?)\)/g; + var namedParam = /(\(\?)?:\w+/g; + var splatParam = /\*\w+/g; + var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g; + // Set up all inheritable **Backbone.Router** properties and methods. + _.extend(Router.prototype, Events, { + // Initialize is an empty function by default. Override it with your own + // initialization logic. + initialize: function() {}, + // Manually bind a single named route to a callback. For example: + // + // this.route('search/:query/p:num', 'search', function(query, num) { + // ... + // }); + // + route: function(route, name, callback) { + if (!_.isRegExp(route)) route = this._routeToRegExp(route); + if (_.isFunction(name)) { + callback = name; + name = ""; + } + if (!callback) callback = this[name]; + var router = this; + Backbone.history.route(route, function(fragment) { + var args = router._extractParameters(route, fragment); + callback && callback.apply(router, args); + router.trigger.apply(router, [ "route:" + name ].concat(args)); + router.trigger("route", name, args); + Backbone.history.trigger("route", router, name, args); + }); + return this; + }, + // Simple proxy to `Backbone.history` to save a fragment into the history. + navigate: function(fragment, options) { + Backbone.history.navigate(fragment, options); + return this; + }, + // Bind all defined routes to `Backbone.history`. We have to reverse the + // order of the routes here to support behavior where the most general + // routes can be defined at the bottom of the route map. + _bindRoutes: function() { + if (!this.routes) return; + this.routes = _.result(this, "routes"); + var route, routes = _.keys(this.routes); + while ((route = routes.pop()) != null) { + this.route(route, this.routes[route]); + } + }, + // Convert a route string into a regular expression, suitable for matching + // against the current location hash. + _routeToRegExp: function(route) { + route = route.replace(escapeRegExp, "\\$&").replace(optionalParam, "(?:$1)?").replace(namedParam, function(match, optional) { + return optional ? match : "([^/]+)"; + }).replace(splatParam, "(.*?)"); + return new RegExp("^" + route + "$"); + }, + // Given a route, and a URL fragment that it matches, return the array of + // extracted decoded parameters. Empty or unmatched parameters will be + // treated as `null` to normalize cross-browser behavior. + _extractParameters: function(route, fragment) { + var params = route.exec(fragment).slice(1); + return _.map(params, function(param) { + return param ? decodeURIComponent(param) : null; + }); + } + }); + // Backbone.History + // ---------------- + // Handles cross-browser history management, based on either + // [pushState](http://diveintohtml5.info/history.html) and real URLs, or + // [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange) + // and URL fragments. If the browser supports neither (old IE, natch), + // falls back to polling. + var History = Backbone.History = function() { + this.handlers = []; + _.bindAll(this, "checkUrl"); + // Ensure that `History` can be used outside of the browser. + if (typeof window !== "undefined") { + this.location = window.location; + this.history = window.history; + } + }; + // Cached regex for stripping a leading hash/slash and trailing space. + var routeStripper = /^[#\/]|\s+$/g; + // Cached regex for stripping leading and trailing slashes. + var rootStripper = /^\/+|\/+$/g; + // Cached regex for detecting MSIE. + var isExplorer = /msie [\w.]+/; + // Cached regex for removing a trailing slash. + var trailingSlash = /\/$/; + // Has the history handling already been started? + History.started = false; + // Set up all inheritable **Backbone.History** properties and methods. + _.extend(History.prototype, Events, { + // The default interval to poll for hash changes, if necessary, is + // twenty times a second. + interval: 50, + // Gets the true hash value. Cannot use location.hash directly due to bug + // in Firefox where location.hash will always be decoded. + getHash: function(window) { + var match = (window || this).location.href.match(/#(.*)$/); + return match ? match[1] : ""; + }, + // Get the cross-browser normalized URL fragment, either from the URL, + // the hash, or the override. + getFragment: function(fragment, forcePushState) { + if (fragment == null) { + if (this._hasPushState || !this._wantsHashChange || forcePushState) { + fragment = this.location.pathname; + var root = this.root.replace(trailingSlash, ""); + if (!fragment.indexOf(root)) fragment = fragment.substr(root.length); + } else { + fragment = this.getHash(); + } + } + return fragment.replace(routeStripper, ""); + }, + // Start the hash change handling, returning `true` if the current URL matches + // an existing route, and `false` otherwise. + start: function(options) { + if (History.started) throw new Error("Backbone.history has already been started"); + History.started = true; + // Figure out the initial configuration. Do we need an iframe? + // Is pushState desired ... is it available? + this.options = _.extend({}, { + root: "/" + }, this.options, options); + this.root = this.options.root; + this._wantsHashChange = this.options.hashChange !== false; + this._wantsPushState = !!this.options.pushState; + this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); + var fragment = this.getFragment(); + var docMode = document.documentMode; + var oldIE = isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7); + // Normalize root to always include a leading and trailing slash. + this.root = ("/" + this.root + "/").replace(rootStripper, "/"); + if (oldIE && this._wantsHashChange) { + this.iframe = Backbone.$('