diff options
75 files changed, 2176 insertions, 4628 deletions
@@ -4,3 +4,4 @@ angular.js angular-minified.js angular-debug.js angular-scenario.js +angularjs.netrc
\ No newline at end of file @@ -1,4 +1,4 @@ -include FileUtils + include FileUtils task :default => [:compile, :test] @@ -45,8 +45,10 @@ task :compile_scenario do src/apis.js \ src/services.js \ src/AngularPublic.js \ - src/scenario/Runner.js \ src/scenario/DSL.js \ + src/scenario/Future.js \ + src/scenario/Matcher.js \ + src/scenario/Runner.js \ src/scenario/angular.suffix \ ) css = %x(cat css/angular-scenario.css) @@ -89,7 +91,7 @@ task :compile do f.close %x(java -jar lib/compiler-closure/compiler.jar \ - --compilation_level ADVANCED_OPTIMIZATIONS \ + --compilation_level SIMPLE_OPTIMIZATIONS \ --js angular-debug.js \ --externs externs.js \ --create_source_map ./angular-minified.map \ @@ -118,3 +120,9 @@ task :lint do out = %x(lib/jsl/jsl -conf lib/jsl/jsl.default.conf) print out end + +desc 'push_angularajs' +task :push_angularjs do + Rake::Task['compile'].execute 0 + sh %(cat angularjs.ftp | ftp -N angularjs.netrc angularjs.org) +end diff --git a/angularjs.ftp b/angularjs.ftp new file mode 100644 index 00000000..dbe10397 --- /dev/null +++ b/angularjs.ftp @@ -0,0 +1,5 @@ +bin +cd angularjs.org/ng +put angular-debug.js js/angular-debug.js +put angular-minified.js js/angular-minified.js +put angular-scenario.js js/angular-scenario.js diff --git a/example/buzz/buzz.css b/example/buzz/buzz.css new file mode 100644 index 00000000..5fd5763d --- /dev/null +++ b/example/buzz/buzz.css @@ -0,0 +1,89 @@ +body { + background: -webkit-gradient(linear, left top, left 100, from(#bbb), to(#fff)); + background-repeat: no-repeat; + margin: 0px; + font-family: sans-serif; + font-size: 12px; +} + +body > div { + border-top: 1px solid white; + border-bottom: 1px solid black; + text-align: center; + background: -webkit-gradient(linear, left top, left bottom, from(#CCC), to(#888)); + -webkit-background-origin: padding; -webkit-background-clip: content; +} +body > div button { + margin: 5px; +} + +body > div span:FIRST-CHILD { + float: left; + font-family: monospace; + font-size: 1.5em; + color: black; + padding: 2px 5px; +} + +body > div span:last-child { + float: right; +} + +ul { + list-style: none; + padding: 10px; + margin: 0; +} + +body > ul > li { + border: 1px solid black; + margin: 15px 5px; + padding: 0; + -webkit-box-shadow: 5px 5px 5px #888; +} + +body > ul > li > h1 { + margin: 0; + background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#999)); + font-size: 13px; + border-bottom: 1px solid black; +} + +h1 > img, +li > img { + max-height: 30px; + max-width: 30px; + vertical-align: middle; + padding: 3px; +} + +a > img { + margin-right: 5px; + margin-top: 5px; +} + +body > ul > li > h1 > a:last-child { + float: right; + margin: 10px; +} + +body > ul > li > div { + background-color: white; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ddd)); + margin: 0; + padding: 10px; +} + +body > ul > li ul { + margin: 0; + padding: 0; + margin-left: 5px; + border-left: 5px solid lightgray; +} + +body > ul > li ul > li { + margin: 0; + padding: 10px; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ddd)); +} + diff --git a/example/buzz/buzz.html b/example/buzz/buzz.html new file mode 100644 index 00000000..a6777ff8 --- /dev/null +++ b/example/buzz/buzz.html @@ -0,0 +1,50 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html xmlns:ng="http://angularjs.org"> + <head> + <script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.js"></script> + <script type="text/javascript" src="http://angularjs.org/ng/js/angular-debug.js#autobind"></script> + <script type="text/javascript" src="buzz.js"></script> + <link rel="stylesheet" type="text/css" href="http://angularjs.org/ng/css/angular.css"/> + <link rel="stylesheet" type="text/css" href="buzz.css"/> + </head> + <body ng:init="$window.$root = this" ng:controller="BuzzController"> + <div> + <span><angular/> Buzz</span> + <span> + filter: + <input type="text" name="filterText"/> + </span> + <span> + user: + <input type="text" name="userId" ng:required/> + <button ng:click="$location.hashPath = userId">fetch</button> + </span> + </div> + <ul> + <li ng:repeat="item in activities.data.items.$filter(filterText)"> + <h1> + <img src="{{item.actor.thumbnailUrl}}"/> + <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a> + <a href="#" ng:click="expandReplies(item)">Replies: {{item.links.replies[0].count}}</a> + </h1> + <div> + {{item.object.content | html}} + <div> + <a href="{{attachment.links.enclosure[0].href}}" ng:repeat="attachment in item.object.attachments"> + <img src="{{attachment.links.preview[0].href}}"/> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <li ng:repeat="reply in item.replies.data.items"> + <img src="{{reply.actor.thumbnailUrl}}"/> + <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: + {{reply.content | html}} + </li> + </ul> + </my:expand> + </li> + </ul> + </body> +</html> diff --git a/example/buzz/buzz.js b/example/buzz/buzz.js new file mode 100644 index 00000000..40813d16 --- /dev/null +++ b/example/buzz/buzz.js @@ -0,0 +1,46 @@ +angular.service('myApplication', function($resource){ + this.Activity = $resource( + 'https://www.googleapis.com/buzz/v1/activities/:userId/:visibility/:activityId/:comments', + {alt:'json', callback:'JSON_CALLBACK'}, + { + get: {method:'JSON', params:{visibility:'@self'}}, + replies: {method:'JSON', params:{visibility:'@self', comments:'@comments'}} + }); +}, {inject:['$resource']}); + +function BuzzController(){ + this.$watch('$location.hashPath', this.userChange); +} +BuzzController.prototype = { + userChange: function(){ + this.userId = this.$location.hashPath; + this.activities = this.Activity.get({userId:this.userId}); + }, + + expandReplies: function(activity) { + var self = this; + if (activity.replies) { + activity.replies.show = !activity.replies.show; + } else { + activity.replies = this.Activity.replies({userId:this.userId, activityId:activity.id}, function(){ + activity.replies.show = true; + }); + } + } +}; + +angular.widget('my:expand', function(element){ + element.css('display', 'block'); + this.descend(true); + return function(element) { + element.hide(); + var watch = element.attr('expand'); + this.$watch(watch, function(value){ + if (value) { + element.delay(0).slideDown('slow'); + } else { + element.slideUp('slow'); + } + }); + }; +}); diff --git a/example/temp.html b/example/temp.html index d07a6948..8a1246ed 100644 --- a/example/temp.html +++ b/example/temp.html @@ -1,13 +1,36 @@ -<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> -<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> +<html xmlns:ng="http://angularjs.org"> <head> - <script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script> + <script type="text/javascript" + src="../angular-debug.js" ng:autobind ng:css="css/angular.css"></script> </head> - <body> - {{$location.hashSearch.order}} <br/> - <input type="radio" name="$location.hashSearch.order" value="A"/> A <br/> - <input type="radio" name="$location.hashSearch.order" checked value="B"/> B <br/> - <input type="radio" name="$location.hashSearch.order" value="C"/> C <br/> - {{$location.hashSearch.order}} <br/> + <body ng:init="$window.$root = this"> + +<script> +angular.widget('my:greeter', function(compileElement){ + var compiler = this; + compileElement.css('style', 'block'); + var salutaitonExp = compileElement.attr('salutation'); + var nameExp = compileElement.attr('name'); + return function(linkElement){ + var salutaitonSpan = angular.element('<span class="salutation"></span'); + var nameSpan = angular.element('<span class="name"></span>'); + linkElement.append(salutaitonSpan); + linkElement.append(compiler.text(' ')); + linkElement.append(nameSpan); + linkElement.append(compiler.text('!')); + this.$watch(salutaitonExp, function(value){ + salutaitonSpan.text(value); + }); + this.$watch(nameExp, function(value){ + nameSpan.text(value); + }); + }; +}); +</script> +<div ng:init="salutation='Hello'; name='World'"> + <my:greeter salutation="salutation" name="name"/> +</div> + </body> -</html> +</html>
\ No newline at end of file diff --git a/jsTestDriver-jquery.conf b/jsTestDriver-jquery.conf index 34538bce..ed58d269 100644 --- a/jsTestDriver-jquery.conf +++ b/jsTestDriver-jquery.conf @@ -6,10 +6,11 @@ load: - lib/jquery/jquery-1.4.2.js - test/jquery_alias.js - src/Angular.js + - src/JSON.js - src/*.js + - test/testabilityPatch.js - src/scenario/Runner.js - src/scenario/*.js - - test/testabilityPatch.js - test/angular-mocks.js - test/scenario/*.js - test/*.js diff --git a/jsTestDriver.conf b/jsTestDriver.conf index 16bcf1db..c7d74b75 100644 --- a/jsTestDriver.conf +++ b/jsTestDriver.conf @@ -6,10 +6,11 @@ load: - lib/jquery/jquery-1.4.2.js - test/jquery_remove.js - src/Angular.js + - src/JSON.js - src/*.js + - test/testabilityPatch.js - src/scenario/Runner.js - src/scenario/*.js - - test/testabilityPatch.js - test/angular-mocks.js - test/scenario/*.js - test/*.js diff --git a/perf/blank.html b/perf/blank.html new file mode 100644 index 00000000..f38c368b --- /dev/null +++ b/perf/blank.html @@ -0,0 +1,33 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <script type="text/javascript"> + function time(timerName){ + var started = new Date().getTime(); + return { + name: timerName, + started: started, + lastLap: {duration:0, name:'BEGIN'}, + lap: function(lapName){ + var duration = (this.lapped = new Date().getTime()) - this.started; + var lastName = this.lastLap.name; + this.lastLap = {duration:duration-this.lastLap.duration, name:lapName}; + console.log(timerName, lapName, 'TOTAL: ' + duration + ' ms.', 'since ' + lastName + ': ' + this.lastLap.duration + ' ms.'); + return duration; + } + }; + } + window.browser = time('BROWSER'); + </script> + <script type="text/javascript" src="../angular-minified.js" ng:autobind ng:css="css/angular.css"></script> + <script type="text/javascript"> + window.browser.lap('parse'); + + </script> + </head> + <body ng:init="$window.$scope = this; $window.browser.lap('ready')" style="display:none;" ng:show="true"> + <a href="startup.html">reload</a> + <hr/> + READY + </body> + </html> diff --git a/perf/buzz.css b/perf/buzz.css new file mode 100644 index 00000000..5fd5763d --- /dev/null +++ b/perf/buzz.css @@ -0,0 +1,89 @@ +body { + background: -webkit-gradient(linear, left top, left 100, from(#bbb), to(#fff)); + background-repeat: no-repeat; + margin: 0px; + font-family: sans-serif; + font-size: 12px; +} + +body > div { + border-top: 1px solid white; + border-bottom: 1px solid black; + text-align: center; + background: -webkit-gradient(linear, left top, left bottom, from(#CCC), to(#888)); + -webkit-background-origin: padding; -webkit-background-clip: content; +} +body > div button { + margin: 5px; +} + +body > div span:FIRST-CHILD { + float: left; + font-family: monospace; + font-size: 1.5em; + color: black; + padding: 2px 5px; +} + +body > div span:last-child { + float: right; +} + +ul { + list-style: none; + padding: 10px; + margin: 0; +} + +body > ul > li { + border: 1px solid black; + margin: 15px 5px; + padding: 0; + -webkit-box-shadow: 5px 5px 5px #888; +} + +body > ul > li > h1 { + margin: 0; + background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#999)); + font-size: 13px; + border-bottom: 1px solid black; +} + +h1 > img, +li > img { + max-height: 30px; + max-width: 30px; + vertical-align: middle; + padding: 3px; +} + +a > img { + margin-right: 5px; + margin-top: 5px; +} + +body > ul > li > h1 > a:last-child { + float: right; + margin: 10px; +} + +body > ul > li > div { + background-color: white; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ddd)); + margin: 0; + padding: 10px; +} + +body > ul > li ul { + margin: 0; + padding: 0; + margin-left: 5px; + border-left: 5px solid lightgray; +} + +body > ul > li ul > li { + margin: 0; + padding: 10px; + background: -webkit-gradient(linear, left top, left bottom, from(#fff), to(#ddd)); +} + diff --git a/perf/buzz.html b/perf/buzz.html new file mode 100644 index 00000000..f2e95c85 --- /dev/null +++ b/perf/buzz.html @@ -0,0 +1,89 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <link rel="stylesheet" type="text/css" href="buzz.css"/> + <script type="text/javascript"> + var log = []; + function time(timerName){ + var started = new Date().getTime(); + return { + name: timerName, + started: started, + lapped: started, + lastLap: {duration:0, name:'BEGIN', time:started}, + lap: function(lapName){ + var totalDuration = (this.lapped = new Date().getTime()) - this.started; + var lapDuration = this.lapped - this.lastLap.time; + var lastName = this.lastLap.name; + this.lastLap = {duration:lapDuration, name:lapName, time:this.lapped}; + log.push(timerName + ' ' + lapName + ': ' + this.lastLap.duration + ' ms. (' + totalDuration + ' ms.)'); + } + }; + } + window.browser = time('boot:'); + </script> + <script type="text/javascript" src="../angular-minified.js" ng:autobind ng:css="css/angular.css"></script> + <script type="text/javascript"> + window.browser.lap('parse <angular/>'); + function onInit(scope) { + window.browser.lap('compile DOM'); + window.setTimeout(function(){ + window.browser.lap('DOM render'); + scope.$eval(); + }, 1); + } + </script> + <script> + // 60kb JSON + window.googlebuzz = {"data":{"kind":"buzz#activityFeed","links":{"next":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self?alt\u003djson&max-results\u003d20&c\u003dCJPT45zJwKECSL72gt20sqMC"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self?alt\u003djson","type":"application/json"}]},"title":"Google Buzz Self Feed for Google Buzz Team","updated":"2010-08-10T05:08:40.954Z","id":"tag:google.com,2010:buzz-feed:self:posted:111062888259659218284","items":[{"kind":"buzz#activity","title":"AutoShare from YouTube to Buzz If you use YouTube a lot, you may be familiar with AutoShare (see ...","published":"2010-07-01T01:00:37.000Z","updated":"2010-07-01T01:00:38.218Z","id":"tag:google.com,2010:buzz:z132tjxxinrhyz4pz04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z132tjxxinrhyz4pz04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":91}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/YyNSUbbuV1N","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z132tjxxinrhyz4pz04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":26,"updated":"2010-08-10T05:08:40.954Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z132tjxxinrhyz4pz04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eAutoShare from YouTube to Buzz\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eIf you use YouTube a lot, you may be familiar with AutoShare (see \u003ca href\u003d\"http://help.youtube.com/support/youtube/bin/answer.py?hl\u003den&answer\u003d157215\" \u003ehttp://help.youtube.com/support/youtube/bin/answer.py?hl\u003den&answer\u003d157215\u003c/a\u003e) which you can use to automatically share your favorites, likes, uploads, or comments on videos to Facebook. Twitter, and Google Reader. Now you can choose to automatically share from YouTube to Google Buzz as well.\r\u003cbr /\u003e\r\u003cbr /\u003eYou can still keep YouTube as a connected site in Buzz if you hooked it up previously, but that only automatically posts your uploads for now.","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/YyNSUbbuV1N","type":"text/html"}]},"attachments":[{"type":"article","title":"AutoShare : Learn More - YouTube Help","links":{"alternate":[{"href":"http://help.youtube.com/support/youtube/bin/answer.py?hl\u003den&answer\u003d157215","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Google Buzz API talks and hackathons Come join us to talk about how you can build on Google Buzz ...","published":"2010-07-02T02:56:27.000Z","updated":"2010-07-02T02:56:28.031Z","id":"tag:google.com,2010:buzz:z13xe5wwtnyqelxkw22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13xe5wwtnyqelxkw22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":96}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/5qbwA75RRr1","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13xe5wwtnyqelxkw22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":62,"updated":"2010-08-08T10:48:37.665Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13xe5wwtnyqelxkw22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eGoogle Buzz API talks and hackathons\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eCome join us to talk about how you can build on Google Buzz -- check out the list below to see if weÕll be in a city near you. We'll cover several open standards (Activity Streams, OAuth, PubSubHubbub, and Salmon) and go in depth with the Google Buzz API and libraries. In some locations weÕll have extended time for hacking with Googlers on hand to answer your questions.\r\u003cbr /\u003e\r\u003cbr /\u003eHereÕs the schedule (weÕll add links for the Cambridge, Kiev, and Moscow sessions soon):\r\u003cbr /\u003e\r\u003cbr /\u003eSydney, Australia - July 1: \u003ca href\u003d\"http://code.google.com/events/devfests/2010/sydney4.html\" \u003ehttp://code.google.com/events/devfests/2010/sydney4.html\u003c/a\u003e\r\u003cbr /\u003eManila, Philippines - July 6: \u003ca href\u003d\"http://code.google.com/events/devfests/2010/asia1.html\" \u003ehttp://code.google.com/events/devfests/2010/asia1.html\u003c/a\u003e\r\u003cbr /\u003eSingapore - July 9: \u003ca href\u003d\"http://code.google.com/events/devfests/2010/asia2.html\" \u003ehttp://code.google.com/events/devfests/2010/asia2.html\u003c/a\u003e\r\u003cbr /\u003eKuala Lumpur, Malaysia - July 16: \u003ca href\u003d\"http://code.google.com/events/devfests/2010/asia3.html\" \u003ehttp://code.google.com/events/devfests/2010/asia3.html\u003c/a\u003e\r\u003cbr /\u003eNew York, NY, USA - July 21: \u003ca href\u003d\"http://www.meetup.com/NYC-GTUG/calendar/13899003/\" \u003ehttp://www.meetup.com/NYC-GTUG/calendar/13899003/\u003c/a\u003e\r\u003cbr /\u003eCambridge, MA, USA - August 7\r\u003cbr /\u003eStockholm, Sweden - July 12: \u003ca href\u003d\"http://sites.google.com/site/stockholmgtug/extra-gtug-mte-juli\" \u003eextra gtug mte juli\u003c/a\u003e\r\u003cbr /\u003eBerlin, Germany - July 15: \u003ca href\u003d\"http://www.berlin-gtug.org/2010/06/july-2010-berlin-gtug-meeting.html\" \u003ehttp://www.berlin-gtug.org/2010/06/july-2010-berlin-gtug-meeting.html\u003c/a\u003e\r\u003cbr /\u003eKiev, Ukraine - July 17\r\u003cbr /\u003eMoscow, Russia - July 19\r\u003cbr /\u003eLisbon, Portugal - July 28th: \u003ca href\u003d\"https://spreadsheets.google.com/a/google.com/viewform?formkey\u003ddFd5aFo3WVVUX2xaeUZOVDJJVlo1ekE6MQ\" \u003ehttps://spreadsheets.google.com/a/google.com/viewform?formkey\u003ddFd5aFo3WVVUX2xaeUZOVDJJVlo1ekE6MQ\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/5qbwA75RRr1","type":"text/html"}]}},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"When Google Buzz launched, it was only available for devices running Android 2.0+ and iPhone. Not...","published":"2010-05-27T19:17:26.000Z","updated":"2010-05-27T19:17:27.039Z","id":"tag:google.com,2010:buzz:z12gybujdnrxufdwu04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12gybujdnrxufdwu04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":199}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/SoyLF1PRQhn","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12gybujdnrxufdwu04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":87,"updated":"2010-08-04T22:16:13.927Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12gybujdnrxufdwu04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"When Google Buzz launched, it was only available for devices running Android 2.0+ and iPhone. Not any more! Today weÕre a releasing an XHTML version of the Buzz website which can be accessed from many other mobile devices, including those running Android pre-2.0, Blackberry, Nokia S60, and Windows Mobile.\r\u003cbr /\u003e\r\u003cbr /\u003eRead more at \u003ca href\u003d\"http://googlemobile.blogspot.com/2010/05/google-buzz-for-mobile-now-available-on.html\" \u003ehttp://googlemobile.blogspot.com/2010/05/google-buzz-for-mobile-now-available-on.html\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/SoyLF1PRQhn","type":"text/html"}]},"attachments":[{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F1.bp.blogspot.com%2F_SS18DlBSIQ8%2FS_6tBXefeGI%2FAAAAAAAAGr4%2FRXnIdiSgYPI%2Fs400%2Fbuzz%2Bpic%2B1.jpg","type":"image/jpeg"}],"enclosure":[{"href":"http://1.bp.blogspot.com/_SS18DlBSIQ8/S_6tBXefeGI/AAAAAAAAGr4/RXnIdiSgYPI/s400/buzz+pic+1.jpg","type":"image/jpeg"}]}},{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F2.bp.blogspot.com%2F_SS18DlBSIQ8%2FS_6t8CKFnSI%2FAAAAAAAAGsA%2FU6qdmKc_6G0%2Fs400%2Fbuzz%2Bpic%2B2.jpg","type":"image/jpeg"}],"enclosure":[{"href":"http://2.bp.blogspot.com/_SS18DlBSIQ8/S_6t8CKFnSI/AAAAAAAAGsA/U6qdmKc_6G0/s400/buzz+pic+2.jpg","type":"image/jpeg"}]}},{"type":"article","title":"Google Buzz for mobile now available on more devices - Official Google Mobile Blog","links":{"alternate":[{"href":"http://googlemobile.blogspot.com/2010/05/google-buzz-for-mobile-now-available-on.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"New! Comments and likes tabs on your profile Many of you have asked for an easy way to refer back...","published":"2010-07-14T23:45:20.000Z","updated":"2010-07-14T23:45:21.182Z","id":"tag:google.com,2010:buzz:z12kcpb5uknazjs1022fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12kcpb5uknazjs1022fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":326}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/6UndwSjNpoH","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12kcpb5uknazjs1022fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":96,"updated":"2010-07-30T18:32:33.815Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12kcpb5uknazjs1022fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eNew! Comments and likes tabs on your profile\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eMany of you have asked for an easy way to refer back to the posts you've commented on and/or liked. Some of you have also requested a way to see what other people have commented on or liked -- a great way to discover posts that may interest you too. \r\u003cbr /\u003e\r\u003cbr /\u003eStarting today, you'll see new tabs on your profile that show all the posts you've commented on or liked. By default, these tabs are only visible to you. If you want to share your comments and likes tabs, you can choose to make them publicly visible to others from the Buzz tab under Settings. This won't change who can see each post; private posts stay private.","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/6UndwSjNpoH","type":"text/html"}]},"attachments":[{"type":"photo","content":"comments&likes.png","links":{"alternate":[{"href":"http://picasaweb.google.com/111062888259659218284/July142010?authkey\u003dGv1sRgCKbByv6M3ougAw#5493911882110402178","type":"text/html"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttps%3A%2F%2Flh6.googleusercontent.com%2F_f0qVMWvWkZQ%2FTD5Lv7B8_oI%2FAAAAAAAAACk%2Fdc1Ndqi_gfQ%2Fs288%2Fcomments%2526likes.png","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttps%3A%2F%2Flh6.googleusercontent.com%2F_f0qVMWvWkZQ%2FTD5Lv7B8_oI%2FAAAAAAAAACk%2Fdc1Ndqi_gfQ%2Fs288%2Fcomments%2526likes.png","type":"image/jpeg"}],"enclosure":[{"href":"https://lh6.googleusercontent.com/_f0qVMWvWkZQ/TD5Lv7B8_oI/AAAAAAAAACk/dc1Ndqi_gfQ/comments%26likes.png","type":"image/jpeg","height":90,"width":380}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Fighting follower spam Any web service that involves following has to fight \"follower spam\" -- ac...","published":"2010-02-24T03:53:53.000Z","updated":"2010-02-24T03:53:54.067Z","id":"tag:google.com,2010:buzz:z12szhgawszuxjyd222fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12szhgawszuxjyd222fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":171}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/6r4ntDbUSet","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12szhgawszuxjyd222fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":187,"updated":"2010-07-23T15:23:13.585Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12szhgawszuxjyd222fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eFighting follower spam\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eAny web service that involves following has to fight "follower spam" -- accounts that follow lots and lots of people in the hopes that some of them will click through to their profiles and a few might even end up buying whatever it is they're selling. Buzz is no different. If you're getting following notices from Buzz users with names that sound suspiciously like spammy marketing scams, the chances are that's exactly what they are -- and we're after them. Just like we work hard to protect your Gmail accounts from email spam, we're working to protect Buzz from follower spam. We look at lots of factors when determining whether to disable an account for follower spam including the number of people it attempts to follow and whether people are blocking it. And just as we're constantly tweaking the algorithms which detect Gmail spam, the same goes for follower spam in Buzz.\r\u003cbr /\u003e\r\u003cbr /\u003eIf you're followed by any suspicious looking users, you can report them by clicking the "Report this profile" link on their profile. If they aren't really suspicious but you just don't want them following you, you can always block them.","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/6r4ntDbUSet","type":"text/html"}]}},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"New Google Buzz API features We just launched the Google Buzz firehose (our top developer feature...","published":"2010-07-19T22:01:39.000Z","updated":"2010-07-19T22:01:40.673Z","id":"tag:google.com,2010:buzz:z13mvpwqktjyf11be04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13mvpwqktjyf11be04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":155}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/g9Rfei3F6C4","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13mvpwqktjyf11be04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":31,"updated":"2010-07-22T05:38:04.392Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13mvpwqktjyf11be04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eNew Google Buzz API features\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eWe just launched the Google Buzz firehose (our top developer feature request) which makes all public activities available as they are published, along with new feeds including comments, likes, and shared counts. \r\u003cbr /\u003e\r\u003cbr /\u003eRead more on the Code Blog: \r\u003cbr /\u003e\u003ca href\u003d\"http://googlecode.blogspot.com/2010/07/new-google-buzz-api-features-including.html\" \u003ehttp://googlecode.blogspot.com/2010/07/new-google-buzz-api-features-including.html\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/g9Rfei3F6C4","type":"text/html"}]},"attachments":[{"type":"article","title":"Google Code Blog: New Google Buzz API features, including a hose of fire","content":"The official Google developer blog. The latest news on Google APIs and developer tools.","links":{"alternate":[{"href":"http://googlecode.blogspot.com/2010/07/new-google-buzz-api-features-including.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Two quick Buzz for mobile updates Yesterday we rolled out a new design for rendering location att...","published":"2010-05-30T05:14:00.000Z","updated":"2010-05-30T05:14:01.345Z","id":"tag:google.com,2010:buzz:z132yb4zzlnlgn24h04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z132yb4zzlnlgn24h04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":105}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/QbXYavQL7ja","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z132yb4zzlnlgn24h04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":45,"updated":"2010-07-19T14:39:55.321Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z132yb4zzlnlgn24h04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eTwo quick Buzz for mobile updates\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eYesterday we rolled out a new design for rendering location attached to posts, including a better place icon, a bigger map, and an easy way to get directions from the post itself.\r\u003cbr /\u003e\r\u003cbr /\u003eIn addition, you can now disable comments from Buzz for mobile. (Look for this option under "More Actions").","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/QbXYavQL7ja","type":"text/html"}]},"attachments":[{"type":"photo","content":"buzz_maps_image.jpg","links":{"alternate":[{"href":"http://picasaweb.google.com/111062888259659218284/May292010?authkey\u003dGv1sRgCJOL7KHU942H6gE#5476926467976674802","type":"text/html"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FTAHzmsivPfI%2FAAAAAAAAACM%2FvdqRNwY1Z6Y%2Fs288%2Fbuzz_maps_image.jpg","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FTAHzmsivPfI%2FAAAAAAAAACM%2FvdqRNwY1Z6Y%2Fs288%2Fbuzz_maps_image.jpg","type":"image/jpeg"}],"enclosure":[{"href":"http://lh3.ggpht.com/_f0qVMWvWkZQ/TAHzmsivPfI/AAAAAAAAACM/vdqRNwY1Z6Y/buzz_maps_image.jpg","type":"image/jpeg","height":800,"width":480}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Read about the updates we're making to the Google Buzz API (http://googlecode.blogspot.com/2010/0...","published":"2010-05-19T20:48:14.000Z","updated":"2010-05-19T20:48:14.902Z","id":"tag:google.com,2010:buzz:z12gehl4pqamhv1cc22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12gehl4pqamhv1cc22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":299}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/2M3ZjDfQoMZ","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12gehl4pqamhv1cc22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":89,"updated":"2010-07-18T21:29:59.234Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12gehl4pqamhv1cc22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"Read about the updates we're making to the Google Buzz API (\u003ca href\u003d\"http://googlecode.blogspot.com/2010/05/introducing-google-buzz-api.html\" \u003ehttp://googlecode.blogspot.com/2010/05/introducing-google-buzz-api.html\u003c/a\u003e) and check out some of the initial apps developers have created so far (\u003ca href\u003d\"http://www.google.com/buzz/apps\" \u003ehttp://www.google.com/buzz/apps\u003c/a\u003e)!","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/2M3ZjDfQoMZ","type":"text/html"}]},"attachments":[{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F4.bp.blogspot.com%2F_WryZsbWA8Vs%2FS_NyVvGdpQI%2FAAAAAAAAAGY%2F6Z3vfWpVSoo%2Fs1600%2FBuzzAPI-logosall.png","type":"image/jpeg"}],"enclosure":[{"href":"http://4.bp.blogspot.com/_WryZsbWA8Vs/S_NyVvGdpQI/AAAAAAAAAGY/6Z3vfWpVSoo/s1600/BuzzAPI-logosall.png","type":"image/jpeg"}]}},{"type":"article","title":"Google Code Blog: Introducing the Google Buzz API","content":"The official Google developer blog. The latest news on Google APIs and developer tools.","links":{"alternate":[{"href":"http://googlecode.blogspot.com/2010/05/introducing-google-buzz-api.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"If you started using Google Buzz before we changed the start-up experience from auto-following to...","published":"2010-04-05T22:44:41.000Z","updated":"2010-04-05T22:44:41.835Z","id":"tag:google.com,2010:buzz:z13ac1fi2nycghz3h22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13ac1fi2nycghz3h22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":88}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/KPspNYN1ZLF","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13ac1fi2nycghz3h22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":61,"updated":"2010-07-18T11:42:47.231Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13ac1fi2nycghz3h22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"If you started using Google Buzz before we changed the start-up experience from auto-following to suggestions for people to follow, we want to help you ensure that Buzz is set up the way you want. So the next time you click into the Buzz tab you'll see a page asking you to confirm your Buzz settings -- you can review and edit your settings from here, or at anytime from the Buzz tab of Gmail Settings. \r\u003cbr /\u003e\r\u003cbr /\u003eFor more info, check out this post on the Gmail blog: \u003ca href\u003d\"http://gmailblog.blogspot.com/2010/04/confirm-your-buzz-settings.html\" \u003ehttp://gmailblog.blogspot.com/2010/04/confirm-your-buzz-settings.html\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/KPspNYN1ZLF","type":"text/html"}]},"attachments":[{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F1.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS7ZUkxT141I%2FAAAAAAAAAjA%2FgEdS8783dFE%2FbuzzConfirmSetup.png","type":"image/jpeg"}],"enclosure":[{"href":"http://1.bp.blogspot.com/_JE4qNpFW6Yk/S7ZUkxT141I/AAAAAAAAAjA/gEdS8783dFE/buzzConfirmSetup.png","type":"image/jpeg"}]}},{"type":"article","title":"Official Gmail Blog: Confirm your Buzz settings","links":{"alternate":[{"href":"http://gmailblog.blogspot.com/2010/04/confirm-your-buzz-settings.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Hey all -- this is the account for the Google Buzz team. We'll post all sorts of updates here abo...","published":"2010-02-20T03:16:48.000Z","updated":"2010-02-20T03:16:49.134Z","id":"tag:google.com,2010:buzz:z12pi5ppytr0gtxgj04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12pi5ppytr0gtxgj04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":335}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/R5C8pdDfLvD","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12pi5ppytr0gtxgj04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":495,"updated":"2010-07-15T15:23:34.937Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12pi5ppytr0gtxgj04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"Hey all -- this is the account for the Google Buzz team. We'll post all sorts of updates here about what we're working on. Feel free to give us feedback and ideas on Google Buzz!\r\u003cbr /\u003e\r\u003cbr /\u003eHere's a photo of our launch "war room" from last week. :-)","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/R5C8pdDfLvD","type":"text/html"}]},"attachments":[{"type":"photo","content":"buzzLaunchRoom.jpg","links":{"alternate":[{"href":"http://picasaweb.google.com/111062888259659218284/20100219?authkey\u003dGv1sRgCPzGzaipscnZqQE#5440159301699698322","type":"text/html"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FS39UD8tY_pI%2FAAAAAAAAAAM%2FCTneAri7FGY%2Fs288%2FbuzzLaunchRoom.jpg","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FS39UD8tY_pI%2FAAAAAAAAAAM%2FCTneAri7FGY%2Fs288%2FbuzzLaunchRoom.jpg","type":"image/jpeg"}],"enclosure":[{"href":"http://lh3.ggpht.com/_f0qVMWvWkZQ/S39UD8tY_pI/AAAAAAAAAAM/CTneAri7FGY/buzzLaunchRoom.jpg","type":"image/jpeg","height":682,"width":1024}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"The Buzz layer in Google Maps for mobile is now available on BlackBerry phones: http://googlemobi...","published":"2010-04-07T20:31:27.000Z","updated":"2010-04-07T20:31:27.548Z","id":"tag:google.com,2010:buzz:z12dij1z2m2exdr3e22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12dij1z2m2exdr3e22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":76}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/JrMcYEeKdJh","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12dij1z2m2exdr3e22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":52,"updated":"2010-07-14T11:12:23.216Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12dij1z2m2exdr3e22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"The Buzz layer in Google Maps for mobile is now available on BlackBerry phones:\r\u003cbr /\u003e\u003ca href\u003d\"http://googlemobile.blogspot.com/2010/04/google-maps-40-for-blackberry-adds.html\" \u003ehttp://googlemobile.blogspot.com/2010/04/google-maps-40-for-blackberry-adds.html\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/JrMcYEeKdJh","type":"text/html"}]},"attachments":[{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F1.bp.blogspot.com%2F_16aYP83FzVs%2FS7zAh47PpZI%2FAAAAAAAAACU%2FUXotejzuskY%2Fs320%2Fbuzz3_cropped.png","type":"image/jpeg"}],"enclosure":[{"href":"http://1.bp.blogspot.com/_16aYP83FzVs/S7zAh47PpZI/AAAAAAAAACU/UXotejzuskY/s320/buzz3_cropped.png","type":"image/jpeg"}]}},{"type":"article","title":"Official Google Mobile Blog: Google Maps 4.0 for BlackBerry adds Search by Voice, Google Buzz, Starring, Labs, and More","links":{"alternate":[{"href":"http://googlemobile.blogspot.com/2010/04/google-maps-40-for-blackberry-adds.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"New! View entire photo albums in Buzz Tired of only being able to view a handful of photos from a...","published":"2010-04-30T22:31:10.000Z","updated":"2010-04-30T22:31:11.329Z","id":"tag:google.com,2010:buzz:z13hspshcvuug3zlt22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13hspshcvuug3zlt22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":266}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/AhZUBLUsFcN","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13hspshcvuug3zlt22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":81,"updated":"2010-07-14T17:23:11.990Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13hspshcvuug3zlt22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eNew! View entire photo albums in Buzz\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eTired of only being able to view a handful of photos from a given Flickr or Picasa album in Buzz's photo viewer? Now you can view the entire thing. \r\u003cbr /\u003e\r\u003cbr /\u003e@\u003ca href\u003d\"http://www.google.com/profiles/104987932455782713675\" class\u003d\"proflink\" oid\u003d\"104987932455782713675\"\u003eThomas Hawk\u003c/a\u003e -- we thought you'd appreciate this ;)","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/AhZUBLUsFcN","type":"text/html"}]}},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Need a refresher on how to post by email? Want an easy way to show your mom how to share privatel...","published":"2010-04-05T18:00:35.000Z","updated":"2010-04-05T18:00:35.941Z","id":"tag:google.com,2010:buzz:z12zy3sgbvabwdr4504cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12zy3sgbvabwdr4504cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":199}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/PNHDxf73hqG","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12zy3sgbvabwdr4504cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":59,"updated":"2010-07-10T16:37:30.481Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12zy3sgbvabwdr4504cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"Need a refresher on how to post by email? Want an easy way to show your mom how to share privately? Check out the 7 tips videos we just added to the Google Buzz YouTube Channel.\r\u003cbr /\u003e\r\u003cbr /\u003eFollowing: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003dMFo2WvPPgfE\" \u003ehttp://www.youtube.com/watch?v\u003dMFo2WvPPgfE\u003c/a\u003e\r\u003cbr /\u003eComments & likes: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003d3H0or0Tnrt8\" \u003ehttp://www.youtube.com/watch?v\u003d3H0or0Tnrt8\u003c/a\u003e\r\u003cbr /\u003ePhotos, links & videos: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003dsDZAPBy-ups\" \u003ehttp://www.youtube.com/watch?v\u003dsDZAPBy-ups\u003c/a\u003e\r\u003cbr /\u003eConnect sites: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003d2G9ZRBGWQc4\" \u003ehttp://www.youtube.com/watch?v\u003d2G9ZRBGWQc4\u003c/a\u003e\r\u003cbr /\u003ePublic/private posting: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003d-IZyk9ECvb8\" \u003ehttp://www.youtube.com/watch?v\u003d-IZyk9ECvb8\u003c/a\u003e\r\u003cbr /\u003e@replies: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003d9LS7MsuHLMI\" \u003ehttp://www.youtube.com/watch?v\u003d9LS7MsuHLMI\u003c/a\u003e\r\u003cbr /\u003ePost via email: \u003ca href\u003d\"http://www.youtube.com/watch?v\u003dJGjj4IGSGQw\" \u003ehttp://www.youtube.com/watch?v\u003dJGjj4IGSGQw\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/PNHDxf73hqG","type":"text/html"}]},"attachments":[{"type":"article","title":"\n\t\tYouTube\n\t\t\t\t- googlebuzz's Channel\n\t","content":"Share your videos with friends, family, and the world","links":{"alternate":[{"href":"http://www.youtube.com/googlebuzz","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Reshare from your mobile device For the last month or so, you've been able to reshare interesting...","published":"2010-06-28T22:10:22.000Z","updated":"2010-06-28T22:10:22.748Z","id":"tag:google.com,2010:buzz:z13fczd5coyswbnk522fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13fczd5coyswbnk522fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":133}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/1wTZFmh3Sso","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13fczd5coyswbnk522fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":52,"updated":"2010-07-10T15:55:04.477Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13fczd5coyswbnk522fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eReshare from your mobile device\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eFor the last month or so, you've been able to reshare interesting buzz posts from your computer (see \u003ca href\u003d\"http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html\" \u003ehttp://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html\u003c/a\u003e for more info).\r\u003cbr /\u003e\r\u003cbr /\u003eToday, we're excited to announce that we've enabled this feature for \u003ca href\u003d\"http://buzz.google.com\" \u003ebuzz.google.com\u003c/a\u003e on Android and iPhone as well. (If you don't see it right away, try clearing your cache/cookies.)","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/1wTZFmh3Sso","type":"text/html"}]},"attachments":[{"type":"photo","content":"Reshare2.png","links":{"alternate":[{"href":"http://picasaweb.google.com/111062888259659218284/June282010?authkey\u003dGv1sRgCLntwPqm-dqTqQE#5487950223732620306","type":"text/html"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FTCkdpu6bvBI%2FAAAAAAAAACc%2FIxE12VFPTDI%2Fs288%2FReshare2.png","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FTCkdpu6bvBI%2FAAAAAAAAACc%2FIxE12VFPTDI%2Fs288%2FReshare2.png","type":"image/jpeg"}],"enclosure":[{"href":"http://lh3.ggpht.com/_f0qVMWvWkZQ/TCkdpu6bvBI/AAAAAAAAACc/IxE12VFPTDI/Reshare2.png","type":"image/jpeg","height":771,"width":489}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"In San Francisco or Toronto? Come chat with the Buzz for mobile team and other Buzz users. Google...","published":"2010-06-19T18:09:22.000Z","updated":"2010-06-19T18:09:23.449Z","id":"tag:google.com,2010:buzz:z12xznrqjlqoix2ja04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12xznrqjlqoix2ja04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":85}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/fV34ajxduiJ","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12xznrqjlqoix2ja04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":101,"updated":"2010-07-03T05:14:31.118Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12xznrqjlqoix2ja04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eIn San Francisco or Toronto? Come chat with the Buzz for mobile team and other Buzz users.\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eGoogle Buzz can be a great place to have a conversation, but we all know face-to-face interactions are still invaluable. Many of you have suggested in person meet ups to mingle and chat with other people using Google Buzz, so we're having our first meet ups focused on Buzz for mobile next week in San Francisco and Toronto. Join @\u003ca href\u003d\"http://www.google.com/profiles/111499908439497508351\" class\u003d\"proflink\" oid\u003d\"111499908439497508351\"\u003ePunit Soni\u003c/a\u003e and @\u003ca href\u003d\"http://www.google.com/profiles/117841284165024027243\" class\u003d\"proflink\" oid\u003d\"117841284165024027243\"\u003eRobin Norvell\u003c/a\u003e from the Buzz for mobile team as well as other users for what promises to be an interesting discussion. \r\u003cbr /\u003e\r\u003cbr /\u003eSan Francisco meet up\r\u003cbr /\u003eTuesday, June 22nd 6pm\r\u003cbr /\u003eAxis Cafe\r\u003cbr /\u003e1201 8th Street\r\u003cbr /\u003eSan Francisco, CA 94107\r\u003cbr /\u003e\r\u003cbr /\u003eToronto meet up*\r\u003cbr /\u003eTuesday, June 29th 7pm\r\u003cbr /\u003elinuxcaffe\r\u003cbr /\u003e326 Harbord Street\r\u003cbr /\u003eToronto, ON M6G 1H3\r\u003cbr /\u003e\r\u003cbr /\u003e* \u003cb\u003eUpdated with new Toronto location\u003c/b\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/fV34ajxduiJ","type":"text/html"}]}},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Two quick Google Buzz for mobile updates Today we released two new features for buzz.google.com o...","published":"2010-06-23T18:33:13.000Z","updated":"2010-06-23T18:33:14.046Z","id":"tag:google.com,2010:buzz:z135ftr5ssyqtxcft22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z135ftr5ssyqtxcft22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":141}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/MHNVs7XYWho","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z135ftr5ssyqtxcft22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":62,"updated":"2010-06-28T19:07:38.888Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z135ftr5ssyqtxcft22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eTwo quick Google Buzz for mobile updates\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eToday we released two new features for \u003ca href\u003d\"http://buzz.google.com\" \u003ebuzz.google.com\u003c/a\u003e on your phone. \r\u003cbr /\u003e\r\u003cbr /\u003e\u003cb\u003eAddress auto-complete for @replies:\u003c/b\u003e Just type @ and the first few letters of the person's name and you'll be prompted with email addresses to choose from. Less typing on tiny mobile keyboards \u003d good ;) You may notice that the first time you do this it seems a little slow to load, but it should get faster after that.\r\u003cbr /\u003e\r\u003cbr /\u003e\u003cb\u003eFull rendering of Google Reader shared items:\u003c/b\u003e You'll now see more details from blogs/Reader shared items right in the post.\r\u003cbr /\u003e\r\u003cbr /\u003eTry these out and let us know what you think in the comments.","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/MHNVs7XYWho","type":"text/html"}]},"attachments":[{"type":"photo","content":"IMG_0224.PNG","links":{"alternate":[{"href":"http://picasaweb.google.com/111062888259659218284/June232010?authkey\u003dGv1sRgCPzDyajUrvy2gAE#5486038443960949314","type":"text/html"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttps%3A%2F%2Flh6.googleusercontent.com%2F_f0qVMWvWkZQ%2FTCJS5gRt2kI%2FAAAAAAAAACU%2FXONANxYRpXU%2Fs288%2FIMG_0224.PNG","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttps%3A%2F%2Flh6.googleusercontent.com%2F_f0qVMWvWkZQ%2FTCJS5gRt2kI%2FAAAAAAAAACU%2FXONANxYRpXU%2Fs288%2FIMG_0224.PNG","type":"image/jpeg"}],"enclosure":[{"href":"https://lh6.googleusercontent.com/_f0qVMWvWkZQ/TCJS5gRt2kI/AAAAAAAAACU/XONANxYRpXU/IMG_0224.PNG","type":"image/jpeg","height":480,"width":320}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"In case you missed it, videos of the Google I/O talks are now all live: http://code.google.com/ev...","published":"2010-06-10T23:23:57.000Z","updated":"2010-06-10T23:23:58.082Z","id":"tag:google.com,2010:buzz:z13agfwgdxamvdm4d04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13agfwgdxamvdm4d04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":98}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/Q1ZTGaJVjvK","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13agfwgdxamvdm4d04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":14,"updated":"2010-06-16T10:30:51.495Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13agfwgdxamvdm4d04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"In case you missed it, videos of the Google I/O talks are now all live: \u003ca href\u003d\"http://code.google.com/events/io/2010/sessions.html\" \u003ehttp://code.google.com/events/io/2010/sessions.html\u003c/a\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eAnd for those of you interested in learning more about how to use the Buzz APIs, here's @\u003ca href\u003d\"http://www.google.com/profiles/108189587050871927619\" class\u003d\"proflink\" oid\u003d\"108189587050871927619\"\u003eChris Chabot\u003c/a\u003e's talk:\r\u003cbr /\u003e\u003ca href\u003d\"http://www.youtube.com/watch?v\u003dqmzXVSvA7W0\" \u003eGoogle I/O 2010 - What's the hubbub about Google Buzz APIs?\u003c/a\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eThanks @\u003ca href\u003d\"http://www.google.com/profiles/112443892461436166386\" class\u003d\"proflink\" oid\u003d\"112443892461436166386\"\u003eGoogle I/O 2010\u003c/a\u003e!","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/Q1ZTGaJVjvK","type":"text/html"}]},"attachments":[{"type":"video","title":"Google I/O 2010 - What's the hubbub about Google Buzz APIs?","content":"Google I/O 2010 - What's the hubbub about Google Buzz APIs? \r\n\r\nSocial Web 101 \r\n\r\nChris Chabot, Marco Kaiser (Seesmic), Ming Yong (Socialwok) \r\n\r\nGoogle Buzz is a new way to share updates, photos, videos and more, and start conversations about the things you find interesting. In this session, we'll take a deep dive into building with the Buzz APIs and the open standards it uses, such as ActivityStrea.ms, PubSubHubbub, OAuth, Salmon and WebFinger. \r\n\r\nFor all I/O 2010 sessions, please go to http://code.google.com/events/io/2010/sessions.html","links":{"alternate":[{"href":"http://www.youtube.com/watch?v\u003dqmzXVSvA7W0","type":"text/html"},{"href":"http://www.youtube.com/v/qmzXVSvA7W0&hl\u003den&fs\u003d1&autoplay\u003d1","type":"application/x-shockwave-flash"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Fytimg.googleusercontent.com%2Fvi%2FqmzXVSvA7W0%2Fdefault.jpg","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Fytimg.googleusercontent.com%2Fvi%2FqmzXVSvA7W0%2Fdefault.jpg","type":"image/jpeg"}]}},{"type":"article","title":"Google I/O 2010","links":{"alternate":[{"href":"","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Nice Google Maps previews in Google Buzz Now when you paste a Google Maps link into the post box,...","published":"2010-06-07T23:12:58.000Z","updated":"2010-06-07T23:12:59.726Z","id":"tag:google.com,2010:buzz:z13isldjrtq4thodv22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13isldjrtq4thodv22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":139}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/AyXWdwLdJua","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13isldjrtq4thodv22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":26,"updated":"2010-06-14T09:15:11.316Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13isldjrtq4thodv22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eNice Google Maps previews in Google Buzz\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eNow when you paste a Google Maps link into the post box, Buzz will automatically fetch a preview of the location you can associate with your post. More info here: \u003ca href\u003d\"http://gmailblog.blogspot.com/2010/06/google-maps-previews-in-gmail-and.html\" \u003ehttp://gmailblog.blogspot.com/2010/06/google-maps-previews-in-gmail-and.html\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/AyXWdwLdJua","type":"text/html"}]},"attachments":[{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F1.bp.blogspot.com%2F_JE4qNpFW6Yk%2FTAhWHBGK22I%2FAAAAAAAAAnQ%2FiAZeq1C8ZRc%2Fmaps_in_buzz.jpg","type":"image/jpeg"}],"enclosure":[{"href":"http://1.bp.blogspot.com/_JE4qNpFW6Yk/TAhWHBGK22I/AAAAAAAAAnQ/iAZeq1C8ZRc/maps_in_buzz.jpg","type":"image/jpeg"}]}},{"type":"article","title":"Google Maps previews in Gmail and Google Buzz - Official Gmail Blog","links":{"alternate":[{"href":"http://gmailblog.blogspot.com/2010/06/google-maps-previews-in-gmail-and.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Reshare - coming out today! When you find an interesting buzz post you want to reshare, instead o...","published":"2010-05-27T16:07:16.000Z","updated":"2010-05-27T16:07:17.116Z","id":"tag:google.com,2010:buzz:z12giviqawz3wjsgg04cdjyb3mjeulcwv3c","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12giviqawz3wjsgg04cdjyb3mjeulcwv3c/@liked?alt\u003djson","type":"application/json","count":436}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/QYa2iFCXzy5","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12giviqawz3wjsgg04cdjyb3mjeulcwv3c/@comments?alt\u003djson","type":"application/json","count":205,"updated":"2010-06-07T16:23:28.974Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z12giviqawz3wjsgg04cdjyb3mjeulcwv3c?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eReshare - coming out today!\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eWhen you find an interesting buzz post you want to reshare, instead of copying and pasting it (and maybe attributing the original poster with an @reply along the way), you'll now be able to reshare posts with two clicks. If you don't see the "Reshare" link quite yet, hang tight. It should be on for everyone by the end of the day. \r\u003cbr /\u003e\r\u003cbr /\u003eLearn more at: \u003ca href\u003d\"http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html\" \u003ehttp://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html\u003c/a\u003e","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/QYa2iFCXzy5","type":"text/html"}]},"attachments":[{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F2.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS_3OkIMCMYI%2FAAAAAAAAAmQ%2FjzyvdUVnk1A%2Freshare1.png","type":"image/jpeg"}],"enclosure":[{"href":"http://2.bp.blogspot.com/_JE4qNpFW6Yk/S_3OkIMCMYI/AAAAAAAAAmQ/jzyvdUVnk1A/reshare1.png","type":"image/jpeg"}]}},{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F2.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS_3Oxy3zb1I%2FAAAAAAAAAmY%2FLgpdu9uY_pA%2Freshare2.png","type":"image/jpeg"}],"enclosure":[{"href":"http://2.bp.blogspot.com/_JE4qNpFW6Yk/S_3Oxy3zb1I/AAAAAAAAAmY/Lgpdu9uY_pA/reshare2.png","type":"image/jpeg"}]}},{"type":"photo","links":{"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2F3.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS_3O5_z4SMI%2FAAAAAAAAAmg%2FZDEoHIDXaE0%2Freshare3.png","type":"image/jpeg"}],"enclosure":[{"href":"http://3.bp.blogspot.com/_JE4qNpFW6Yk/S_3O5_z4SMI/AAAAAAAAAmg/ZDEoHIDXaE0/reshare3.png","type":"image/jpeg"}]}},{"type":"article","title":"\"Reshare\" in Google Buzz - Official Gmail Blog","links":{"alternate":[{"href":"http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html","type":"text/html"}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}},{"kind":"buzz#activity","title":"Tweets with location now showing with Buzz on the map If you've added Twitter as a connected site...","published":"2010-05-11T17:12:43.000Z","updated":"2010-05-11T17:12:45.087Z","id":"tag:google.com,2010:buzz:z13ri1dioqjtjpmur22fzlswnuqoebasu","links":{"liked":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13ri1dioqjtjpmur22fzlswnuqoebasu/@liked?alt\u003djson","type":"application/json","count":218}],"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/FemS5bvnLMb","type":"text/html"}],"replies":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13ri1dioqjtjpmur22fzlswnuqoebasu/@comments?alt\u003djson","type":"application/json","count":100,"updated":"2010-05-21T21:10:34.279Z"}],"self":[{"href":"https://www.googleapis.com/buzz/v1/activities/111062888259659218284/@self/B:z13ri1dioqjtjpmur22fzlswnuqoebasu?alt\u003djson","type":"application/json"}]},"actor":{"id":"111062888259659218284","name":"Google Buzz Team","profileUrl":"http://www.google.com/profiles/googlebuzz","thumbnailUrl":"http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g"},"verbs":["post"],"object":{"type":"note","content":"\u003cb\u003eTweets with location now showing with Buzz on the map\u003c/b\u003e\r\u003cbr /\u003e\r\u003cbr /\u003eIf you've added Twitter as a connected site in Google Buzz and your tweets have a location (via Twitter or other services), they'll now appear in the mobile \u003ca href\u003d\"http://buzz.google.com\" \u003ebuzz.google.com\u003c/a\u003e "Nearby" view and on the Buzz layer in Google Maps (both mobile and desktop). Check out one of these views to see buzz posts, tweets and more around you.","links":{"alternate":[{"href":"http://www.google.com/buzz/111062888259659218284/FemS5bvnLMb","type":"text/html"}]},"attachments":[{"type":"photo","content":"buzz_twitter_traffic.jpg","links":{"alternate":[{"href":"http://picasaweb.google.com/111062888259659218284/20100511?authkey\u003dGv1sRgCJio0JmzpvCI9QE#5470061328611656498","type":"text/html"}],"preview":[{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh4.ggpht.com%2F_f0qVMWvWkZQ%2FS-mPy_d_wzI%2FAAAAAAAAACE%2FtRh1_oH8W04%2Fs288%2Fbuzz_twitter_traffic.jpg","type":"image/jpeg"},{"href":"http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container\u003dfocus&gadget\u003da&resize_h\u003d100&url\u003dhttp%3A%2F%2Flh4.ggpht.com%2F_f0qVMWvWkZQ%2FS-mPy_d_wzI%2FAAAAAAAAACE%2FtRh1_oH8W04%2Fs288%2Fbuzz_twitter_traffic.jpg","type":"image/jpeg"}],"enclosure":[{"href":"http://lh4.ggpht.com/_f0qVMWvWkZQ/S-mPy_d_wzI/AAAAAAAAACE/tRh1_oH8W04/buzz_twitter_traffic.jpg","type":"image/jpeg","height":322,"width":521}]}}]},"source":{"title":"Buzz"},"visibility":{"entries":[{"id":"tag:google.com,2010:buzz-group:@me:@public","title":"Public"}]}}]}}; + </script> + <script type="text/javascript"> + window.browser.lap('parse model'); + </script> + </head> + <body ng:init="$window.$scope = this;$window.onInit(this); activities = $window.googlebuzz; log = $window.log" style="display:none;" ng:show="true"> + <a href="{{$location.href}}">reload</a><br/> + <tt ng:repeat="line in log">{{line}}<br/></tt> + <hr/> + <div> + <span><angular/> Buzz</span> + <span> + filter: + <input type="text" name="filterText"/> + </span> + <span> + user: + <input type="text" name="userId" ng:required/> + <button>fetch</button> + </span> + </div> + <ul> + <li ng:repeat="item in activities.data.items.$filter(filterText)"> + <h1> + <img ng:attr-bind="{src:'{{item.actor.thumbnailUrl}}'}"/> + <a href="{{item.actor.profileUrl}}">{{item.actor.name}}</a> + <a href="#" ng:click="expandReplies(item)">Replies: {{item.links.replies[0].count}}</a> + </h1> + <div> + {{item.object.content | html}} + <div> + <a href="{{attachment.links.enclosure[0].href}}" ng:repeat="attachment in item.object.attachments"> + <img ng:attr-bind="{src:'{{attachment.links.preview[0].href}}'}"/> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <li ng:repeat="reply in item.replies.data.items"> + <img ng:attr-bind="{src:'{{reply.actor.thumbnailUrl}}'}"/> + <a href="{{reply.actor.profileUrl}}">{{reply.actor.name}}</a>: + {{reply.content | html}} + </li> + </ul> + </my:expand> + </li> + </ul> + + <div ng:eval="$window.browser.lap('DOM created')"></div> + </body> + </html> diff --git a/perf/buzz_raw.html b/perf/buzz_raw.html new file mode 100644 index 00000000..059b80d6 --- /dev/null +++ b/perf/buzz_raw.html @@ -0,0 +1,550 @@ +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <link rel="stylesheet" type="text/css" href="buzz.css"/> + <script type="text/javascript"> + + function time(timerName){ + var started = new Date().getTime(); + return { + name: timerName, + started: started, + lapped: started, + lastLap: {duration:0, name:'BEGIN', time:started}, + lap: function(lapName){ + var totalDuration = (this.lapped = new Date().getTime()) - this.started; + var lapDuration = this.lapped - this.lastLap.time; + var lastName = this.lastLap.name; + this.lastLap = {duration:lapDuration, name:lapName, time:this.lapped}; + console.log(timerName, lapName + ': ' + this.lastLap.duration + ' ms.', '(' + totalDuration + ' ms.)'); + } + }; + } + window.browser = time('boot:'); + + </script> + <script type="text/javascript"> + window.browser.lap('parse <angular/>'); + function onInit() { + window.browser.lap('compile DOM'); + window.setTimeout(function(){ + window.browser.lap('DOM render'); + }, 1); + } + onInit(); + </script> + <script type="text/javascript"> + window.browser.lap('parse model'); + </script> + </head> + <body> + <a ng:bind-attr="{"href":"{{$location.href}}"}" href="buzz_raw.html">reload</a> + <hr> + <div> + <span><angular/> Buzz</span> + <span> + filter: + <input type="text" name="filterText" class=""> + </span> + <span> + user: + <input type="text" name="userId" ng:required="" class="ng-validation-error" ng-validation-error="Required"> + <button>fetch</button> + </span> + </div> + <ul> + <!--ng:repeat: item in activities.data.items.$filter(filterText)--><li ng:repeat-index="0"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">26</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>AutoShare from YouTube to Buzz</b> +<br> +<br>If you use YouTube a lot, you may be familiar with AutoShare (see <a href="http://help.youtube.com/support/youtube/bin/answer.py?hl=en&answer=157215">http://help.youtube.com/support/youtube/bin/answer.py?hl=en&answer=157215</a>) which you can use to automatically share your favorites, likes, uploads, or comments on videos to Facebook. Twitter, and Google Reader. Now you can choose to automatically share from YouTube to Google Buzz as well. +<br> +<br>You can still keep YouTube as a connected site in Buzz if you hooked it up previously, but that only automatically posts your uploads for now.</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="1"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">62</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Google Buzz API talks and hackathons</b> +<br> +<br>Come join us to talk about how you can build on Google Buzz -- check out the list below to see if weÍll be in a city near you. We'll cover several open standards (Activity Streams, OAuth, PubSubHubbub, and Salmon) and go in depth with the Google Buzz API and libraries. In some locations weÍll have extended time for hacking with Googlers on hand to answer your questions. +<br> +<br>HereÍs the schedule (weÍll add links for the Cambridge, Kiev, and Moscow sessions soon): +<br> +<br>Sydney, Australia - July 1: <a href="http://code.google.com/events/devfests/2010/sydney4.html">http://code.google.com/events/devfests/2010/sydney4.html</a> +<br>Manila, Philippines - July 6: <a href="http://code.google.com/events/devfests/2010/asia1.html">http://code.google.com/events/devfests/2010/asia1.html</a> +<br>Singapore - July 9: <a href="http://code.google.com/events/devfests/2010/asia2.html">http://code.google.com/events/devfests/2010/asia2.html</a> +<br>Kuala Lumpur, Malaysia - July 16: <a href="http://code.google.com/events/devfests/2010/asia3.html">http://code.google.com/events/devfests/2010/asia3.html</a> +<br>New York, NY, USA - July 21: <a href="http://www.meetup.com/NYC-GTUG/calendar/13899003/">http://www.meetup.com/NYC-GTUG/calendar/13899003/</a> +<br>Cambridge, MA, USA - August 7 +<br>Stockholm, Sweden - July 12: <a href="http://sites.google.com/site/stockholmgtug/extra-gtug-mte-juli">extra gtug mte juli</a> +<br>Berlin, Germany - July 15: <a href="http://www.berlin-gtug.org/2010/06/july-2010-berlin-gtug-meeting.html">http://www.berlin-gtug.org/2010/06/july-2010-berlin-gtug-meeting.html</a> +<br>Kiev, Ukraine - July 17 +<br>Moscow, Russia - July 19 +<br>Lisbon, Portugal - July 28th: <a href="https://spreadsheets.google.com/a/google.com/viewform?formkey=dFd5aFo3WVVUX2xaeUZOVDJJVlo1ekE6MQ">https://spreadsheets.google.com/a/google.com/viewform?formkey=dFd5aFo3WVVUX2xaeUZOVDJJVlo1ekE6MQ</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="2"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">87</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">When Google Buzz launched, it was only available for devices running Android 2.0+ and iPhone. Not any more! Today weÍre a releasing an XHTML version of the Buzz website which can be accessed from many other mobile devices, including those running Android pre-2.0, Blackberry, Nokia S60, and Windows Mobile. +<br> +<br>Read more at <a href="http://googlemobile.blogspot.com/2010/05/google-buzz-for-mobile-now-available-on.html">http://googlemobile.blogspot.com/2010/05/google-buzz-for-mobile-now-available-on.html</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://1.bp.blogspot.com/_SS18DlBSIQ8/S_6tBXefeGI/AAAAAAAAGr4/RXnIdiSgYPI/s400/buzz+pic+1.jpg"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F1.bp.blogspot.com%2F_SS18DlBSIQ8%2FS_6tBXefeGI%2FAAAAAAAAGr4%2FRXnIdiSgYPI%2Fs400%2Fbuzz%2Bpic%2B1.jpg'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href="http://2.bp.blogspot.com/_SS18DlBSIQ8/S_6t8CKFnSI/AAAAAAAAGsA/U6qdmKc_6G0/s400/buzz+pic+2.jpg"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F2.bp.blogspot.com%2F_SS18DlBSIQ8%2FS_6t8CKFnSI%2FAAAAAAAAGsA%2FU6qdmKc_6G0%2Fs400%2Fbuzz%2Bpic%2B2.jpg'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="2" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="3"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">96</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>New! Comments and likes tabs on your profile</b> +<br> +<br>Many of you have asked for an easy way to refer back to the posts you've commented on and/or liked. Some of you have also requested a way to see what other people have commented on or liked -- a great way to discover posts that may interest you too. +<br> +<br>Starting today, you'll see new tabs on your profile that show all the posts you've commented on or liked. By default, these tabs are only visible to you. If you want to share your comments and likes tabs, you can choose to make them publicly visible to others from the Buzz tab under Settings. This won't change who can see each post; private posts stay private.</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="https://lh6.googleusercontent.com/_f0qVMWvWkZQ/TD5Lv7B8_oI/AAAAAAAAACk/dc1Ndqi_gfQ/comments%26likes.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=https%3A%2F%2Flh6.googleusercontent.com%2F_f0qVMWvWkZQ%2FTD5Lv7B8_oI%2FAAAAAAAAACk%2Fdc1Ndqi_gfQ%2Fs288%2Fcomments%2526likes.png'}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="4"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">187</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Fighting follower spam</b> +<br> +<br>Any web service that involves following has to fight "follower spam" -- accounts that follow lots and lots of people in the hopes that some of them will click through to their profiles and a few might even end up buying whatever it is they're selling. Buzz is no different. If you're getting following notices from Buzz users with names that sound suspiciously like spammy marketing scams, the chances are that's exactly what they are -- and we're after them. Just like we work hard to protect your Gmail accounts from email spam, we're working to protect Buzz from follower spam. We look at lots of factors when determining whether to disable an account for follower spam including the number of people it attempts to follow and whether people are blocking it. And just as we're constantly tweaking the algorithms which detect Gmail spam, the same goes for follower spam in Buzz. +<br> +<br>If you're followed by any suspicious looking users, you can report them by clicking the "Report this profile" link on their profile. If they aren't really suspicious but you just don't want them following you, you can always block them.</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="5"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">31</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>New Google Buzz API features</b> +<br> +<br>We just launched the Google Buzz firehose (our top developer feature request) which makes all public activities available as they are published, along with new feeds including comments, likes, and shared counts. +<br> +<br>Read more on the Code Blog: +<br><a href="http://googlecode.blogspot.com/2010/07/new-google-buzz-api-features-including.html">http://googlecode.blogspot.com/2010/07/new-google-buzz-api-features-including.html</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="6"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">45</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Two quick Buzz for mobile updates</b> +<br> +<br>Yesterday we rolled out a new design for rendering location attached to posts, including a better place icon, a bigger map, and an easy way to get directions from the post itself. +<br> +<br>In addition, you can now disable comments from Buzz for mobile. (Look for this option under "More Actions").</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://lh3.ggpht.com/_f0qVMWvWkZQ/TAHzmsivPfI/AAAAAAAAACM/vdqRNwY1Z6Y/buzz_maps_image.jpg"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FTAHzmsivPfI%2FAAAAAAAAACM%2FvdqRNwY1Z6Y%2Fs288%2Fbuzz_maps_image.jpg'}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="7"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">89</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">Read about the updates we're making to the Google Buzz API (<a href="http://googlecode.blogspot.com/2010/05/introducing-google-buzz-api.html">http://googlecode.blogspot.com/2010/05/introducing-google-buzz-api.html</a>) and check out some of the initial apps developers have created so far (<a href="http://www.google.com/buzz/apps">http://www.google.com/buzz/apps</a>)!</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://4.bp.blogspot.com/_WryZsbWA8Vs/S_NyVvGdpQI/AAAAAAAAAGY/6Z3vfWpVSoo/s1600/BuzzAPI-logosall.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F4.bp.blogspot.com%2F_WryZsbWA8Vs%2FS_NyVvGdpQI%2FAAAAAAAAAGY%2F6Z3vfWpVSoo%2Fs1600%2FBuzzAPI-logosall.png'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="8"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">61</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">If you started using Google Buzz before we changed the start-up experience from auto-following to suggestions for people to follow, we want to help you ensure that Buzz is set up the way you want. So the next time you click into the Buzz tab you'll see a page asking you to confirm your Buzz settings -- you can review and edit your settings from here, or at anytime from the Buzz tab of Gmail Settings. +<br> +<br>For more info, check out this post on the Gmail blog: <a href="http://gmailblog.blogspot.com/2010/04/confirm-your-buzz-settings.html">http://gmailblog.blogspot.com/2010/04/confirm-your-buzz-settings.html</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://1.bp.blogspot.com/_JE4qNpFW6Yk/S7ZUkxT141I/AAAAAAAAAjA/gEdS8783dFE/buzzConfirmSetup.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F1.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS7ZUkxT141I%2FAAAAAAAAAjA%2FgEdS8783dFE%2FbuzzConfirmSetup.png'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="9"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">495</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">Hey all -- this is the account for the Google Buzz team. We'll post all sorts of updates here about what we're working on. Feel free to give us feedback and ideas on Google Buzz! +<br> +<br>Here's a photo of our launch "war room" from last week. :-)</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://lh3.ggpht.com/_f0qVMWvWkZQ/S39UD8tY_pI/AAAAAAAAAAM/CTneAri7FGY/buzzLaunchRoom.jpg"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FS39UD8tY_pI%2FAAAAAAAAAAM%2FCTneAri7FGY%2Fs288%2FbuzzLaunchRoom.jpg'}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="10"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">52</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">The Buzz layer in Google Maps for mobile is now available on BlackBerry phones: +<br><a href="http://googlemobile.blogspot.com/2010/04/google-maps-40-for-blackberry-adds.html">http://googlemobile.blogspot.com/2010/04/google-maps-40-for-blackberry-adds.html</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://1.bp.blogspot.com/_16aYP83FzVs/S7zAh47PpZI/AAAAAAAAACU/UXotejzuskY/s320/buzz3_cropped.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F1.bp.blogspot.com%2F_16aYP83FzVs%2FS7zAh47PpZI%2FAAAAAAAAACU%2FUXotejzuskY%2Fs320%2Fbuzz3_cropped.png'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="11"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">81</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>New! View entire photo albums in Buzz</b> +<br> +<br>Tired of only being able to view a handful of photos from a given Flickr or Picasa album in Buzz's photo viewer? Now you can view the entire thing. +<br> +<br>@<a href="http://www.google.com/profiles/104987932455782713675" class="proflink" oid="104987932455782713675">Thomas Hawk</a> -- we thought you'd appreciate this ;)</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="12"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">59</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">Need a refresher on how to post by email? Want an easy way to show your mom how to share privately? Check out the 7 tips videos we just added to the Google Buzz YouTube Channel. +<br> +<br>Following: <a href="http://www.youtube.com/watch?v=MFo2WvPPgfE">http://www.youtube.com/watch?v=MFo2WvPPgfE</a> +<br>Comments & likes: <a href="http://www.youtube.com/watch?v=3H0or0Tnrt8">http://www.youtube.com/watch?v=3H0or0Tnrt8</a> +<br>Photos, links & videos: <a href="http://www.youtube.com/watch?v=sDZAPBy-ups">http://www.youtube.com/watch?v=sDZAPBy-ups</a> +<br>Connect sites: <a href="http://www.youtube.com/watch?v=2G9ZRBGWQc4">http://www.youtube.com/watch?v=2G9ZRBGWQc4</a> +<br>Public/private posting: <a href="http://www.youtube.com/watch?v=-IZyk9ECvb8">http://www.youtube.com/watch?v=-IZyk9ECvb8</a> +<br>@replies: <a href="http://www.youtube.com/watch?v=9LS7MsuHLMI">http://www.youtube.com/watch?v=9LS7MsuHLMI</a> +<br>Post via email: <a href="http://www.youtube.com/watch?v=JGjj4IGSGQw">http://www.youtube.com/watch?v=JGjj4IGSGQw</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="13"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">52</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Reshare from your mobile device</b> +<br> +<br>For the last month or so, you've been able to reshare interesting buzz posts from your computer (see <a href="http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html">http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html</a> for more info). +<br> +<br>Today, we're excited to announce that we've enabled this feature for <a href="http://buzz.google.com">buzz.google.com</a> on Android and iPhone as well. (If you don't see it right away, try clearing your cache/cookies.)</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://lh3.ggpht.com/_f0qVMWvWkZQ/TCkdpu6bvBI/AAAAAAAAACc/IxE12VFPTDI/Reshare2.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2Flh3.ggpht.com%2F_f0qVMWvWkZQ%2FTCkdpu6bvBI%2FAAAAAAAAACc%2FIxE12VFPTDI%2Fs288%2FReshare2.png'}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="14"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">101</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>In San Francisco or Toronto? Come chat with the Buzz for mobile team and other Buzz users.</b> +<br> +<br>Google Buzz can be a great place to have a conversation, but we all know face-to-face interactions are still invaluable. Many of you have suggested in person meet ups to mingle and chat with other people using Google Buzz, so we're having our first meet ups focused on Buzz for mobile next week in San Francisco and Toronto. Join @<a href="http://www.google.com/profiles/111499908439497508351" class="proflink" oid="111499908439497508351">Punit Soni</a> and @<a href="http://www.google.com/profiles/117841284165024027243" class="proflink" oid="117841284165024027243">Robin Norvell</a> from the Buzz for mobile team as well as other users for what promises to be an interesting discussion. +<br> +<br>San Francisco meet up +<br>Tuesday, June 22nd 6pm +<br>Axis Cafe +<br>1201 8th Street +<br>San Francisco, CA 94107 +<br> +<br>Toronto meet up* +<br>Tuesday, June 29th 7pm +<br>linuxcaffe +<br>326 Harbord Street +<br>Toronto, ON M6G 1H3 +<br> +<br>* <b>Updated with new Toronto location</b></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="15"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">62</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Two quick Google Buzz for mobile updates</b> +<br> +<br>Today we released two new features for <a href="http://buzz.google.com">buzz.google.com</a> on your phone. +<br> +<br><b>Address auto-complete for @replies:</b> Just type @ and the first few letters of the person's name and you'll be prompted with email addresses to choose from. Less typing on tiny mobile keyboards = good ;) You may notice that the first time you do this it seems a little slow to load, but it should get faster after that. +<br> +<br><b>Full rendering of Google Reader shared items:</b> You'll now see more details from blogs/Reader shared items right in the post. +<br> +<br>Try these out and let us know what you think in the comments.</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="https://lh6.googleusercontent.com/_f0qVMWvWkZQ/TCJS5gRt2kI/AAAAAAAAACU/XONANxYRpXU/IMG_0224.PNG"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=https%3A%2F%2Flh6.googleusercontent.com%2F_f0qVMWvWkZQ%2FTCJS5gRt2kI%2FAAAAAAAAACU%2FXONANxYRpXU%2Fs288%2FIMG_0224.PNG'}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="16"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">14</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html">In case you missed it, videos of the Google I/O talks are now all live: <a href="http://code.google.com/events/io/2010/sessions.html">http://code.google.com/events/io/2010/sessions.html</a> +<br> +<br>And for those of you interested in learning more about how to use the Buzz APIs, here's @<a href="http://www.google.com/profiles/108189587050871927619" class="proflink" oid="108189587050871927619">Chris Chabot</a>'s talk: +<br><a href="http://www.youtube.com/watch?v=qmzXVSvA7W0">Google I/O 2010 - What's the hubbub about Google Buzz APIs?</a> +<br> +<br>Thanks @<a href="http://www.google.com/profiles/112443892461436166386" class="proflink" oid="112443892461436166386">Google I/O 2010</a>!</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2Fytimg.googleusercontent.com%2Fvi%2FqmzXVSvA7W0%2Fdefault.jpg'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="17"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">26</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Nice Google Maps previews in Google Buzz</b> +<br> +<br>Now when you paste a Google Maps link into the post box, Buzz will automatically fetch a preview of the location you can associate with your post. More info here: <a href="http://gmailblog.blogspot.com/2010/06/google-maps-previews-in-gmail-and.html">http://gmailblog.blogspot.com/2010/06/google-maps-previews-in-gmail-and.html</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://1.bp.blogspot.com/_JE4qNpFW6Yk/TAhWHBGK22I/AAAAAAAAAnQ/iAZeq1C8ZRc/maps_in_buzz.jpg"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F1.bp.blogspot.com%2F_JE4qNpFW6Yk%2FTAhWHBGK22I%2FAAAAAAAAAnQ%2FiAZeq1C8ZRc%2Fmaps_in_buzz.jpg'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="18"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">205</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Reshare - coming out today!</b> +<br> +<br>When you find an interesting buzz post you want to reshare, instead of copying and pasting it (and maybe attributing the original poster with an @reply along the way), you'll now be able to reshare posts with two clicks. If you don't see the "Reshare" link quite yet, hang tight. It should be on for everyone by the end of the day. +<br> +<br>Learn more at: <a href="http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html">http://gmailblog.blogspot.com/2010/05/reshare-in-google-buzz.html</a></span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://2.bp.blogspot.com/_JE4qNpFW6Yk/S_3OkIMCMYI/AAAAAAAAAmQ/jzyvdUVnk1A/reshare1.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F2.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS_3OkIMCMYI%2FAAAAAAAAAmQ%2FjzyvdUVnk1A%2Freshare1.png'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="1" href="http://2.bp.blogspot.com/_JE4qNpFW6Yk/S_3Oxy3zb1I/AAAAAAAAAmY/Lgpdu9uY_pA/reshare2.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F2.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS_3Oxy3zb1I%2FAAAAAAAAAmY%2FLgpdu9uY_pA%2Freshare2.png'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="2" href="http://3.bp.blogspot.com/_JE4qNpFW6Yk/S_3O5_z4SMI/AAAAAAAAAmg/ZDEoHIDXaE0/reshare3.png"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2F3.bp.blogspot.com%2F_JE4qNpFW6Yk%2FS_3O5_z4SMI%2FAAAAAAAAAmg%2FZDEoHIDXaE0%2Freshare3.png'}"> + </a><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="3" href=""> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:''}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li><li ng:repeat-index="19"> + <h1> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{item.actor.thumbnailUrl}}'}"}" ng:attr-bind="{src:'http://www.google.com/s2/photos/public/AIbEiAIAAABECOz6ztaiyNHDmQEiC3ZjYXJkX3Bob3RvKigyYTBkYmNlNDgyOGI0NDViOTRlYTlkZjVmNzIxZTllNjMzMWFhODljMAEgSk99ukLxRAAHhTbzhgNkGnE-8g'}"> + <a ng:bind-attr="{"href":"{{item.actor.profileUrl}}"}" href="http://www.google.com/profiles/googlebuzz"><span ng:bind="item.actor.name">Google Buzz Team</span></a> + <a href="#" ng:click="expandReplies(item)">Replies: <span ng:bind="item.links.replies[0].count">100</span></a> + </h1> + <div> + <span ng:bind="item.object.content | html"><b>Tweets with location now showing with Buzz on the map</b> +<br> +<br>If you've added Twitter as a connected site in Google Buzz and your tweets have a location (via Twitter or other services), they'll now appear in the mobile <a href="http://buzz.google.com">buzz.google.com</a> "Nearby" view and on the Buzz layer in Google Maps (both mobile and desktop). Check out one of these views to see buzz posts, tweets and more around you.</span> + <div> + <!--ng:repeat: attachment in item.object.attachments--><a ng:bind-attr="{"href":"{{attachment.links.enclosure[0].href}}"}" ng:repeat-index="0" href="http://lh4.ggpht.com/_f0qVMWvWkZQ/S-mPy_d_wzI/AAAAAAAAACE/tRh1_oH8W04/buzz_twitter_traffic.jpg"> + <img ng:bind-attr="{"ng:attr-bind":"{src:'{{attachment.links.preview[0].href}}'}"}" ng:attr-bind="{src:'http://images0-focus-opensocial.googleusercontent.com/gadgets/proxy?container=focus&gadget=a&resize_h=100&url=http%3A%2F%2Flh4.ggpht.com%2F_f0qVMWvWkZQ%2FS-mPy_d_wzI%2FAAAAAAAAACE%2FtRh1_oH8W04%2Fs288%2Fbuzz_twitter_traffic.jpg'}"> + </a> + </div> + </div> + <my:expand expand="item.replies.show"> + <ul> + <!--ng:repeat: reply in item.replies.data.items--> + </ul> + </my:expand> + </li> + </ul> + + <div ng:eval="$window.browser.lap('DOM created')"></div> + </body> + </html> diff --git a/scenario/location.html b/scenario/location.html new file mode 100644 index 00000000..75041615 --- /dev/null +++ b/scenario/location.html @@ -0,0 +1,19 @@ + <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> + <head> + <link rel="stylesheet" type="text/css" href="style.css"/> + <script type="text/javascript" src="../src/angular-bootstrap.js#autobind"></script> + </head> + <body ng:init="$window.$scope = this"> + <pre>$location={{$location}}</pre> + Hash Search: + <ul> + <li ng:repeat="(key, value) in $location.hashSearch"><tt>{{key}}={{value}}</tt></li> + </ul> + <hr/> + href: <input type="text" name="$location.href" size="120"/> <br/> + hash: <input type="text" name="$location.hash" size="120"/> <br/> + hashPath: <input type="text" name="$location.hashPath" size="120"/> <br/> + hashSearch: <input type="text" name="$location.hashSearch" size="120" ng:format="json"/> <br/> + </body> + </html> diff --git a/scenario/widgets.html b/scenario/widgets.html index 2626843d..d5285ea6 100644 --- a/scenario/widgets.html +++ b/scenario/widgets.html @@ -72,12 +72,12 @@ </tr> <tr><th colspan="3">Buttons</th></tr> <tr> - <td>ng-change<br/>ng:click</td> + <td>ng:change<br/>ng:click</td> <td ng:init="button.count = 0"> <form> - <input type="button" value="button" ng-change="button.count = button.count + 1"/> <br/> - <input type="submit" value="submit" ng-change="button.count = button.count + 1"/><br/> - <input type="image" src="" ng-change="button.count = button.count + 1"/><br/> + <input type="button" value="button" ng:change="button.count = button.count + 1"/> <br/> + <input type="submit" value="submit" ng:change="button.count = button.count + 1"/><br/> + <input type="image" src="" ng:change="button.count = button.count + 1"/><br/> <a href="" ng:click="button.count = button.count + 1">action</a> </form> </td> diff --git a/src/Angular.js b/src/Angular.js index 2b26c88d..3970f762 100644 --- a/src/Angular.js +++ b/src/Angular.js @@ -3,8 +3,6 @@ if (typeof document.getAttribute == 'undefined') document.getAttribute = function() {}; -if (!window['console']) window['console']={'log':noop, 'error':noop}; - var consoleNode, PRIORITY_FIRST = -99999, PRIORITY_WATCH = -1000, @@ -18,11 +16,12 @@ var consoleNode, msie = !!/(msie) ([\w.]+)/.exec(lowercase(navigator.userAgent)), jqLite = jQuery || jqLiteWrap, slice = Array.prototype.slice, + error = window['console'] ? bind(window['console'], window['console']['error'] || noop) : noop, angular = window['angular'] || (window['angular'] = {}), - angularTextMarkup = extensionMap(angular, 'textMarkup'), + angularTextMarkup = extensionMap(angular, 'markup'), angularAttrMarkup = extensionMap(angular, 'attrMarkup'), angularDirective = extensionMap(angular, 'directive'), - angularWidget = extensionMap(angular, 'widget'), + angularWidget = extensionMap(angular, 'widget', lowercase), angularValidator = extensionMap(angular, 'validator'), angularFilter = extensionMap(angular, 'filter'), angularFormatter = extensionMap(angular, 'formatter'), @@ -30,10 +29,6 @@ var consoleNode, angularCallbacks = extensionMap(angular, 'callbacks'), nodeName; -function angularAlert(){ - log(arguments); window.alert.apply(window, arguments); -} - function foreach(obj, iterator, context) { var key; if (obj) { @@ -78,11 +73,16 @@ function extend(dst) { return dst; } +function inherit(parent, extra) { + return extend(new (extend(function(){}, {prototype:parent}))(), extra); +}; + function noop() {} function identity($) {return $;} -function extensionMap(angular, name) { +function extensionMap(angular, name, transform) { var extPoint; return angular[name] || (extPoint = angular[name] = function (name, fn, prop){ + name = (transform || identity)(name); if (isDefined(fn)) { extPoint[name] = extend(fn, prop || {}); } @@ -173,50 +173,6 @@ function indexOf(array, obj) { return -1; } -function log(a, b, c){ - var console = window['console']; - switch(arguments.length) { - case 1: - console['log'](a); - break; - case 2: - console['log'](a, b); - break; - default: - console['log'](a, b, c); - break; - } -} - -function error(a, b, c){ - var console = window['console']; - switch(arguments.length) { - case 1: - console['error'](a); - break; - case 2: - console['error'](a, b); - break; - default: - console['error'](a, b, c); - break; - } -} - -function consoleLog(level, objs) { - var log = document.createElement("div"); - log.className = level; - var msg = ""; - var sep = ""; - for ( var i = 0; i < objs.length; i++) { - var obj = objs[i]; - msg += sep + (typeof obj == 'string' ? obj : toJson(obj)); - sep = " "; - } - log.appendChild(document.createTextNode(msg)); - consoleNode.appendChild(log); -} - function isLeafNode (node) { if (node) { switch (node.nodeName) { @@ -259,6 +215,32 @@ function copy(source, destination){ } } +function equals(o1, o2) { + if (o1 == o2) return true; + var t1 = typeof o1, t2 = typeof o2, length, key, keySet; + if (t1 == t2 && t1 == 'object') { + if (o1 instanceof Array) { + if ((length = o1.length) == o2.length) { + for(key=0; key<length; key++) { + if (!equals(o1[key], o2[key])) return false; + } + return true; + } + } else { + keySet = {}; + for(key in o1) { + if (key.charAt(0) !== '$' && !equals(o1[key], o2[key])) return false; + keySet[key] = true; + } + for(key in o2) { + if (key.charAt(0) !== '$' && keySet[key] !== true) return false; + } + return true; + } + } + return false; +} + function setHtml(node, html) { if (isLeafNode(node)) { if (msie) { @@ -309,27 +291,24 @@ function escapeAttr(html) { '"'); } -function bind(_this, _function) { - if (!isFunction(_function)) - throw "Not a function!"; - var curryArgs = slice.call(arguments, 2, arguments.length); - return function() { - return _function.apply(_this, curryArgs.concat(slice.call(arguments, 0, arguments.length))); - }; -} - -function outerHTML(node) { - var temp = document.createElement('div'); - temp.appendChild(node); - var outerHTML = temp.innerHTML; - temp.removeChild(node); - return outerHTML; +function bind(self, fn) { + var curryArgs = arguments.length > 2 ? slice.call(arguments, 2, arguments.length) : []; + if (typeof fn == 'function') { + return curryArgs.length ? function() { + return arguments.length ? fn.apply(self, curryArgs.concat(slice.call(arguments, 0, arguments.length))) : fn.apply(self, curryArgs); + }: function() { + return arguments.length ? fn.apply(self, arguments) : fn.call(self); + }; + } else { + // in IE, native methods ore not functions and so they can not be bound (but they don't need to be) + return fn; + } } function toBoolean(value) { if (value && value.length !== 0) { var v = lowercase("" + value); - value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == '[]'); + value = !(v == 'f' || v == '0' || v == 'false' || v == 'no' || v == 'n' || v == '[]'); } else { value = false; } @@ -349,12 +328,11 @@ function merge(src, dst) { } } -function compile(element, parentScope, overrides) { +function compile(element, existingScope) { var compiler = new Compiler(angularTextMarkup, angularAttrMarkup, angularDirective, angularWidget), - $element = jqLite(element), - parent = extend({}, parentScope); + $element = jqLite(element); parent.$element = $element; - return compiler.compile($element)($element, parent, overrides); + return compiler.compile($element)($element, existingScope); } ///////////////////////////////////////////////// @@ -363,8 +341,8 @@ function parseKeyValue(keyValue) { foreach((keyValue || "").split('&'), function(keyValue){ if (keyValue) { key_value = keyValue.split('='); - key = decodeURIComponent(key_value[0]); - obj[key] = key_value[1] ? decodeURIComponent(key_value[1]) : true; + key = unescape(key_value[0]); + obj[key] = key_value[1] ? unescape(key_value[1]) : true; } }); return obj; @@ -373,29 +351,42 @@ function parseKeyValue(keyValue) { function toKeyValue(obj) { var parts = []; foreach(obj, function(value, key){ - parts.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); + parts.push(escape(key) + '=' + escape(value)); }); return parts.length ? parts.join('&') : ''; } function angularInit(config){ if (config.autobind) { - var scope = compile(window.document, null, {'$config':config}); // TODO default to the source of angular.js - scope.$browser.addCss('css/angular.css'); + var scope = compile(window.document, null, {'$config':config}); + if (config.css) + scope.$browser.addCss(config.base_url + config.css); scope.$init(); } } -function angularJsConfig(document) { - var filename = /(.*)\/angular(-(.*))?.js(#(.*))?/, - scripts = document.getElementsByTagName("SCRIPT"), +function angularJsConfig(document, config) { + var filename = /^(.*)\/angular(-([^\/]*))?.js(#(.*))?$/, + scripts = document.getElementsByTagName("script"), match; + config = extend({ + base_url: '', + css: '../css/angular.css' + }, config); for(var j = 0; j < scripts.length; j++) { match = (scripts[j].src || "").match(filename); if (match) { - return match[5]; + config.base_url = match[1] + '/'; + extend(config, parseKeyValue(match[5])); + eachAttribute(jqLite(scripts[j]), function(value, name){ + if (/^ng:/.exec(name)) { + name = name.substring(3).replace(/-/g, '_'); + if (name == 'autobind') value = true; + config[name] = value; + } + }); } } - return ""; + return config; } diff --git a/src/AngularPublic.js b/src/AngularPublic.js index 7230c3e5..7b093f88 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -1,7 +1,10 @@ var browserSingleton; angularService('$browser', function browserFactory(){ if (!browserSingleton) { - browserSingleton = new Browser(window.location, window.document); + browserSingleton = new Browser( + window.location, + jqLite(window.document), + jqLite(window.document.getElementsByTagName('head')[0])); browserSingleton.startUrlWatcher(); browserSingleton.bind(); } @@ -14,9 +17,12 @@ extend(angular, { 'scope': createScope, 'copy': copy, 'extend': extend, + 'equals': equals, 'foreach': foreach, 'noop':noop, 'bind':bind, + 'toJson': toJson, + 'fromJson': fromJson, 'identity':identity, 'isUndefined': isUndefined, 'isDefined': isDefined, diff --git a/src/Browser.js b/src/Browser.js index 0552b3ae..b4314e2c 100644 --- a/src/Browser.js +++ b/src/Browser.js @@ -2,7 +2,7 @@ // Browser ////////////////////////////// -function Browser(location, document) { +function Browser(location, document, head) { this.delay = 50; this.expectedUrl = location.href; this.urlListeners = []; @@ -21,8 +21,9 @@ function Browser(location, document) { }; this.location = location; - this.document = jqLite(document); - this.body = jqLite(document.body); + this.document = document; + this.head = head; + this.idCounter = 0; } Browser.prototype = { @@ -58,21 +59,37 @@ Browser.prototype = { callback = post; post = null; } - var xhr = new this.XHR(), - self = this; - xhr.open(method, url, true); - this.outstandingRequests.count ++; - xhr.onreadystatechange = function() { - if (xhr.readyState == 4) { - try { - callback(xhr.status || 200, xhr.responseText); - } finally { - self.outstandingRequests.count--; - self.processRequestCallbacks(); + if (lowercase(method) == 'json') { + var callbackId = "angular_" + Math.random() + '_' + (this.idCounter++); + callbackId = callbackId.replace(/\d\./, ''); + var script = this.document[0].createElement('script'); + script.type = 'text/javascript'; + script.src = url.replace('JSON_CALLBACK', callbackId); + this.head.append(script); + window[callbackId] = function(data){ + window[callbackId] = undefined; + callback(200, data); + }; + } else { + var xhr = new this.XHR(), + self = this; + xhr.open(method, url, true); + xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded"); + xhr.setRequestHeader("Accept", "application/json, text/plain, */*"); + xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); + this.outstandingRequests.count ++; + xhr.onreadystatechange = function() { + if (xhr.readyState == 4) { + try { + callback(xhr.status || 200, xhr.responseText); + } finally { + self.outstandingRequests.count--; + self.processRequestCallbacks(); + } } - } - }; - xhr.send(post || ''); + }; + xhr.send(post || ''); + } }, processRequestCallbacks: function(){ diff --git a/src/Compiler.js b/src/Compiler.js index 9faafb13..e09f1876 100644 --- a/src/Compiler.js +++ b/src/Compiler.js @@ -30,7 +30,9 @@ Template.prototype = { element = jqLite(element); foreach(this.inits, function(fn) { queue.push(function(scope) { - scope.$tryEval(fn, element, element); + scope.$tryEval(function(){ + return fn.call(scope, element); + }, element); }); }); @@ -67,8 +69,8 @@ Template.prototype = { /////////////////////////////////// //Compiler ////////////////////////////////// -function Compiler(textMarkup, attrMarkup, directives, widgets){ - this.textMarkup = textMarkup; +function Compiler(markup, attrMarkup, directives, widgets){ + this.markup = markup; this.attrMarkup = attrMarkup; this.directives = directives; this.widgets = widgets; @@ -121,20 +123,25 @@ Compiler.prototype = { descend: function(value){ if(isDefined(value)) descend = value; return descend;}, directives: function(value){ if(isDefined(value)) directives = value; return directives;} }; - priority = element.attr('ng:eval-order') || priority || 0; + try { + priority = element.attr('ng:eval-order') || priority || 0; + } catch (e) { + // for some reason IE throws error under some weird circumstances. so just assume nothing + priority = priority || 0; + } if (isString(priority)) { priority = PRIORITY[uppercase(priority)] || 0; } template = new Template(priority); eachAttribute(element, function(value, name){ if (!widget) { - if (widget = self.widgets['@' + name]) { + if (widget = self.widgets('@' + name)) { widget = bind(selfApi, widget, value, element); } } }); if (!widget) { - if (widget = self.widgets[nodeName(element)]) { + if (widget = self.widgets(nodeName(element))) { widget = bind(selfApi, widget, element); } } @@ -151,7 +158,7 @@ Compiler.prototype = { // process markup for text nodes only eachTextNode(element, function(textNode){ var text = textNode.text(); - foreach(self.textMarkup, function(markup){ + foreach(self.markup, function(markup){ markup.call(selfApi, text, textNode, element); }); }); diff --git a/src/JSON.js b/src/JSON.js index 340b075a..0311c317 100644 --- a/src/JSON.js +++ b/src/JSON.js @@ -1,4 +1,4 @@ -array = [].constructor; +var array = [].constructor; function toJson(obj, pretty){ var buf = []; @@ -6,10 +6,6 @@ function toJson(obj, pretty){ return buf.join(''); } -function toPrettyJson(obj) { - return toJson(obj, true); -} - function fromJson(json) { if (!json) return json; try { @@ -74,7 +70,7 @@ function toJsonArray(buf, obj, pretty, stack){ var childPretty = pretty ? pretty + " " : false; var keys = []; for(var k in obj) { - if (k.indexOf('$$') === 0 || obj[k] === undefined) + if (!obj.hasOwnProperty(k) || k.indexOf('$$') === 0 || obj[k] === undefined) continue; keys.push(k); } diff --git a/src/Parser.js b/src/Parser.js index df270792..5eb75713 100644 --- a/src/Parser.js +++ b/src/Parser.js @@ -599,12 +599,11 @@ Parser.prototype = { for ( var i = 0; i < argsFn.length; i++) { args.push(argsFn[i](self)); } - var fnPtr = fn(self); - if (typeof fnPtr === 'function') { - return fnPtr.apply(self, args); - } else { - throw "Expression '" + fn.isAssignable + "' is not a function."; - } + var fnPtr = fn(self) || noop; + // IE stupidity! + return fnPtr.apply ? + fnPtr.apply(self, args) : + fnPtr(args[0], args[1], args[2], args[3], args[4]); }; }, diff --git a/src/Resource.js b/src/Resource.js index 1279dc54..f4f26ebd 100644 --- a/src/Resource.js +++ b/src/Resource.js @@ -28,6 +28,7 @@ Route.prototype = { query.push(encodeURI(key) + '=' + encodeURI(value)); } }); + url = url.replace(/\/*$/, ''); return url + (query.length ? '?' + query.join('&') : ''); } }; @@ -88,7 +89,7 @@ ResourceFactory.prototype = { throw "Expected between 0-3 arguments [params, data, callback], got " + arguments.length + " arguments."; } - var value = action.isArray ? [] : new Resource(data) + var value = action.isArray ? [] : new Resource(data); self.xhr( action.method, route.url(extend({}, action.params || {}, extractParams(data), params)), diff --git a/src/Scope.js b/src/Scope.js index 35da77fe..27fafc3a 100644 --- a/src/Scope.js +++ b/src/Scope.js @@ -44,14 +44,16 @@ function setter(instance, path, value){ } /////////////////////////////////// - -var getterFnCache = {}; -var JS_KEYWORDS = {}; +var scopeId = 0; + getterFnCache = {}, + compileCache = {}, + JS_KEYWORDS = {}; foreach( - ["break", "const", "continue", "class", "delete", - "do", "while", "for", "function", "if", - "instanceof", "new", "return", "switch", - "this", "throw", "try", "catch", "with"], + ["abstract", "boolean", "break", "byte", "case", "catch", "char", "class", "const", "continue", "debugger", "default", + "delete", "do", "double", "else", "enum", "export", "extends", "false", "final", "finally", "float", "for", "function", "goto", + "if", "implements", "import", "ininstanceof", "intinterface", "long", "native", "new", "null", "package", "private", + "protected", "public", "return", "short", "static", "super", "switch", "synchronized", "this", "throw", "throws", + "transient", "true", "try", "typeof", "var", "volatile", "void", "while", "with"], function(key){ JS_KEYWORDS[key] = true;} ); function getterFn(path){ @@ -74,7 +76,7 @@ function getterFn(path){ code += ' type = angular.Global.typeOf(last);\n'; code += ' fn = (angular[type.charAt(0).toUpperCase() + type.substring(1)]||{})["' + name + '"];\n'; code += ' if (fn)\n'; - code += ' self = function(){ return fn.apply(last, [last].concat(slice.call(arguments, 0, arguments.length))); };\n'; + code += ' self = function(){ return fn.apply(last, [last].concat(Array.prototype.slice.call(arguments, 0, arguments.length))); };\n'; code += ' }\n'; } }); @@ -87,9 +89,8 @@ function getterFn(path){ /////////////////////////////////// -var compileCache = {}; function expressionCompile(exp){ - if (isFunction(exp)) return exp; + if (typeof exp === 'function') return exp; var fn = compileCache[exp]; if (!fn) { var parser = new Parser(exp); @@ -107,7 +108,6 @@ function errorHandlerFor(element, error) { elementError(element, NG_EXCEPTION, isDefined(error) ? toJson(error) : error); } -var scopeId = 0; function createScope(parent, services, existing) { function Parent(){} function API(){} @@ -129,24 +129,32 @@ function createScope(parent, services, existing) { $set: bind(instance, setter, instance), $eval: function $eval(exp) { - if (exp !== undefined) { - return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length)); - } else { + var type = typeof exp; + if (type == 'undefined') { for ( var i = 0, iSize = evalLists.sorted.length; i < iSize; i++) { for ( var queue = evalLists.sorted[i], - jSize = queue.length, - j= 0; j < jSize; j++) { + jSize = queue.length, + j= 0; j < jSize; j++) { instance.$tryEval(queue[j].fn, queue[j].handler); } } + } else if (type === 'function') { + return exp.call(instance); + } else if (type === 'string') { + return expressionCompile(exp).call(instance); } }, $tryEval: function (expression, exceptionHandler) { + var type = typeof expression; try { - return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length)); + if (type == 'function') { + return expression.call(instance); + } else if (type == 'string'){ + return expressionCompile(expression).call(instance); + } } catch (e) { - error(e); + (instance.$log || {error:error}).error(e); if (isFunction(exceptionHandler)) { exceptionHandler(e); } else if (exceptionHandler) { @@ -160,12 +168,15 @@ function createScope(parent, services, existing) { $watch: function(watchExp, listener, exceptionHandler) { var watch = expressionCompile(watchExp), last; + listener = expressionCompile(listener); function watcher(){ var value = watch.call(instance), lastValue = last; if (last !== value) { last = value; - instance.$tryEval(listener, exceptionHandler, value, lastValue); + instance.$tryEval(function(){ + return listener.call(instance, value, lastValue); + }, exceptionHandler); } } instance.$onEval(PRIORITY_WATCH, watcher); diff --git a/src/angular-bootstrap.js b/src/angular-bootstrap.js index 90e1104e..1f03b8a3 100644 --- a/src/angular-bootstrap.js +++ b/src/angular-bootstrap.js @@ -22,16 +22,14 @@ * THE SOFTWARE. */ (function(previousOnLoad){ - var filename = /(.*)\/angular-(.*).js(#(.*))?/, + var filename = /^(.*)\/angular-bootstrap.js(#.*)?$/, scripts = document.getElementsByTagName("SCRIPT"), serverPath, - config, match; for(var j = 0; j < scripts.length; j++) { match = (scripts[j].src || "").match(filename); if (match) { serverPath = match[1]; - config = match[4]; } } @@ -63,7 +61,7 @@ try { if (previousOnLoad) previousOnLoad(); } catch(e) {} - angularInit(parseKeyValue(config)); + angularInit(angularJsConfig(document)); }; })(window.onload); diff --git a/src/angular.suffix b/src/angular.suffix index 36d73df2..7e86c5d5 100644 --- a/src/angular.suffix +++ b/src/angular.suffix @@ -3,7 +3,7 @@ try { if (previousOnLoad) previousOnLoad(); } catch(e) {} - angularInit(parseKeyValue(angularJsConfig(document))); + angularInit(angularJsConfig(document)); }; })(window, document, window.onload); diff --git a/src/apis.js b/src/apis.js index 306d0ce8..0cf24016 100644 --- a/src/apis.js +++ b/src/apis.js @@ -12,24 +12,15 @@ var angularGlobal = { }; var angularCollection = { - 'size': size + 'copy': copy, + 'size': size, + 'equals': equals }; var angularObject = { 'extend': extend }; var angularArray = { 'indexOf': indexOf, - 'include': includes, - 'includeIf':function(array, value, condition) { - var index = indexOf(array, value); - if (condition) { - if (index == -1) - array.push(value); - } else { - array.splice(index, 1); - } - return array; - }, 'sum':function(array, expression) { var fn = angular['Function']['compile'](expression); var sum = 0; @@ -47,20 +38,6 @@ var angularArray = { array.splice(index, 1); return value; }, - 'find':function(array, condition, defaultValue) { - if (!condition) return undefined; - var fn = angular['Function']['compile'](condition); - foreach(array, function($){ - if (fn($)){ - defaultValue = $; - return true; - } - }); - return defaultValue; - }, - 'findById':function(array, id) { - return angular.Array.find(array, function($){return $.$id == id;}, null); - }, 'filter':function(array, expression) { var predicates = []; predicates.check = function(value) { @@ -193,52 +170,6 @@ var angularArray = { var arrayCopy = []; for ( var i = 0; i < array.length; i++) { arrayCopy.push(array[i]); } return arrayCopy.sort(reverse(comparator, descend)); - }, - 'orderByToggle':function(predicate, attribute) { - var STRIP = /^([+|-])?(.*)/; - var ascending = false; - var index = -1; - foreach(predicate, function($, i){ - if (index == -1) { - if ($ == attribute) { - ascending = true; - index = i; - return true; - } - if (($.charAt(0)=='+'||$.charAt(0)=='-') && $.substring(1) == attribute) { - ascending = $.charAt(0) == '+'; - index = i; - return true; - } - } - }); - if (index >= 0) { - predicate.splice(index, 1); - } - predicate.unshift((ascending ? "-" : "+") + attribute); - return predicate; - }, - 'orderByDirection':function(predicate, attribute, ascend, descend) { - ascend = ascend || 'ng-ascend'; - descend = descend || 'ng-descend'; - var att = predicate[0] || ''; - var direction = true; - if (att.charAt(0) == '-') { - att = att.substring(1); - direction = false; - } else if(att.charAt(0) == '+') { - att = att.substring(1); - } - return att == attribute ? (direction ? ascend : descend) : ""; - }, - 'merge':function(array, index, mergeValue) { - var value = array[index]; - if (!value) { - value = {}; - array[index] = value; - } - merge(mergeValue, value); - return array; } }; diff --git a/src/delete/Binder.js b/src/delete/Binder.js deleted file mode 100644 index 095e2b08..00000000 --- a/src/delete/Binder.js +++ /dev/null @@ -1,356 +0,0 @@ -function Binder(doc, widgetFactory, datastore, location, config) { - this.doc = doc; - this.location = location; - this.datastore = datastore; - this.anchor = {}; - this.widgetFactory = widgetFactory; - this.config = config || {}; - this.updateListeners = []; -} - -Binder.parseBindings = function(string) { - var results = []; - var lastIndex = 0; - var index; - while((index = string.indexOf('{{', lastIndex)) > -1) { - if (lastIndex < index) - results.push(string.substr(lastIndex, index - lastIndex)); - lastIndex = index; - - index = string.indexOf('}}', index); - index = index < 0 ? string.length : index + 2; - - results.push(string.substr(lastIndex, index - lastIndex)); - lastIndex = index; - } - if (lastIndex != string.length) - results.push(string.substr(lastIndex, string.length - lastIndex)); - return results.length === 0 ? [ string ] : results; -}; - -Binder.hasBinding = function(string) { - var bindings = Binder.parseBindings(string); - return bindings.length > 1 || Binder.binding(bindings[0]) !== null; -}; - -Binder.binding = function(string) { - var binding = string.replace(/\n/gm, ' ').match(/^\{\{(.*)\}\}$/); - return binding ? binding[1] : null; -}; - - -Binder.prototype = { - parseQueryString: function(query) { - var params = {}; - query.replace(/(?:^|&)([^&=]*)=?([^&]*)/g, - function (match, left, right) { - if (left) params[decodeURIComponent(left)] = decodeURIComponent(right); - }); - return params; - }, - - parseAnchor: function() { - var self = this, url = this.location['get']() || ""; - - var anchorIndex = url.indexOf('#'); - if (anchorIndex < 0) return; - var anchor = url.substring(anchorIndex + 1); - - var anchorQuery = this.parseQueryString(anchor); - foreach(self.anchor, function(newValue, key) { - delete self.anchor[key]; - }); - foreach(anchorQuery, function(newValue, key) { - self.anchor[key] = newValue; - }); - }, - - onUrlChange: function() { - this.parseAnchor(); - this.updateView(); - }, - - updateAnchor: function() { - var url = this.location['get']() || ""; - var anchorIndex = url.indexOf('#'); - if (anchorIndex > -1) - url = url.substring(0, anchorIndex); - url += "#"; - var sep = ''; - for (var key in this.anchor) { - var value = this.anchor[key]; - if (typeof value === 'undefined' || value === null) { - delete this.anchor[key]; - } else { - url += sep + encodeURIComponent(key); - if (value !== true) - url += "=" + encodeURIComponent(value); - sep = '&'; - } - } - this.location['set'](url); - return url; - }, - - updateView: function() { - var start = new Date().getTime(); - var scope = jQuery(this.doc).scope(); - scope.clearInvalid(); - scope.updateView(); - var end = new Date().getTime(); - this.updateAnchor(); - foreach(this.updateListeners, function(fn) {fn();}); - }, - - docFindWithSelf: function(exp){ - var doc = jQuery(this.doc); - var selection = doc.find(exp); - if (doc.is(exp)){ - selection = selection.andSelf(); - } - return selection; - }, - - executeInit: function() { - this.docFindWithSelf("[ng:init]").each(function() { - var jThis = jQuery(this); - var scope = jThis.scope(); - try { - scope.eval(jThis.attr('ng:init')); - } catch (e) { - alert("EVAL ERROR:\n" + jThis.attr('ng:init') + '\n' + toJson(e, true)); - } - }); - }, - - entity: function (scope) { - var self = this; - this.docFindWithSelf("[ng-entity]").attr("ng:watch", function() { - try { - var jNode = jQuery(this); - var decl = scope.entity(jNode.attr("ng-entity"), self.datastore); - return decl + (jNode.attr('ng:watch') || ""); - } catch (e) { - log(e); - alert(e); - } - }); - }, - - compile: function() { - var jNode = jQuery(this.doc); - if (this.config['autoSubmit']) { - var submits = this.docFindWithSelf(":submit").not("[ng-action]"); - submits.attr("ng-action", "$save()"); - submits.not(":disabled").not("ng:bind-attr").attr("ng:bind-attr", '{disabled:"{{$invalidWidgets}}"}'); - } - this.precompile(this.doc)(this.doc, jNode.scope(), ""); - this.docFindWithSelf("a[ng-action]").live('click', function (event) { - var jNode = jQuery(this); - var scope = jNode.scope(); - try { - scope.eval(jNode.attr('ng-action')); - jNode.removeAttr('ng-error'); - jNode.removeClass("ng-exception"); - } catch (e) { - jNode.addClass("ng-exception"); - jNode.attr('ng-error', toJson(e, true)); - } - scope.get('$updateView')(); - return false; - }); - }, - - translateBinding: function(node, parentPath, factories) { - var path = parentPath.concat(); - var offset = path.pop(); - var parts = Binder.parseBindings(node.nodeValue); - if (parts.length > 1 || Binder.binding(parts[0])) { - var parent = node.parentNode; - if (isLeafNode(parent)) { - parent.setAttribute('ng:bind-template', node.nodeValue); - factories.push({path:path, fn:function(node, scope, prefix) { - return new BindUpdater(node, node.getAttribute('ng:bind-template')); - }}); - } else { - for (var i = 0; i < parts.length; i++) { - var part = parts[i]; - var binding = Binder.binding(part); - var newNode; - if (binding) { - newNode = document.createElement("span"); - var jNewNode = jQuery(newNode); - jNewNode.attr("ng:bind", binding); - if (i === 0) { - factories.push({path:path.concat(offset + i), fn:this.ng_bind}); - } - } else if (msie && part.charAt(0) == ' ') { - newNode = document.createElement("span"); - newNode.innerHTML = ' ' + part.substring(1); - } else { - newNode = document.createTextNode(part); - } - parent.insertBefore(newNode, node); - } - } - parent.removeChild(node); - } - }, - - precompile: function(root) { - var factories = []; - this.precompileNode(root, [], factories); - return function (template, scope, prefix) { - var len = factories.length; - for (var i = 0; i < len; i++) { - var factory = factories[i]; - var node = template; - var path = factory.path; - for (var j = 0; j < path.length; j++) { - node = node.childNodes[path[j]]; - } - try { - scope.addWidget(factory.fn(node, scope, prefix)); - } catch (e) { - alert(e); - } - } - }; - }, - - precompileNode: function(node, path, factories) { - var nodeType = node.nodeType; - if (nodeType == Node.TEXT_NODE) { - this.translateBinding(node, path, factories); - return; - } else if (nodeType != Node.ELEMENT_NODE && nodeType != Node.DOCUMENT_NODE) { - return; - } - - if (!node.getAttribute) return; - var nonBindable = node.getAttribute('ng:non-bindable'); - if (nonBindable || nonBindable === "") return; - - var attributes = node.attributes; - if (attributes) { - var bindings = node.getAttribute('ng:bind-attr'); - node.removeAttribute('ng:bind-attr'); - bindings = bindings ? fromJson(bindings) : {}; - var attrLen = attributes.length; - for (var i = 0; i < attrLen; i++) { - var attr = attributes[i]; - var attrName = attr.name; - // http://www.glennjones.net/Post/809/getAttributehrefbug.htm - var attrValue = msie && attrName == 'href' ? - decodeURI(node.getAttribute(attrName, 2)) : attr.value; - if (Binder.hasBinding(attrValue)) { - bindings[attrName] = attrValue; - } - } - var json = toJson(bindings); - if (json.length > 2) { - node.setAttribute("ng:bind-attr", json); - } - } - - if (!node.getAttribute) log(node); - var repeaterExpression = node.getAttribute('ng:repeat'); - if (repeaterExpression) { - node.removeAttribute('ng:repeat'); - var precompiled = this.precompile(node); - var view = document.createComment("ng:repeat: " + repeaterExpression); - var parentNode = node.parentNode; - parentNode.insertBefore(view, node); - parentNode.removeChild(node); - function template(childScope, prefix, i) { - var clone = jQuery(node).clone(); - clone.css('display', ''); - clone.attr('ng:repeat-index', "" + i); - clone.data('scope', childScope); - precompiled(clone[0], childScope, prefix + i + ":"); - return clone; - } - factories.push({path:path, fn:function(node, scope, prefix) { - return new RepeaterUpdater(jQuery(node), repeaterExpression, template, prefix); - }}); - return; - } - - if (node.getAttribute('ng:eval')) factories.push({path:path, fn:this.ng_eval}); - if (node.getAttribute('ng:bind')) factories.push({path:path, fn:this.ng_bind}); - if (node.getAttribute('ng:bind-attr')) factories.push({path:path, fn:this.ng_bind_attr}); - if (node.getAttribute('ng:hide')) factories.push({path:path, fn:this.ng_hide}); - if (node.getAttribute('ng:show')) factories.push({path:path, fn:this.ng_show}); - if (node.getAttribute('ng:class')) factories.push({path:path, fn:this.ng_class}); - if (node.getAttribute('ng:class-odd')) factories.push({path:path, fn:this.ng_class_odd}); - if (node.getAttribute('ng:class-even')) factories.push({path:path, fn:this.ng_class_even}); - if (node.getAttribute('ng:style')) factories.push({path:path, fn:this.ng_style}); - if (node.getAttribute('ng:watch')) factories.push({path:path, fn:this.ng_watch}); - var nodeName = node.nodeName; - if ((nodeName == 'INPUT' ) || - nodeName == 'TEXTAREA' || - nodeName == 'SELECT' || - nodeName == 'BUTTON') { - var self = this; - factories.push({path:path, fn:function(node, scope, prefix) { - node.name = prefix + node.name.split(":").pop(); - return self.widgetFactory.createController(jQuery(node), scope); - }}); - } - if (nodeName == 'OPTION') { - var html = jQuery('<select/>').append(jQuery(node).clone()).html(); - if (!html.match(/<option(\s.*\s|\s)value\s*=\s*.*>.*<\/\s*option\s*>/gi)) { - if (Binder.hasBinding(node.text)) { - jQuery(node).attr('ng:bind-attr', angular.toJson({'value':node.text})); - } else { - node.value = node.text; - } - } - } - - var children = node.childNodes; - for (var k = 0; k < children.length; k++) { - this.precompileNode(children[k], path.concat(k), factories); - } - }, - - ng_eval: function(node) { - return new EvalUpdater(node, node.getAttribute('ng:eval')); - }, - - ng_bind: function(node) { - return new BindUpdater(node, "{{" + node.getAttribute('ng:bind') + "}}"); - }, - - ng_bind_attr: function(node) { - return new BindAttrUpdater(node, fromJson(node.getAttribute('ng:bind-attr'))); - }, - - ng_hide: function(node) { - return new HideUpdater(node, node.getAttribute('ng:hide')); - }, - - ng_show: function(node) { - return new ShowUpdater(node, node.getAttribute('ng:show')); - }, - - ng_class: function(node) { - return new ClassUpdater(node, node.getAttribute('ng:class')); - }, - - ng_class_even: function(node) { - return new ClassEvenUpdater(node, node.getAttribute('ng:class-even')); - }, - - ng_class_odd: function(node) { - return new ClassOddUpdater(node, node.getAttribute('ng:class-odd')); - }, - - ng_style: function(node) { - return new StyleUpdater(node, node.getAttribute('ng:style')); - }, - - ng_watch: function(node, scope) { - scope.watch(node.getAttribute('ng:watch')); - } -}; diff --git a/src/delete/Model.js b/src/delete/Model.js deleted file mode 100644 index b09efd0e..00000000 --- a/src/delete/Model.js +++ /dev/null @@ -1,65 +0,0 @@ -// Single $ is special and does not get searched -// Double $$ is special an is client only (does not get sent to server) - -function Model(entity, initial) { - this['$$entity'] = entity; - this['$loadFrom'](initial||{}); - this['$entity'] = entity['title']; - this['$migrate'](); -}; - -Model.copyDirectFields = function(src, dst) { - if (src === dst || !src || !dst) return; - var isDataField = function(src, dst, field) { - return (field.substring(0,2) !== '$$') && - (typeof src[field] !== 'function') && - (typeof dst[field] !== 'function'); - }; - for (var field in dst) { - if (isDataField(src, dst, field)) - delete dst[field]; - } - for (field in src) { - if (isDataField(src, dst, field)) - dst[field] = src[field]; - } -}; - -extend(Model.prototype, { - '$migrate': function() { - merge(this['$$entity']['defaults'], this); - return this; - }, - - '$merge': function(other) { - merge(other, this); - return this; - }, - - '$save': function(callback) { - this['$$entity'].datastore.save(this, callback === true ? undefined : callback); - if (callback === true) this['$$entity'].datastore.flush(); - return this; - }, - - '$delete': function(callback) { - this['$$entity'].datastore.remove(this, callback === true ? undefined : callback); - if (callback === true) this['$$entity'].datastore.flush(); - return this; - }, - - '$loadById': function(id, callback) { - this['$$entity'].datastore.load(this, id, callback); - return this; - }, - - '$loadFrom': function(other) { - Model.copyDirectFields(other, this); - return this; - }, - - '$saveTo': function(other) { - Model.copyDirectFields(this, other); - return this; - } -});
\ No newline at end of file diff --git a/src/delete/Scope.js b/src/delete/Scope.js deleted file mode 100644 index ae3f9f11..00000000 --- a/src/delete/Scope.js +++ /dev/null @@ -1,407 +0,0 @@ -function Scope(initialState, name) { - var self = this; - self.widgets = []; - self.evals = []; - self.watchListeners = {}; - self.name = name; - initialState = initialState || {}; - var State = function(){}; - State.prototype = initialState; - self.state = new State(); - extend(self.state, { - '$parent': initialState, - '$watch': bind(self, self.addWatchListener), - '$eval': bind(self, self.eval), - '$bind': bind(self, bind, self), - // change name to autoEval? - '$addEval': bind(self, self.addEval), - '$updateView': bind(self, self.updateView) - }); - if (name == "ROOT") { - self.state['$root'] = self.state; - } -}; - -Scope.expressionCache = {}; -Scope.getter = function(instance, path) { - if (!path) return instance; - var element = path.split('.'); - var key; - var lastInstance = instance; - var len = element.length; - for ( var i = 0; i < len; i++) { - key = element[i]; - if (!key.match(/^[\$\w][\$\w\d]*$/)) - throw "Expression '" + path + "' is not a valid expression for accesing variables."; - if (instance) { - lastInstance = instance; - instance = instance[key]; - } - if (_.isUndefined(instance) && key.charAt(0) == '$') { - var type = angular['Global']['typeOf'](lastInstance); - type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; - var fn = type ? type[[key.substring(1)]] : undefined; - if (fn) { - instance = _.bind(fn, lastInstance, lastInstance); - return instance; - } - } - } - if (typeof instance === 'function' && !instance['$$factory']) { - return bind(lastInstance, instance); - } - return instance; -}; - -Scope.setter = function(instance, path, value){ - var element = path.split('.'); - for ( var i = 0; element.length > 1; i++) { - var key = element.shift(); - var newInstance = instance[key]; - if (!newInstance) { - newInstance = {}; - instance[key] = newInstance; - } - instance = newInstance; - } - instance[element.shift()] = value; - return value; -}; - -Scope.prototype = { - // TODO: rename to update? or eval? - updateView: function() { - var self = this; - this.fireWatchers(); - foreach(this.widgets, function(widget){ - self.evalWidget(widget, "", {}, function(){ - this.updateView(self); - }); - }); - foreach(this.evals, bind(this, this.apply)); - }, - - addWidget: function(controller) { - if (controller) this.widgets.push(controller); - }, - - addEval: function(fn, listener) { - // todo: this should take a function/string and a listener - // todo: this is a hack, which will need to be cleaned up. - var self = this, - listenFn = listener || noop, - expr = self.compile(fn); - this.evals.push(function(){ - self.apply(listenFn, expr()); - }); - }, - - isProperty: function(exp) { - for ( var i = 0; i < exp.length; i++) { - var ch = exp.charAt(i); - if (ch!='.' && !Lexer.prototype.isIdent(ch)) { - return false; - } - } - return true; - }, - - get: function(path) { -// log('SCOPE.get', path, Scope.getter(this.state, path)); - return Scope.getter(this.state, path); - }, - - set: function(path, value) { -// log('SCOPE.set', path, value); - var instance = this.state; - return Scope.setter(instance, path, value); - }, - - setEval: function(expressionText, value) { - this.eval(expressionText + "=" + toJson(value)); - }, - - compile: function(exp) { - if (isFunction(exp)) return bind(this.state, exp); - var expFn = Scope.expressionCache[exp], self = this; - if (!expFn) { - var parser = new Parser(exp); - expFn = parser.statements(); - parser.assertAllConsumed(); - Scope.expressionCache[exp] = expFn; - } - return function(context){ - context = context || {}; - context.self = self.state; - context.scope = self; - return expFn.call(self, context); - }; - }, - - eval: function(exp, context) { -// log('Scope.eval', expressionText); - return this.compile(exp)(context); - }, - - //TODO: Refactor. This function needs to be an execution closure for widgets - // move to widgets - // remove expression, just have inner closure. - evalWidget: function(widget, expression, context, onSuccess, onFailure) { - try { - var value = this.eval(expression, context); - if (widget.hasError) { - widget.hasError = false; - jQuery(widget.view). - removeClass('ng-exception'). - removeAttr('ng-error'); - } - if (onSuccess) { - value = onSuccess.apply(widget, [value]); - } - return true; - } catch (e){ - var jsonError = toJson(e, true); - error('Eval Widget Error:', jsonError); - widget.hasError = true; - jQuery(widget.view). - addClass('ng-exception'). - attr('ng-error', jsonError); - if (onFailure) { - onFailure.apply(widget, [e, jsonError]); - } - return false; - } - }, - - validate: function(expressionText, value, element) { - var expression = Scope.expressionCache[expressionText]; - if (!expression) { - expression = new Parser(expressionText).validator(); - Scope.expressionCache[expressionText] = expression; - } - var self = {scope:this, self:this.state, '$element':element}; - return expression(self)(self, value); - }, - - entity: function(entityDeclaration, datastore) { - var expression = new Parser(entityDeclaration).entityDeclaration(); - return expression({scope:this, datastore:datastore}); - }, - - clearInvalid: function() { - var invalid = this.state['$invalidWidgets']; - while(invalid.length > 0) {invalid.pop();} - }, - - markInvalid: function(widget) { - this.state['$invalidWidgets'].push(widget); - }, - - watch: function(declaration) { - var self = this; - new Parser(declaration).watch()({ - scope:this, - addListener:function(watch, exp){ - self.addWatchListener(watch, function(n,o){ - try { - return exp({scope:self}, n, o); - } catch(e) { - alert(e); - } - }); - } - }); - }, - - addWatchListener: function(watchExpression, listener) { - // TODO: clean me up! - if (!isFunction(listener)) { - listener = this.compile(listener); - } - var watcher = this.watchListeners[watchExpression]; - if (!watcher) { - watcher = {listeners:[], expression:watchExpression}; - this.watchListeners[watchExpression] = watcher; - } - watcher.listeners.push(listener); - }, - - fireWatchers: function() { - var self = this, fired = false; - foreach(this.watchListeners, function(watcher) { - var value = self.eval(watcher.expression); - if (value !== watcher.lastValue) { - foreach(watcher.listeners, function(listener){ - listener(value, watcher.lastValue); - fired = true; - }); - watcher.lastValue = value; - } - }); - return fired; - }, - - apply: function(fn) { - fn.apply(this.state, slice.call(arguments, 1, arguments.length)); - } -}; - -////////////////////////////// - -function getter(instance, path) { - if (!path) return instance; - var element = path.split('.'); - var key; - var lastInstance = instance; - var len = element.length; - for ( var i = 0; i < len; i++) { - key = element[i]; - if (!key.match(/^[\$\w][\$\w\d]*$/)) - throw "Expression '" + path + "' is not a valid expression for accesing variables."; - if (instance) { - lastInstance = instance; - instance = instance[key]; - } - if (_.isUndefined(instance) && key.charAt(0) == '$') { - var type = angular['Global']['typeOf'](lastInstance); - type = angular[type.charAt(0).toUpperCase()+type.substring(1)]; - var fn = type ? type[[key.substring(1)]] : undefined; - if (fn) { - instance = _.bind(fn, lastInstance, lastInstance); - return instance; - } - } - } - if (typeof instance === 'function' && !instance['$$factory']) { - return bind(lastInstance, instance); - } - return instance; -}; - -function setter(instance, path, value){ - var element = path.split('.'); - for ( var i = 0; element.length > 1; i++) { - var key = element.shift(); - var newInstance = instance[key]; - if (!newInstance) { - newInstance = {}; - instance[key] = newInstance; - } - instance = newInstance; - } - instance[element.shift()] = value; - return value; -}; - -var compileCache = {}; -function expressionCompile(exp){ - if (isFunction(exp)) return exp; - var expFn = compileCache[exp]; - if (!expFn) { - var parser = new Parser(exp); - expFn = parser.statements(); - parser.assertAllConsumed(); - compileCache[exp] = expFn; - } - // return expFn - // TODO(remove this hack) - return function(){ - return expFn({ - scope: { - set: this.$set, - get: this.$get - } - }); - }; -}; - -var NON_RENDERABLE_ELEMENTS = { - '#text': 1, '#comment':1, 'TR':1, 'TH':1 -}; - -function isRenderableElement(element){ - return element && element[0] && !NON_RENDERABLE_ELEMENTS[element[0].nodeName]; -} - -function rethrow(e) { throw e; } -function errorHandlerFor(element) { - while (!isRenderableElement(element)) { - element = element.parent() || jqLite(document.body); - } - return function(error) { - element.attr('ng-error', angular.toJson(error)); - element.addClass('ng-exception'); - }; -} - -function createScope(parent, Class) { - function Parent(){} - function API(){} - function Behavior(){} - - var instance, behavior, api, watchList = [], evalList = []; - - Class = Class || noop; - parent = Parent.prototype = parent || {}; - api = API.prototype = new Parent(); - behavior = Behavior.prototype = extend(new API(), Class.prototype); - instance = new Behavior(); - - extend(api, { - $parent: parent, - $bind: bind(instance, bind, instance), - $get: bind(instance, getter, instance), - $set: bind(instance, setter, instance), - - $eval: function(exp) { - if (isDefined(exp)) { - return expressionCompile(exp).apply(instance, slice.call(arguments, 1, arguments.length)); - } else { - foreach(evalList, function(eval) { - instance.$tryEval(eval.fn, eval.handler); - }); - foreach(watchList, function(watch) { - var value = instance.$tryEval(watch.watch, watch.handler); - if (watch.last !== value) { - instance.$tryEval(watch.listener, watch.handler, value, watch.last); - watch.last = value; - } - }); - } - }, - - $tryEval: function (expression, exceptionHandler) { - try { - return expressionCompile(expression).apply(instance, slice.call(arguments, 2, arguments.length)); - } catch (e) { - error(e); - if (isFunction(exceptionHandler)) { - exceptionHandler(e); - } else if (exceptionHandler) { - errorHandlerFor(exceptionHandler)(e); - } - } - }, - - $watch: function(watchExp, listener, exceptionHandler) { - var watch = expressionCompile(watchExp); - watchList.push({ - watch: watch, - last: watch.call(instance), - handler: exceptionHandler, - listener:expressionCompile(listener) - }); - }, - - $onEval: function(expr, exceptionHandler){ - evalList.push({ - fn: expressionCompile(expr), - handler: exceptionHandler - }); - } - }); - - Class.apply(instance, slice.call(arguments, 2, arguments.length)); - - return instance; -} diff --git a/src/delete/Widgets.js b/src/delete/Widgets.js deleted file mode 100644 index 53536ed0..00000000 --- a/src/delete/Widgets.js +++ /dev/null @@ -1,806 +0,0 @@ -function WidgetFactory(serverUrl, database) { - this.nextUploadId = 0; - this.serverUrl = serverUrl; - this.database = database; - if (window['swfobject']) { - this.createSWF = window['swfobject']['createSWF']; - } else { - this.createSWF = function(){ - alert("ERROR: swfobject not loaded!"); - }; - } -}; - -WidgetFactory.prototype = { - createController: function(input, scope) { - var controller; - var type = input.attr('type').toLowerCase(); - var exp = input.attr('name'); - if (exp) exp = exp.split(':').pop(); - var event = "change"; - var bubbleEvent = true; - var formatter = angularFormatter[input.attr('ng:format')] || angularFormatter['noop']; - if (type == 'button' || type == 'submit' || type == 'reset' || type == 'image') { - controller = new ButtonController(input[0], exp, formatter); - event = "click"; - bubbleEvent = false; - } else if (type == 'text' || type == 'textarea' || type == 'hidden' || type == 'password') { - controller = new TextController(input[0], exp, formatter); - event = "keyup change"; - } else if (type == 'checkbox') { - controller = new CheckboxController(input[0], exp, formatter); - event = "click"; - } else if (type == 'radio') { - controller = new RadioController(input[0], exp, formatter); - event="click"; - } else if (type == 'select-one') { - controller = new SelectController(input[0], exp, formatter); - } else if (type == 'select-multiple') { - controller = new MultiSelectController(input[0], exp, formatter); - } else if (type == 'file') { - controller = this.createFileController(input, exp, formatter); - } else { - throw 'Unknown type: ' + type; - } - input.data('controller', controller); - var updateView = scope.get('$updateView'); - var action = function() { - if (controller.updateModel(scope)) { - var action = jQuery(controller.view).attr('ng-action') || ""; - if (scope.evalWidget(controller, action)) { - updateView(scope); - } - } - return bubbleEvent; - }; - jQuery(controller.view, ":input"). - bind(event, action); - return controller; - }, - - createFileController: function(fileInput) { - var uploadId = '__uploadWidget_' + (this.nextUploadId++); - var view = FileController.template(uploadId); - fileInput.after(view); - var att = { - 'data':this.serverUrl + "/admin/ServerAPI.swf", - 'width':"95", 'height':"20", 'align':"top", - 'wmode':"transparent"}; - var par = { - 'flashvars':"uploadWidgetId=" + uploadId, - 'allowScriptAccess':"always"}; - var swfNode = this.createSWF(att, par, uploadId); - fileInput.remove(); - var cntl = new FileController(view, fileInput[0].name, swfNode, this.serverUrl + "/data/" + this.database); - jQuery(swfNode).parent().data('controller', cntl); - return cntl; - } -}; -///////////////////// -// FileController -/////////////////////// - -function FileController(view, scopeName, uploader, databaseUrl) { - this.view = view; - this.uploader = uploader; - this.scopeName = scopeName; - this.attachmentsPath = databaseUrl + '/_attachments'; - this.value = null; - this.lastValue = undefined; -}; - -angularCallbacks['flashEvent'] = function(id, event, args) { - var object = document.getElementById(id); - var jobject = jQuery(object); - var controller = jobject.parent().data("controller"); - FileController.prototype[event].apply(controller, args); - _.defer(jobject.scope().get('$updateView')); -}; - -FileController.template = function(id) { - return jQuery('<span class="ng-upload-widget">' + - '<input type="checkbox" ng:non-bindable="true"/>' + - '<object id="' + id + '" />' + - '<a></a>' + - '<span/>' + - '</span>'); -}; - -extend(FileController.prototype, { - 'cancel': noop, - 'complete': noop, - 'httpStatus': function(status) { - alert("httpStatus:" + this.scopeName + " status:" + status); - }, - 'ioError': function() { - alert("ioError:" + this.scopeName); - }, - 'open': function() { - alert("open:" + this.scopeName); - }, - 'progress':noop, - 'securityError': function() { - alert("securityError:" + this.scopeName); - }, - 'uploadCompleteData': function(data) { - var value = fromJson(data); - value.url = this.attachmentsPath + '/' + value.id + '/' + value.text; - this.view.find("input").attr('checked', true); - var scope = this.view.scope(); - this.value = value; - this.updateModel(scope); - this.value = null; - }, - 'select': function(name, size, type) { - this.name = name; - this.view.find("a").text(name).attr('href', name); - this.view.find("span").text(angular['filter']['bytes'](size)); - this.upload(); - }, - - updateModel: function(scope) { - var isChecked = this.view.find("input").attr('checked'); - var value = isChecked ? this.value : null; - if (this.lastValue === value) { - return false; - } else { - scope.set(this.scopeName, value); - return true; - } - }, - - updateView: function(scope) { - var modelValue = scope.get(this.scopeName); - if (modelValue && this.value !== modelValue) { - this.value = modelValue; - this.view.find("a"). - attr("href", this.value.url). - text(this.value.text); - this.view.find("span").text(angular['filter']['bytes'](this.value.size)); - } - this.view.find("input").attr('checked', !!modelValue); - }, - - upload: function() { - if (this.name) { - this.uploader['uploadFile'](this.attachmentsPath); - } - } -}); - -/////////////////////// -// NullController -/////////////////////// -function NullController(view) {this.view = view;}; -NullController.prototype = { - updateModel: function() { return true; }, - updateView: noop -}; -NullController.instance = new NullController(); - - -/////////////////////// -// ButtonController -/////////////////////// -var ButtonController = NullController; - -/////////////////////// -// TextController -/////////////////////// -function TextController(view, exp, formatter) { - this.view = view; - this.formatter = formatter; - this.exp = exp; - this.validator = view.getAttribute('ng:validate'); - this.required = typeof view.attributes['ng:required'] != "undefined"; - this.lastErrorText = null; - this.lastValue = undefined; - this.initialValue = this.formatter['parse'](view.value); - var widget = view.getAttribute('ng-widget'); - if (widget === 'datepicker') { - jQuery(view).datepicker(); - } -}; - -TextController.prototype = { - updateModel: function(scope) { - var value = this.formatter['parse'](this.view.value); - if (this.lastValue === value) { - return false; - } else { - scope.setEval(this.exp, value); - this.lastValue = value; - return true; - } - }, - - updateView: function(scope) { - var view = this.view; - var value = scope.get(this.exp); - if (typeof value === "undefined") { - value = this.initialValue; - scope.setEval(this.exp, value); - } - value = value ? value : ''; - if (!_(this.lastValue).isEqual(value)) { - view.value = this.formatter['format'](value); - this.lastValue = value; - } - - var isValidationError = false; - view.removeAttribute('ng-error'); - if (this.required) { - isValidationError = !(value && $.trim("" + value).length > 0); - } - var errorText = isValidationError ? "Required Value" : null; - if (!isValidationError && this.validator && value) { - errorText = scope.validate(this.validator, value, view); - isValidationError = !!errorText; - } - if (this.lastErrorText !== errorText) { - this.lastErrorText = isValidationError; - if (errorText && isVisible(view)) { - view.setAttribute('ng-error', errorText); - scope.markInvalid(this); - } - jQuery(view).toggleClass('ng-validation-error', isValidationError); - } - } -}; - -/////////////////////// -// CheckboxController -/////////////////////// -function CheckboxController(view, exp, formatter) { - this.view = view; - this.exp = exp; - this.lastValue = undefined; - this.formatter = formatter; - this.initialValue = this.formatter['parse'](view.checked ? view.value : ""); -}; - -CheckboxController.prototype = { - updateModel: function(scope) { - var input = this.view; - var value = input.checked ? input.value : ''; - value = this.formatter['parse'](value); - value = this.formatter['format'](value); - if (this.lastValue === value) { - return false; - } else { - scope.setEval(this.exp, this.formatter['parse'](value)); - this.lastValue = value; - return true; - } - }, - - updateView: function(scope) { - var input = this.view; - var value = scope.eval(this.exp); - if (typeof value === "undefined") { - value = this.initialValue; - scope.setEval(this.exp, value); - } - input.checked = this.formatter['parse'](input.value) == value; - } -}; - -/////////////////////// -// SelectController -/////////////////////// -function SelectController(view, exp) { - this.view = view; - this.exp = exp; - this.lastValue = undefined; - this.initialValue = view.value; -}; - -SelectController.prototype = { - updateModel: function(scope) { - var input = this.view; - if (input.selectedIndex < 0) { - scope.setEval(this.exp, null); - } else { - var value = this.view.value; - if (this.lastValue === value) { - return false; - } else { - scope.setEval(this.exp, value); - this.lastValue = value; - return true; - } - } - }, - - updateView: function(scope) { - var input = this.view; - var value = scope.get(this.exp); - if (typeof value === 'undefined') { - value = this.initialValue; - scope.setEval(this.exp, value); - } - if (value !== this.lastValue) { - input.value = value ? value : ""; - this.lastValue = value; - } - } -}; - -/////////////////////// -// MultiSelectController -/////////////////////// -function MultiSelectController(view, exp) { - this.view = view; - this.exp = exp; - this.lastValue = undefined; - this.initialValue = this.selected(); -}; - -MultiSelectController.prototype = { - selected: function () { - var value = []; - var options = this.view.options; - for ( var i = 0; i < options.length; i++) { - var option = options[i]; - if (option.selected) { - value.push(option.value); - } - } - return value; - }, - - updateModel: function(scope) { - var value = this.selected(); - // TODO: This is wrong! no caching going on here as we are always comparing arrays - if (this.lastValue === value) { - return false; - } else { - scope.setEval(this.exp, value); - this.lastValue = value; - return true; - } - }, - - updateView: function(scope) { - var input = this.view; - var selected = scope.get(this.exp); - if (typeof selected === "undefined") { - selected = this.initialValue; - scope.setEval(this.exp, selected); - } - if (selected !== this.lastValue) { - var options = input.options; - for ( var i = 0; i < options.length; i++) { - var option = options[i]; - option.selected = _.include(selected, option.value); - } - this.lastValue = selected; - } - } -}; - -/////////////////////// -// RadioController -/////////////////////// -function RadioController(view, exp) { - this.view = view; - this.exp = exp; - this.lastChecked = undefined; - this.lastValue = undefined; - this.inputValue = view.value; - this.initialValue = view.checked ? view.value : null; -}; - -RadioController.prototype = { - updateModel: function(scope) { - var input = this.view; - if (this.lastChecked) { - return false; - } else { - input.checked = true; - this.lastValue = scope.setEval(this.exp, this.inputValue); - this.lastChecked = true; - return true; - } - }, - - updateView: function(scope) { - var input = this.view; - var value = scope.get(this.exp); - if (this.initialValue && typeof value === "undefined") { - value = this.initialValue; - scope.setEval(this.exp, value); - } - if (this.lastValue != value) { - this.lastChecked = input.checked = this.inputValue == (''+value); - this.lastValue = value; - } - } -}; - -/////////////////////// -//ElementController -/////////////////////// -function BindUpdater(view, exp) { - this.view = view; - this.exp = Binder.parseBindings(exp); - this.hasError = false; -}; - -BindUpdater.toText = function(obj) { - var e = escapeHtml; - switch(typeof obj) { - case "string": - case "boolean": - case "number": - return e(obj); - case "function": - return BindUpdater.toText(obj()); - case "object": - if (isNode(obj)) { - return outerHTML(obj); - } else if (obj instanceof angular.filter.Meta) { - switch(typeof obj.html) { - case "string": - case "number": - return obj.html; - case "function": - return obj.html(); - case "object": - if (isNode(obj.html)) - return outerHTML(obj.html); - default: - break; - } - switch(typeof obj.text) { - case "string": - case "number": - return e(obj.text); - case "function": - return e(obj.text()); - default: - break; - } - } - if (obj === null) - return ""; - return e(toJson(obj, true)); - default: - return ""; - } -}; - -BindUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - var html = []; - var parts = this.exp; - var length = parts.length; - for(var i=0; i<length; i++) { - var part = parts[i]; - var binding = Binder.binding(part); - if (binding) { - scope.evalWidget(this, binding, {$element:this.view}, function(value){ - html.push(BindUpdater.toText(value)); - }, function(e, text){ - setHtml(this.view, text); - }); - if (this.hasError) { - return; - } - } else { - html.push(escapeHtml(part)); - } - } - setHtml(this.view, html.join('')); - } -}; - -function BindAttrUpdater(view, attrs) { - this.view = view; - this.attrs = attrs; -}; - -BindAttrUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - var jNode = jQuery(this.view); - var attributeTemplates = this.attrs; - if (this.hasError) { - this.hasError = false; - jNode. - removeClass('ng-exception'). - removeAttr('ng-error'); - } - var isImage = jNode.is('img'); - for (var attrName in attributeTemplates) { - var attributeTemplate = Binder.parseBindings(attributeTemplates[attrName]); - var attrValues = []; - for ( var i = 0; i < attributeTemplate.length; i++) { - var binding = Binder.binding(attributeTemplate[i]); - if (binding) { - try { - var value = scope.eval(binding, {$element:jNode[0], attrName:attrName}); - if (value && (value.constructor !== array || value.length !== 0)) - attrValues.push(value); - } catch (e) { - this.hasError = true; - error('BindAttrUpdater', e); - var jsonError = toJson(e, true); - attrValues.push('[' + jsonError + ']'); - jNode. - addClass('ng-exception'). - attr('ng-error', jsonError); - } - } else { - attrValues.push(attributeTemplate[i]); - } - } - var attrValue = attrValues.length ? attrValues.join('') : null; - if(isImage && attrName == 'src' && !attrValue) - attrValue = scope.get('$config.blankImage'); - jNode.attr(attrName, attrValue); - } - } -}; - -function EvalUpdater(view, exp) { - this.view = view; - this.exp = exp; - this.hasError = false; -}; -EvalUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp); - } -}; - -function HideUpdater(view, exp) { this.view = view; this.exp = exp; }; -HideUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp, {}, function(hideValue){ - var view = jQuery(this.view); - if (toBoolean(hideValue)) { - view.hide(); - } else { - view.show(); - } - }); - } -}; - -function ShowUpdater(view, exp) { this.view = view; this.exp = exp; }; -ShowUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp, {}, function(hideValue){ - var view = jQuery(this.view); - if (toBoolean(hideValue)) { - view.show(); - } else { - view.hide(); - } - }); - } -}; - -function ClassUpdater(view, exp) { this.view = view; this.exp = exp; }; -ClassUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp, {}, function(classValue){ - if (classValue !== null && classValue !== undefined) { - this.view.className = classValue; - } - }); - } -}; - -function ClassEvenUpdater(view, exp) { this.view = view; this.exp = exp; }; -ClassEvenUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp, {}, function(classValue){ - var index = scope.get('$index'); - jQuery(this.view).toggleClass(classValue, index % 2 === 1); - }); - } -}; - -function ClassOddUpdater(view, exp) { this.view = view; this.exp = exp; }; -ClassOddUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp, {}, function(classValue){ - var index = scope.get('$index'); - jQuery(this.view).toggleClass(classValue, index % 2 === 0); - }); - } -}; - -function StyleUpdater(view, exp) { this.view = view; this.exp = exp; }; -StyleUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.exp, {}, function(styleValue){ - jQuery(this.view).attr('style', "").css(styleValue); - }); - } -}; - -/////////////////////// -// RepeaterUpdater -/////////////////////// -function RepeaterUpdater(view, repeaterExpression, template, prefix) { - this.view = view; - this.template = template; - this.prefix = prefix; - this.children = []; - var match = repeaterExpression.match(/^\s*(.+)\s+in\s+(.*)\s*$/); - if (! match) { - throw "Expected ng:repeat in form of 'item in collection' but got '" + - repeaterExpression + "'."; - } - var keyValue = match[1]; - this.iteratorExp = match[2]; - match = keyValue.match(/^([\$\w]+)|\(([\$\w]+)\s*,\s*([\$\w]+)\)$/); - if (!match) { - throw "'item' in 'item in collection' should be identifier or (key, value) but get '" + - keyValue + "'."; - } - this.valueExp = match[3] || match[1]; - this.keyExp = match[2]; -}; - -RepeaterUpdater.prototype = { - updateModel: noop, - updateView: function(scope) { - scope.evalWidget(this, this.iteratorExp, {}, function(iterator){ - var self = this; - if (!iterator) { - iterator = []; - if (scope.isProperty(this.iteratorExp)) { - scope.set(this.iteratorExp, iterator); - } - } - var childrenLength = this.children.length; - var cursor = this.view; - var time = 0; - var child = null; - var keyExp = this.keyExp; - var valueExp = this.valueExp; - var iteratorCounter = 0; - foreach(iterator, function(value, key){ - if (iteratorCounter < childrenLength) { - // reuse children - child = self.children[iteratorCounter]; - child.scope.set(valueExp, value); - } else { - // grow children - var name = self.prefix + - valueExp + " in " + self.iteratorExp + "[" + iteratorCounter + "]"; - var childScope = new Scope(scope.state, name); - childScope.set('$index', iteratorCounter); - if (keyExp) - childScope.set(keyExp, key); - childScope.set(valueExp, value); - child = { scope:childScope, element:self.template(childScope, self.prefix, iteratorCounter) }; - cursor.after(child.element); - self.children.push(child); - } - cursor = child.element; - var s = new Date().getTime(); - child.scope.updateView(); - time += new Date().getTime() - s; - iteratorCounter++; - }); - // shrink children - for ( var r = childrenLength; r > iteratorCounter; --r) { - this.children.pop().element.remove(); - } - // Special case for option in select - if (child && child.element[0].nodeName === "OPTION") { - var select = jQuery(child.element[0].parentNode); - var cntl = select.data('controller'); - if (cntl) { - cntl.lastValue = undefined; - cntl.updateView(scope); - } - } - }); - } -}; - -////////////////////////////////// -// PopUp -////////////////////////////////// - -function PopUp(doc) { - this.doc = doc; -}; - -PopUp.OUT_EVENT = "mouseleave mouseout click dblclick keypress keyup"; - -PopUp.onOver = function(e) { - PopUp.onOut(); - var jNode = jQuery(this); - jNode.bind(PopUp.OUT_EVENT, PopUp.onOut); - var position = jNode.position(); - var de = document.documentElement; - var w = self.innerWidth || (de && de.clientWidth) || document.body.clientWidth; - var hasArea = w - position.left; - var width = 300; - var title = jNode.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."; - var msg = jNode.attr("ng-error"); - - var x; - var arrowPos = hasArea>(width+75) ? "left" : "right"; - var tip = jQuery( - "<div id='ng-callout' style='width:"+width+"px'>" + - "<div class='ng-arrow-"+arrowPos+"'/>" + - "<div class='ng-title'>"+title+"</div>" + - "<div class='ng-content'>"+msg+"</div>" + - "</div>"); - jQuery("body").append(tip); - if(arrowPos === 'left'){ - x = position.left + this.offsetWidth + 11; - }else{ - x = position.left - (width + 15); - tip.find('.ng-arrow-right').css({left:width+1}); - } - - tip.css({left: x+"px", top: (position.top - 3)+"px"}); - return true; -}; - -PopUp.onOut = function() { - jQuery('#ng-callout'). - unbind(PopUp.OUT_EVENT, PopUp.onOut). - remove(); - return true; -}; - -PopUp.prototype = { - bind: function () { - var self = this; - this.doc.find('.ng-validation-error,.ng-exception'). - live("mouseover", PopUp.onOver); - } -}; - -////////////////////////////////// -// Status -////////////////////////////////// - -function NullStatus(body) { -}; - -NullStatus.prototype = { - beginRequest:function(){}, - endRequest:function(){} -}; - -function Status(body) { - this.requestCount = 0; - this.body = body; -}; - -Status.DOM ='<div id="ng-spacer"></div><div id="ng-loading">loading....</div>'; - -Status.prototype = { - beginRequest: function () { - if (this.requestCount === 0) { - (this.loader = this.loader || this.body.append(Status.DOM).find("#ng-loading")).show(); - } - this.requestCount++; - }, - - endRequest: function () { - this.requestCount--; - if (this.requestCount === 0) { - this.loader.hide("fold"); - } - } -}; diff --git a/src/directives.js b/src/directives.js index 6b81d864..9aadbd11 100644 --- a/src/directives.js +++ b/src/directives.js @@ -26,12 +26,13 @@ angularDirective("ng:bind", function(expression){ return function(element) { var lastValue = noop, lastError = noop; this.$onEval(function() { - var error, - value = this.$tryEval(expression, function(e){ - error = toJson(e); - }), - isHtml, - isDomElement; + var error, value, isHtml, isDomElement, + oldElement = this.hasOwnProperty('$element') ? this.$element : undefined; + this.$element = element; + value = this.$tryEval(expression, function(e){ + error = toJson(e); + }); + this.$element = oldElement; if (lastValue === value && lastError == error) return; isHtml = value instanceof HTML, isDomElement = isElement(value); @@ -74,7 +75,9 @@ function compileBindTemplate(template){ }); }); bindTemplateCache[template] = fn = function(element){ - var parts = [], self = this; + var parts = [], self = this, + oldElement = this.hasOwnProperty('$element') ? self.$element : undefined; + self.$element = element; for ( var i = 0; i < bindings.length; i++) { var value = bindings[i].call(self, element); if (isElement(value)) @@ -83,6 +86,7 @@ function compileBindTemplate(template){ value = toJson(value, true); parts.push(value); }; + self.$element = oldElement; return parts.join(''); }; } @@ -157,33 +161,32 @@ angularWidget("@ng:repeat", function(expression, element){ valueIdent = match[3] || match[1]; keyIdent = match[2]; - if (isUndefined(this.$eval(rhs))) this.$set(rhs, []); - var children = [], currentScope = this; this.$onEval(function(){ var index = 0, childCount = children.length, childScope, lastElement = reference, collection = this.$tryEval(rhs, reference), is_array = isArray(collection); for ( var key in collection) { - if (is_array && !collection.hasOwnProperty(key)) break; - if (index < childCount) { - // reuse existing child - childScope = children[index]; - childScope[valueIdent] = collection[key]; - if (keyIdent) childScope[keyIdent] = key; - } else { - // grow children - childScope = template(element.clone(), createScope(currentScope)); - childScope[valueIdent] = collection[key]; - if (keyIdent) childScope[keyIdent] = key; - lastElement.after(childScope.$element); - childScope.$index = index; - childScope.$element.attr('ng:repeat-index', index); - childScope.$init(); - children.push(childScope); + if (!is_array || collection.hasOwnProperty(key)) { + if (index < childCount) { + // reuse existing child + childScope = children[index]; + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + } else { + // grow children + childScope = template(element.clone(), createScope(currentScope)); + childScope[valueIdent] = collection[key]; + if (keyIdent) childScope[keyIdent] = key; + lastElement.after(childScope.$element); + childScope.$index = index; + childScope.$element.attr('ng:repeat-index', index); + childScope.$init(); + children.push(childScope); + } + childScope.$eval(); + lastElement = childScope.$element; + index ++; } - childScope.$eval(); - lastElement = childScope.$element; - index ++; }; // shrink children while(children.length > index) { @@ -196,10 +199,10 @@ angularWidget("@ng:repeat", function(expression, element){ angularDirective("ng:click", function(expression, element){ return function(element){ var self = this; - element.bind('click', function(){ + element.bind('click', function(event){ self.$tryEval(expression, element); self.$root.$eval(); - return false; + event.preventDefault(); }); }; }); @@ -222,8 +225,8 @@ function ngClass(selector) { var existing = element[0].className + ' '; return function(element){ this.$onEval(function(){ - var value = this.$eval(expression); if (selector(this.$index)) { + var value = this.$eval(expression); if (isArray(value)) value = value.join(' '); element[0].className = trim(existing + value); } @@ -254,8 +257,17 @@ angularDirective("ng:hide", function(expression, element){ angularDirective("ng:style", function(expression, element){ return function(element){ + var resetStyle = getStyle(element); this.$onEval(function(){ - element.css(this.$eval(expression) || {}); + var style = this.$eval(expression) || {}, key, mergedStyle = {}; + for(key in style) { + if (resetStyle[key] === undefined) resetStyle[key] = ''; + mergedStyle[key] = style[key]; + } + for(key in resetStyle) { + mergedStyle[key] = mergedStyle[key] || resetStyle[key]; + } + element.css(mergedStyle); }, element); }; }); diff --git a/src/filters.js b/src/filters.js index 24464477..27e3deca 100644 --- a/src/filters.js +++ b/src/filters.js @@ -1,298 +1,74 @@ -var angularFilterGoogleChartApi; - -foreach({ - 'currency': function(amount){ - this.$element.toggleClass('ng:format-negative', amount < 0); - return '$' + angularFilter['number'].apply(this, [amount, 2]); - }, - - 'number': function(amount, fractionSize){ - if (isNaN(amount) || !isFinite(amount)) { - return ''; - } - fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize; - var isNegative = amount < 0; - amount = Math.abs(amount); - var pow = Math.pow(10, fractionSize); - var text = "" + Math.round(amount * pow); - var whole = text.substring(0, text.length - fractionSize); - whole = whole || '0'; - var frc = text.substring(text.length - fractionSize); - text = isNegative ? '-' : ''; - for (var i = 0; i < whole.length; i++) { - if ((whole.length - i)%3 === 0 && i !== 0) { - text += ','; - } - text += whole.charAt(i); - } - if (fractionSize > 0) { - for (var j = frc.length; j < fractionSize; j++) { - frc += '0'; - } - text += '.' + frc.substring(0, fractionSize); - } - return text; - }, - - 'date': function(amount) { - }, - - 'json': function(object) { - this.$element.addClass("ng-monospace"); - return toJson(object, true); - }, - - 'trackPackage': (function(){ - var MATCHERS = [ - { name: "UPS", - url: "http://wwwapps.ups.com/WebTracking/processInputRequest?sort_by=status&tracknums_displayed=1&TypeOfInquiryNumber=T&loc=en_US&track.x=0&track.y=0&InquiryNumber1=", - regexp: [ - /^1Z[0-9A-Z]{16}$/i]}, - { name: "FedEx", - url: "http://www.fedex.com/Tracking?tracknumbers=", - regexp: [ - /^96\d{10}?$/i, - /^96\d{17}?$/i, - /^96\d{20}?$/i, - /^\d{15}$/i, - /^\d{12}$/i]}, - { name: "USPS", - url: "http://trkcnfrm1.smi.usps.com/PTSInternetWeb/InterLabelInquiry.do?origTrackNum=", - regexp: [ - /^(91\d{20})$/i, - /^(91\d{18})$/i]}]; - return function(trackingNo, noMatch) { - trackingNo = trim(trackingNo); - var tNo = trackingNo.replace(/ /g, ''); - var returnValue; - foreach(MATCHERS, function(carrier){ - foreach(carrier.regexp, function(regexp){ - if (!returnValue && regexp.test(tNo)) { - var text = carrier.name + ": " + trackingNo; - var url = carrier.url + trackingNo; - returnValue = jqLite('<a></a>'); - returnValue.text(text); - returnValue.attr('href', url); - } - }); - }); - if (returnValue) - return returnValue; - else if (trackingNo) - return noMatch || trackingNo + " is not recognized"; - else - return null; - };})(), - - 'link': function(obj, title) { - if (obj) { - var text = title || obj.text || obj; - var url = obj.url || obj; - if (url) { - if (angular.validator.email(url) === null) { - url = "mailto:" + url; - } - var a = jqLite('<a></a>'); - a.attr('href', url); - a.text(text); - return a; - } - } - return obj; - }, - - - 'bytes': (function(){ - var SUFFIX = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; - return function(size) { - if(size === null) return ""; - - var suffix = 0; - while (size > 1000) { - size = size / 1024; - suffix++; - } - var txt = "" + size; - var dot = txt.indexOf('.'); - if (dot > -1 && dot + 2 < txt.length) { - txt = txt.substring(0, dot + 2); - } - return txt + " " + SUFFIX[suffix]; - }; - })(), - - 'image': function(obj, width, height) { - if (obj && obj.url) { - var style = "", img = jqLite('<img>'); - if (width) { - img.css('max-width', width + 'px'); - img.css('max-height', (height || width) + 'px'); - } - img.attr('src', obj.url); - return img; +angularFilter.currency = function(amount){ + this.$element.toggleClass('ng:format-negative', amount < 0); + return '$' + angularFilter['number'].apply(this, [amount, 2]); +}; + +angularFilter.number = function(amount, fractionSize){ + if (isNaN(amount) || !isFinite(amount)) { + return ''; + } + fractionSize = typeof fractionSize == 'undefined' ? 2 : fractionSize; + var isNegative = amount < 0; + amount = Math.abs(amount); + var pow = Math.pow(10, fractionSize); + var text = "" + Math.round(amount * pow); + var whole = text.substring(0, text.length - fractionSize); + whole = whole || '0'; + var frc = text.substring(text.length - fractionSize); + text = isNegative ? '-' : ''; + for (var i = 0; i < whole.length; i++) { + if ((whole.length - i)%3 === 0 && i !== 0) { + text += ','; } - return null; - }, - - 'lowercase': lowercase, - - 'uppercase': uppercase, - - 'linecount': function (obj) { - if (isString(obj)) { - if (obj==='') return 1; - return obj.split(/\n|\f/).length; + text += whole.charAt(i); + } + if (fractionSize > 0) { + for (var j = frc.length; j < fractionSize; j++) { + frc += '0'; } - return 1; - }, - - 'if': function (result, expression) { - return expression ? result : undefined; - }, - - 'unless': function (result, expression) { - return expression ? undefined : result; - }, - - 'googleChartApi': extend( - function(type, data, width, height) { - data = data || {}; - var chart = { - 'cht':type, - 'chco':angularFilterGoogleChartApi['collect'](data, 'color'), - 'chtt':angularFilterGoogleChartApi['title'](data), - 'chdl':angularFilterGoogleChartApi['collect'](data, 'label'), - 'chd':angularFilterGoogleChartApi['values'](data), - 'chf':'bg,s,FFFFFF00' - }; - if (_.isArray(data['xLabels'])) { - chart['chxt']='x'; - chart['chxl']='0:|' + data.xLabels.join('|'); - } - return angularFilterGoogleChartApi['encode'](chart, width, height); - }, - { - 'values': function(data){ - var seriesValues = []; - foreach(data['series']||[], function(serie){ - var values = []; - foreach(serie['values']||[], function(value){ - values.push(value); - }); - seriesValues.push(values.join(',')); - }); - var values = seriesValues.join('|'); - return values === "" ? null : "t:" + values; - }, - - 'title': function(data){ - var titles = []; - var title = data['title'] || []; - foreach(_.isArray(title)?title:[title], function(text){ - titles.push(encodeURIComponent(text)); - }); - return titles.join('|'); - }, + text += '.' + frc.substring(0, fractionSize); + } + return text; +}; - 'collect': function(data, key){ - var outterValues = []; - var count = 0; - foreach(data['series']||[], function(serie){ - var innerValues = []; - var value = serie[key] || []; - foreach(_.isArray(value)?value:[value], function(color){ - innerValues.push(encodeURIComponent(color)); - count++; - }); - outterValues.push(innerValues.join('|')); - }); - return count?outterValues.join(','):null; - }, +angularFilter.date = function(date) { + if (date instanceof Date) + return date.toLocaleDateString(); + else + return date; +}; - 'encode': function(params, width, height) { - width = width || 200; - height = height || width; - var url = "http://chart.apis.google.com/chart?", - urlParam = [], - img = jqLite('<img>'); - params['chs'] = width + "x" + height; - foreach(params, function(value, key){ - if (value) { - urlParam.push(key + "=" + value); - } - }); - urlParam.sort(); - url += urlParam.join("&"); - img.attr('src', url); - img.css({width: width + 'px', height: height + 'px'}); - return img; - } - } - ), +angularFilter.json = function(object) { + this.$element.addClass("ng-monospace"); + return toJson(object, true); +}; +angularFilter.lowercase = lowercase; - 'qrcode': function(value, width, height) { - return angularFilterGoogleChartApi['encode']({ - 'cht':'qr', 'chl':encodeURIComponent(value)}, width, height); - }, - 'chart': { - 'pie':function(data, width, height) { - return angularFilterGoogleChartApi('p', data, width, height); - }, - 'pie3d':function(data, width, height) { - return angularFilterGoogleChartApi('p3', data, width, height); - }, - 'pieConcentric':function(data, width, height) { - return angularFilterGoogleChartApi('pc', data, width, height); - }, - 'barHorizontalStacked':function(data, width, height) { - return angularFilterGoogleChartApi('bhs', data, width, height); - }, - 'barHorizontalGrouped':function(data, width, height) { - return angularFilterGoogleChartApi('bhg', data, width, height); - }, - 'barVerticalStacked':function(data, width, height) { - return angularFilterGoogleChartApi('bvs', data, width, height); - }, - 'barVerticalGrouped':function(data, width, height) { - return angularFilterGoogleChartApi('bvg', data, width, height); - }, - 'line':function(data, width, height) { - return angularFilterGoogleChartApi('lc', data, width, height); - }, - 'sparkline':function(data, width, height) { - return angularFilterGoogleChartApi('ls', data, width, height); - }, - 'scatter':function(data, width, height) { - return angularFilterGoogleChartApi('s', data, width, height); - } - }, +angularFilter.uppercase = uppercase; - 'html': function(html){ - return new HTML(html); - }, +angularFilter.html = function(html){ + return new HTML(html); +}; - 'linky': function(text){ - if (!text) return text; - function regExpEscape(text) { - return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1'); - } - var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/; - var match; - var raw = text; - var html = []; - while (match=raw.match(URL)) { - var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,''); - var i = raw.indexOf(url); - html.push(escapeHtml(raw.substr(0, i))); - html.push('<a href="' + url + '">'); - html.push(url); - html.push('</a>'); - raw = raw.substring(i + url.length); - } - html.push(escapeHtml(raw)); - return new HTML(html.join('')); +angularFilter.linky = function(text){ + if (!text) return text; + function regExpEscape(text) { + return text.replace(/([\/\.\*\+\?\|\(\)\[\]\{\}\\])/g, '\\$1'); } -}, function(v,k){angularFilter[k] = v;}); - -angularFilterGoogleChartApi = angularFilter['googleChartApi']; + var URL = /(ftp|http|https|mailto):\/\/([^\(\)|\s]+)/; + var match; + var raw = text; + var html = []; + while (match=raw.match(URL)) { + var url = match[0].replace(/[\.\;\,\(\)\{\}\<\>]$/,''); + var i = raw.indexOf(url); + html.push(escapeHtml(raw.substr(0, i))); + html.push('<a href="' + url + '">'); + html.push(url); + html.push('</a>'); + raw = raw.substring(i + url.length); + } + html.push(escapeHtml(raw)); + return new HTML(html.join('')); +}; diff --git a/src/formatters.js b/src/formatters.js index 40462cf3..9122489f 100644 --- a/src/formatters.js +++ b/src/formatters.js @@ -3,30 +3,28 @@ function toString(obj) {return (isDefined(obj) && obj !== null) ? "" + obj : obj var NUMBER = /^\s*[-+]?\d*(\.\d*)?\s*$/; -extend(angularFormatter, { - 'noop':formatter(identity, identity), - 'boolean':formatter(toString, toBoolean), - 'number':formatter(toString, - function(obj){ - if (isString(obj) && NUMBER.exec(obj)) { - return obj ? 1*obj : null; - } - throw "Not a number"; - }), +angularFormatter.noop = formatter(identity, identity); +angularFormatter.json = formatter(toJson, fromJson); +angularFormatter['boolean'] = formatter(toString, toBoolean); +angularFormatter.number = formatter(toString, function(obj){ + if (isString(obj) && NUMBER.exec(obj)) { + return obj ? 1*obj : null; + } + throw "Not a number"; +}); - 'list':formatter( - function(obj) { return obj ? obj.join(", ") : obj; }, - function(value) { - var list = []; - foreach((value || '').split(','), function(item){ - item = trim(item); - if (item) list.push(item); - }); - return list; - } - ), +angularFormatter.list = formatter( + function(obj) { return obj ? obj.join(", ") : obj; }, + function(value) { + var list = []; + foreach((value || '').split(','), function(item){ + item = trim(item); + if (item) list.push(item); + }); + return list; + } +); - 'trim':formatter( - function(obj) { return obj ? trim("" + obj) : ""; } - ) -}); +angularFormatter.trim = formatter( + function(obj) { return obj ? trim("" + obj) : ""; } +); diff --git a/src/jqLite.js b/src/jqLite.js index 68172fd8..1ad4d96d 100644 --- a/src/jqLite.js +++ b/src/jqLite.js @@ -2,24 +2,17 @@ //JQLite ////////////////////////////////// -var jqCache = {}; -var jqName = 'ng-' + new Date().getTime(); -var jqId = 1; -function jqNextId() { return (jqId++); } - -var addEventListener = window.document.attachEvent ? - function(element, type, fn) { - element.attachEvent('on' + type, fn); - } : function(element, type, fn) { - element.addEventListener(type, fn, false); - }; +var jqCache = {}, + jqName = 'ng-' + new Date().getTime(), + jqId = 1, + addEventListener = (window.document.attachEvent ? + function(element, type, fn) {element.attachEvent('on' + type, fn);} : + function(element, type, fn) {element.addEventListener(type, fn, false);}), + removeEventListener = (window.document.detachEvent ? + function(element, type, fn) {element.detachEvent('on' + type, fn); } : + function(element, type, fn) { element.removeEventListener(type, fn, false); }); -var removeEventListener = window.document.detachEvent ? - function(element, type, fn) { - element.detachEvent('on' + type, fn); - } : function(element, type, fn) { - element.removeEventListener(type, fn, false); - }; +function jqNextId() { return (jqId++); } function jqClearData(element) { var cacheId = element[jqName], @@ -36,6 +29,23 @@ function jqClearData(element) { } } +function getStyle(element) { + var current = {}, style = element[0].style, value, name, i; + if (typeof style.length == 'number') { + for(i = 0; i < style.length; i++) { + name = style[i]; + current[name] = style[name]; + } + } else { + for (name in style) { + value = style[name]; + if (1*name != name && name != 'cssText' && value && typeof value == 'string' && value !='false') + current[name] = value; + } + } + return current; +} + function JQLite(element) { if (isElement(element)) { this[0] = element; @@ -87,19 +97,14 @@ JQLite.prototype = { eventHandler = bind[type]; if (!eventHandler) { bind[type] = eventHandler = function(event) { - var bubbleEvent = false; - foreach(eventHandler.fns, function(fn){ - bubbleEvent = bubbleEvent || fn.call(self, event); - }); - if (!bubbleEvent) { - if (msie) { + if (!event.preventDefault) { + event.preventDefault = function(){ event.returnValue = false; - event.cancelBubble = true; - } else { - event.preventDefault(); - event.stopPropagation(); - } + }; } + foreach(eventHandler.fns, function(fn){ + fn.call(self, event); + }); }; eventHandler.fns = []; addEventListener(element, type, eventHandler); diff --git a/src/moveToAngularCom/ControlBar.js b/src/moveToAngularCom/ControlBar.js deleted file mode 100644 index b9a2de8a..00000000 --- a/src/moveToAngularCom/ControlBar.js +++ /dev/null @@ -1,72 +0,0 @@ -function ControlBar(document, serverUrl, database) { - this._document = document; - this.serverUrl = serverUrl; - this.database = database; - this._window = window; - this.callbacks = []; -}; - -ControlBar.HTML = - '<div>' + - '<div class="ui-widget-overlay"></div>' + - '<div id="ng-login" ng:non-bindable="true">' + - '<div class="ng-login-container"></div>' + - '</div>' + - '</div>'; - - -ControlBar.FORBIDEN = - '<div ng:non-bindable="true" title="Permission Error:">' + - 'Sorry, you do not have permission for this!'+ - '</div>'; - -ControlBar.prototype = { - bind: function () { - }, - - login: function (loginSubmitFn) { - this.callbacks.push(loginSubmitFn); - if (this.callbacks.length == 1) { - this.doTemplate("/user_session/new.mini?database="+encodeURIComponent(this.database)+"&return_url=" + encodeURIComponent(this.urlWithoutAnchor())); - } - }, - - logout: function (loginSubmitFn) { - this.callbacks.push(loginSubmitFn); - if (this.callbacks.length == 1) { - this.doTemplate("/user_session/do_destroy.mini"); - } - }, - - urlWithoutAnchor: function (path) { - return this._window['location']['href'].split("#")[0]; - }, - - doTemplate: function (path) { - var self = this; - var id = new Date().getTime(); - var url = this.urlWithoutAnchor() + "#$iframe_notify=" + id; - var iframeHeight = 330; - var loginView = jQuery('<div style="overflow:hidden; padding:2px 0 0 0;"><iframe name="'+ url +'" src="'+this.serverUrl + path + '" width="500" height="'+ iframeHeight +'"/></div>'); - this._document.append(loginView); - loginView['dialog']({ - 'height':iframeHeight + 33, 'width':500, - 'resizable': false, 'modal':true, - 'title': 'Authentication: <a href="http://www.getangular.com"><tt><angular/></tt></a>' - }); - angularCallbacks["_iframe_notify_" + id] = function() { - loginView['dialog']("destroy"); - loginView['remove'](); - foreach(self.callbacks, function(callback){ - callback(); - }); - self.callbacks = []; - }; - }, - - notAuthorized: function () { - if (this.forbidenView) return; - this.forbidenView = jQuery(ControlBar.FORBIDEN); - this.forbidenView.dialog({bgiframe:true, height:70, modal:true}); - } -};
\ No newline at end of file diff --git a/src/moveToAngularCom/DataStore.js b/src/moveToAngularCom/DataStore.js deleted file mode 100644 index 70bcc623..00000000 --- a/src/moveToAngularCom/DataStore.js +++ /dev/null @@ -1,330 +0,0 @@ -function DataStore(post, users, anchor) { - this.post = post; - this.users = users; - this._cache_collections = []; - this._cache = {'$collections':this._cache_collections}; - this.anchor = anchor; - this.bulkRequest = []; -}; - -DataStore.NullEntity = extend(function(){}, { - 'all': function(){return [];}, - 'query': function(){return [];}, - 'load': function(){return {};}, - 'title': undefined -}); - -DataStore.prototype = { - cache: function(document) { - if (! document.datastore === this) { - throw "Parameter must be an instance of Entity! " + toJson(document); - } - var key = document['$entity'] + '/' + document['$id']; - var cachedDocument = this._cache[key]; - if (cachedDocument) { - Model.copyDirectFields(document, cachedDocument); - } else { - this._cache[key] = document; - cachedDocument = document; - } - return cachedDocument; - }, - - load: function(instance, id, callback, failure) { - if (id && id !== '*') { - var self = this; - this._jsonRequest(["GET", instance['$entity'] + "/" + id], function(response) { - instance['$loadFrom'](response); - instance['$migrate'](); - var clone = instance['$$entity'](instance); - self.cache(clone); - (callback||noop)(instance); - }, failure); - } - return instance; - }, - - loadMany: function(entity, ids, callback) { - var self=this; - var list = []; - var callbackCount = 0; - foreach(ids, function(id){ - list.push(self.load(entity(), id, function(){ - callbackCount++; - if (callbackCount == ids.length) { - (callback||noop)(list); - } - })); - }); - return list; - }, - - loadOrCreate: function(instance, id, callback) { - var self=this; - return this.load(instance, id, callback, function(response){ - if (response['$status_code'] == 404) { - instance['$id'] = id; - (callback||noop)(instance); - } else { - throw response; - } - }); - }, - - loadAll: function(entity, callback) { - var self = this; - var list = []; - list['$$accept'] = function(doc){ - return doc['$entity'] == entity['title']; - }; - this._cache_collections.push(list); - this._jsonRequest(["GET", entity['title']], function(response) { - var rows = response; - for ( var i = 0; i < rows.length; i++) { - var document = entity(); - document['$loadFrom'](rows[i]); - list.push(self.cache(document)); - } - (callback||noop)(list); - }); - return list; - }, - - save: function(document, callback) { - var self = this; - var data = {}; - document['$saveTo'](data); - this._jsonRequest(["POST", "", data], function(response) { - document['$loadFrom'](response); - var cachedDoc = self.cache(document); - _.each(self._cache_collections, function(collection){ - if (collection['$$accept'](document)) { - angularArray['includeIf'](collection, cachedDoc, true); - } - }); - if (document['$$anchor']) { - self.anchor[document['$$anchor']] = document['$id']; - } - if (callback) - callback(document); - }); - }, - - remove: function(document, callback) { - var self = this; - var data = {}; - document['$saveTo'](data); - this._jsonRequest(["DELETE", "", data], function(response) { - delete self._cache[document['$entity'] + '/' + document['$id']]; - _.each(self._cache_collections, function(collection){ - for ( var i = 0; i < collection.length; i++) { - var item = collection[i]; - if (item['$id'] == document['$id']) { - collection.splice(i, 1); - } - } - }); - (callback||noop)(response); - }); - }, - - _jsonRequest: function(request, callback, failure) { - request['$$callback'] = callback; - request['$$failure'] = failure||function(response){ - throw response; - }; - this.bulkRequest.push(request); - }, - - flush: function() { - if (this.bulkRequest.length === 0) return; - var self = this; - var bulkRequest = this.bulkRequest; - this.bulkRequest = []; - log('REQUEST:', bulkRequest); - function callback(code, bulkResponse){ - log('RESPONSE[' + code + ']: ', bulkResponse); - if(bulkResponse['$status_code'] == 401) { - self.users['login'](function(){ - self.post(bulkRequest, callback); - }); - } else if(bulkResponse['$status_code']) { - alert(toJson(bulkResponse)); - } else { - for ( var i = 0; i < bulkResponse.length; i++) { - var response = bulkResponse[i]; - var request = bulkRequest[i]; - var responseCode = response['$status_code']; - if(responseCode) { - if(responseCode == 403) { - self.users['notAuthorized'](); - } else { - request['$$failure'](response); - } - } else { - request['$$callback'](response); - } - } - } - } - this.post(bulkRequest, callback); - }, - - saveScope: function(scope, callback) { - var saveCounter = 1; - function onSaveDone() { - saveCounter--; - if (saveCounter === 0 && callback) - callback(); - } - for(var key in scope) { - var item = scope[key]; - if (item && item['$save'] == Model.prototype['$save']) { - saveCounter++; - item['$save'](onSaveDone); - } - } - onSaveDone(); - }, - - query: function(type, query, arg, callback){ - var self = this; - var queryList = []; - queryList['$$accept'] = function(doc){ - return false; - }; - this._cache_collections.push(queryList); - var request = type['title'] + '/' + query + '=' + arg; - this._jsonRequest(["GET", request], function(response){ - var list = response; - foreach(list, function(item){ - var document = type()['$loadFrom'](item); - queryList.push(self.cache(document)); - }); - (callback||noop)(queryList); - }); - return queryList; - }, - - entities: function(callback) { - var entities = []; - var self = this; - this._jsonRequest(["GET", "$entities"], function(response) { - foreach(response, function(value, entityName){ - entities.push(self.entity(entityName)); - }); - entities.sort(function(a,b){return a.title > b.title ? 1 : -1;}); - (callback||noop)(entities); - }); - return entities; - }, - - documentCountsByUser: function(){ - var counts = {}; - var self = this; - self.post([["GET", "$users"]], function(code, response){ - extend(counts, response[0]); - }); - return counts; - }, - - userDocumentIdsByEntity: function(user){ - var ids = {}; - var self = this; - self.post([["GET", "$users/" + user]], function(code, response){ - extend(ids, response[0]); - }); - return ids; - }, - - entity: function(name, defaults){ - if (!name) { - return DataStore.NullEntity; - } - var self = this; - var entity = extend(function(initialState){ - return new Model(entity, initialState); - }, { - // entity.name does not work as name seems to be reserved for functions - 'title': name, - '$$factory': true, - datastore: this, //private, obfuscate - 'defaults': defaults || {}, - 'load': function(id, callback){ - return self.load(entity(), id, callback); - }, - 'loadMany': function(ids, callback){ - return self.loadMany(entity, ids, callback); - }, - 'loadOrCreate': function(id, callback){ - return self.loadOrCreate(entity(), id, callback); - }, - 'all': function(callback){ - return self.loadAll(entity, callback); - }, - 'query': function(query, queryArgs, callback){ - return self.query(entity, query, queryArgs, callback); - }, - 'properties': function(callback) { - self._jsonRequest(["GET", name + "/$properties"], callback); - } - }); - return entity; - }, - - join: function(join){ - function fn(){ - throw "Joined entities can not be instantiated into a document."; - }; - function base(name){return name ? name.substring(0, name.indexOf('.')) : undefined;} - function next(name){return name.substring(name.indexOf('.') + 1);} - var joinOrder = _(join).chain(). - map(function($, name){ - return name;}). - sortBy(function(name){ - var path = []; - do { - if (_(path).include(name)) throw "Infinite loop in join: " + path.join(" -> "); - path.push(name); - if (!join[name]) throw _("Named entity '<%=name%>' is undefined.").template({name:name}); - name = base(join[name].on); - } while(name); - return path.length; - }). - value(); - if (_(joinOrder).select(function($){return join[$].on;}).length != joinOrder.length - 1) - throw "Exactly one entity needs to be primary."; - fn['query'] = function(exp, value) { - var joinedResult = []; - var baseName = base(exp); - if (baseName != joinOrder[0]) throw _("Named entity '<%=name%>' is not a primary entity.").template({name:baseName}); - var Entity = join[baseName].join; - var joinIndex = 1; - Entity['query'](next(exp), value, function(result){ - var nextJoinName = joinOrder[joinIndex++]; - var nextJoin = join[nextJoinName]; - var nextJoinOn = nextJoin.on; - var joinIds = {}; - _(result).each(function(doc){ - var row = {}; - joinedResult.push(row); - row[baseName] = doc; - var id = Scope.getter(row, nextJoinOn); - joinIds[id] = id; - }); - nextJoin.join.loadMany(_.toArray(joinIds), function(result){ - var byId = {}; - _(result).each(function(doc){ - byId[doc.$id] = doc; - }); - _(joinedResult).each(function(row){ - var id = Scope.getter(row, nextJoinOn); - row[nextJoinName] = byId[id]; - }); - }); - }); - return joinedResult; - }; - return fn; - } -}; diff --git a/src/moveToAngularCom/Server.js b/src/moveToAngularCom/Server.js deleted file mode 100644 index 5c4ec3c6..00000000 --- a/src/moveToAngularCom/Server.js +++ /dev/null @@ -1,68 +0,0 @@ -function Server(url, getScript) { - this.url = url; - this.nextId = 0; - this.getScript = getScript; - this.uuid = "_" + ("" + Math.random()).substr(2) + "_"; - this.maxSize = 1800; -}; - -Server.prototype = { - base64url: function(txt) { - return Base64.encode(txt); - }, - - request: function(method, url, request, callback) { - var requestId = this.uuid + (this.nextId++); - var payload = this.base64url(toJson({'u':url, 'm':method, 'p':request})); - var totalPockets = Math.ceil(payload.length / this.maxSize); - var baseUrl = this.url + "/$/" + requestId + "/" + totalPockets + "/"; - angularCallbacks[requestId] = function(response) { - delete angularCallbacks[requestId]; - callback(200, response); - }; - for ( var pocketNo = 0; pocketNo < totalPockets; pocketNo++) { - var pocket = payload.substr(pocketNo * this.maxSize, this.maxSize); - this.getScript(baseUrl + (pocketNo+1) + "?h=" + pocket, noop); - } - } -}; - -function FrameServer(frame) { - this.frame = frame; -}; -FrameServer.PREFIX = "$DATASET:"; - -FrameServer.prototype = { - read:function(){ - this.data = fromJson(this.frame.name.substr(FrameServer.PREFIX.length)); - }, - write:function(){ - this.frame.name = FrameServer.PREFIX + toJson(this.data); - }, - request: function(method, url, request, callback) { - //alert(method + " " + url + " " + toJson(request) + " " + toJson(callback)); - } -}; - - -function VisualServer(delegate, status, update) { - this.delegate = delegate; - this.update = update; - this.status = status; -}; - -VisualServer.prototype = { - request:function(method, url, request, callback) { - var self = this; - this.status.beginRequest(request); - this.delegate.request(method, url, request, function() { - self.status.endRequest(); - try { - callback.apply(this, arguments); - } catch (e) { - alert(toJson(e)); - } - self.update(); - }); - } -}; diff --git a/src/moveToAngularCom/Users.js b/src/moveToAngularCom/Users.js deleted file mode 100644 index fb5845d3..00000000 --- a/src/moveToAngularCom/Users.js +++ /dev/null @@ -1,35 +0,0 @@ -function Users(server, controlBar) { - this.server = server; - this.controlBar = controlBar; -}; - -extend(Users.prototype, { - 'fetchCurrentUser':function(callback) { - var self = this; - this.server.request("GET", "/account.json", {}, function(code, response){ - self['current'] = response['user']; - callback(response['user']); - }); - }, - - 'logout': function(callback) { - var self = this; - this.controlBar.logout(function(){ - delete self['current']; - (callback||noop)(); - }); - }, - - 'login': function(callback) { - var self = this; - this.controlBar.login(function(){ - self['fetchCurrentUser'](function(){ - (callback||noop)(); - }); - }); - }, - - 'notAuthorized': function(){ - this.controlBar.notAuthorized(); - } -}); diff --git a/src/moveToAngularCom/directivesAngularCom.js b/src/moveToAngularCom/directivesAngularCom.js deleted file mode 100644 index 84032bdd..00000000 --- a/src/moveToAngularCom/directivesAngularCom.js +++ /dev/null @@ -1,29 +0,0 @@ - -angular.directive("auth", function(expression, element){ - return function(){ - if(expression == "eager") { - this.$users.fetchCurrent(); - } - }; -}); - - -//expression = "book=Book:{year=2000}" -angular.directive("entity", function(expression, element){ - //parse expression, ignore element - var entityName; // "Book"; - var instanceName; // "book"; - var defaults; // {year: 2000}; - - parse(expression); - - return function(){ - this[entityName] = this.$datastore.entity(entityName, defaults); - this[instanceName] = this[entityName](); - this.$watch("$anchor."+instanceName, function(newAnchor){ - this[instanceName] = this[entityName].get(this.$anchor[instanceName]); - }); - }; -}); - - diff --git a/src/scenario/DSL.js b/src/scenario/DSL.js index b713cfd6..0607238c 100644 --- a/src/scenario/DSL.js +++ b/src/scenario/DSL.js @@ -1,38 +1,53 @@ angular.scenario.dsl.browser = { navigateTo: function(url){ - $scenario.addStep('Navigate to: ' + url, function(done){ + var location = this.location; + return $scenario.addFuture('Navigate to: ' + url, function(done){ var self = this; this.testFrame.load(function(){ self.testFrame.unbind(); self.testWindow = self.testFrame[0].contentWindow; - self.testDocument = jQuery(self.testWindow.document); + self.testDocument = self.jQuery(self.testWindow.document); self.$browser = self.testWindow.angular.service.$browser(); - self.notifyWhenNoOutstandingRequests = bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests); + self.notifyWhenNoOutstandingRequests = + bind(self.$browser, self.$browser.notifyWhenNoOutstandingRequests); self.notifyWhenNoOutstandingRequests(done); }); if (this.testFrame.attr('src') == url) { this.testFrame[0].contentWindow.location.reload(); } else { this.testFrame.attr('src', url); + location.setLocation(url); } }); + }, + location: { + href: "", + hash: "", + toEqual: function(url) { + return (this.hash == "" ? (url == this.href) : + (url == (this.href + "/#/" + this.hash))); + }, + setLocation: function(url) { + var urlParts = url.split("/#/"); + this.href = urlParts[0] || ""; + this.hash = urlParts[1] || ""; + } } }; angular.scenario.dsl.input = function(selector) { + var namePrefix = "input '" + selector + "'"; return { - enter: function(value){ - $scenario.addStep("Set input text of '" + selector + "' to '" + - value + "'", function(done){ - var input = this.testDocument.find('input[name=' + selector + ']'); - input.val(value); - this.testWindow.angular.element(input[0]).trigger('change'); - done(); + enter: function(value) { + return $scenario.addFuture(namePrefix + " enter '" + value + "'", function(done) { + var input = this.testDocument.find('input[name=' + selector + ']'); + input.val(value); + this.testWindow.angular.element(input[0]).trigger('change'); + done(); }); }, - select: function(value){ - $scenario.addStep("Select radio '" + selector + "' to '" + - value + "'", function(done){ + select: function(value) { + return $scenario.addFuture(namePrefix + " select '" + value + "'", function(done) { var input = this.testDocument. find(':radio[name$=@' + selector + '][value=' + value + ']'); jqLiteWrap(input[0]).trigger('click'); @@ -41,22 +56,76 @@ angular.scenario.dsl.input = function(selector) { }); } }; -}; +}, -angular.scenario.dsl.expect = { - repeater: function(selector) { - return { - count: { - toEqual: function(number) { - $scenario.addStep("Expect that there are " + number + " items in Repeater with selector '" + selector + "'", function(done) { - var items = this.testDocument.find(selector); - if (items.length != number) { - this.result.fail("Expected " + number + " but was " + items.length); - } - done(); +angular.scenario.dsl.NG_BIND_PATTERN =/\{\{[^\}]+\}\}/; + +angular.scenario.dsl.repeater = function(selector) { + var namePrefix = "repeater '" + selector + "'"; + return { + count: function() { + return $scenario.addFuture(namePrefix + ' count', function(done) { + done(this.testDocument.find(selector).size()); + }); + }, + collect: function(collectSelector) { + return $scenario.addFuture( + namePrefix + " collect '" + collectSelector + "'", + function(done) { + var self = this; + var doCollect = bind(this, function() { + var repeaterArray = [], ngBindPattern; + var startIndex = collectSelector.search( + angular.scenario.dsl.NG_BIND_PATTERN); + if (startIndex >= 0) { + ngBindPattern = collectSelector.substring( + startIndex + 2, collectSelector.length - 2); + collectSelector = '*'; + + } + this.testDocument.find(selector).each(function() { + var element = self.jQuery(this); + element.find(collectSelector). + each(function() { + var foundElem = self.jQuery(this); + if (foundElem.attr('ng:bind') == ngBindPattern) { + repeaterArray.push(foundElem.text()); + } + }); }); - } - } - }; + return repeaterArray; + }); + done(doCollect()); + }); + } + }; +}; + +angular.scenario.dsl.element = function(selector) { + var namePrefix = "Element '" + selector + "'"; + var futureJquery = {}; + for (key in (jQuery || _jQuery).fn) { + (function(){ + var jqFnName = key; + var jqFn = (jQuery || _jQuery).fn[key]; + futureJquery[key] = function() { + var jqArgs = arguments; + return $scenario.addFuture(namePrefix + "." + jqFnName + "()", + function(done) { + var self = this, repeaterArray = [], ngBindPattern; + var startIndex = selector.search(angular.scenario.dsl.NG_BIND_PATTERN); + if (startIndex >= 0) { + ngBindPattern = selector.substring(startIndex + 2, selector.length - 2); + var element = this.testDocument.find('*').filter(function() { + return self.jQuery(this).attr('ng:bind') == ngBindPattern; + }); + done(jqFn.apply(element, jqArgs)); + } else { + done(jqFn.apply(this.testDocument.find(selector), jqArgs)); + } + }); + }; + })(); } + return futureJquery; }; diff --git a/src/scenario/Future.js b/src/scenario/Future.js new file mode 100644 index 00000000..d70e8e6e --- /dev/null +++ b/src/scenario/Future.js @@ -0,0 +1,13 @@ +function Future(name, behavior) { + this.name = name; + this.behavior = behavior; + this.fulfilled = false; + this.value = undefined; +} + +Future.prototype = { + fulfill: function(value) { + this.fulfilled = true; + this.value = value; + } +}; diff --git a/src/scenario/Matcher.js b/src/scenario/Matcher.js new file mode 100644 index 00000000..a9c86571 --- /dev/null +++ b/src/scenario/Matcher.js @@ -0,0 +1,21 @@ +function Matcher(scope, future, logger) { + var self = scope.$scenario = this; + this.logger = logger; + this.future = future; +} + +Matcher.addMatcher = function(name, matcher) { + Matcher.prototype[name] = function(expected) { + var future = this.future; + $scenario.addFuture( + 'expect ' + future.name + ' ' + name + ' ' + expected, + function(done){ + if (!matcher(future.value, expected)) + throw "Expected " + expected + ' but was ' + future.value; + done(); + } + ); + }; +}; + +Matcher.addMatcher('toEqual', angular.equals); diff --git a/src/scenario/Runner.js b/src/scenario/Runner.js index 13ba5af0..ac32559c 100644 --- a/src/scenario/Runner.js +++ b/src/scenario/Runner.js @@ -8,6 +8,7 @@ angular.scenario.Runner = function(scope, jQuery){ this.scope.$testrun = {done: false, results: []}; var specs = this.specs = {}; + this.currentSpec = {name: '', futures: []}; var path = []; this.scope.describe = function(name, body){ path.push(name); @@ -22,17 +23,20 @@ angular.scenario.Runner = function(scope, jQuery){ this.scope.afterEach = function(body) { afterEach = body; }; + this.scope.expect = function(future) { + return new Matcher(self, future, self.logger); + }; this.scope.it = function(name, body) { var specName = path.join(' ') + ': it ' + name; self.currentSpec = specs[specName] = { name: specName, - steps:[] + futures: [] }; try { beforeEach(); body(); } catch(err) { - self.addStep(err.message || 'ERROR', function(){ + self.addFuture(err.message || 'ERROR', function(){ throw err; }); } finally { @@ -107,14 +111,16 @@ angular.scenario.Runner.prototype = { callback(); }, - addStep: function(name, step) { - this.currentSpec.steps.push({name:name, fn:step}); + addFuture: function(name, behavior) { + var future = new Future(name, behavior); + this.currentSpec.futures.push(future); + return future; }, execute: function(name, callback) { var spec = this.specs[name], self = this, - stepsDone = [], + futuresFulfilled = [], result = { passed: false, failed: false, @@ -128,33 +134,37 @@ angular.scenario.Runner.prototype = { }, specThis = createScope({ result: result, + jQuery: this.jQuery, testFrame: this.testFrame, testWindow: this.testWindow }, angularService, {}); this.self = specThis; - var stepLogger = this.logger('spec', name); - spec.nextStepIndex = 0; + var futureLogger = this.logger('spec', name); + spec.nextFutureIndex = 0; function done() { result.finished = true; - stepLogger.close(); + futureLogger.close(); self.self = null; (callback||noop).call(specThis); } - function next(){ - var step = spec.steps[spec.nextStepIndex]; + function next(value){ + if (spec.nextFutureIndex > 0) { + spec.futures[spec.nextFutureIndex - 1].fulfill(value); + } + var future = spec.futures[spec.nextFutureIndex]; (result.log || {close:noop}).close(); result.log = null; - if (step) { - spec.nextStepIndex ++; - result.log = stepLogger('step', step.name); - stepsDone.push(step.name); + if (future) { + spec.nextFutureIndex ++; + result.log = futureLogger('future', future.name); + futuresFulfilled.push(future.name); try { - step.fn.call(specThis, next); + future.behavior.call(specThis, next); } catch (e) { console.error(e); result.fail(e); self.scope.$testrun.results.push( - {name: name, passed: false, error: e, steps: stepsDone}); + {name: name, passed: false, error: e, steps: futuresFulfilled}); done(); } } else { @@ -163,7 +173,7 @@ angular.scenario.Runner.prototype = { name: name, passed: !result.failed, error: result.error, - steps: stepsDone}); + steps: futuresFulfilled}); done(); } }; diff --git a/src/services.js b/src/services.js index ed6f73ad..a5158149 100644 --- a/src/services.js +++ b/src/services.js @@ -1,82 +1,115 @@ +var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/, + HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/, + DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; + angularService("$window", bind(window, identity, window)); angularService("$document", function(window){ return jqLite(window.document); }, {inject:['$window']}); -var URL_MATCH = /^(file|ftp|http|https):\/\/(\w+:{0,1}\w*@)?([\w\.-]*)(:([0-9]+))?([^\?#]+)(\?([^#]*))?(#(.*))?$/; -var HASH_MATCH = /^([^\?]*)?(\?([^\?]*))?$/; -var DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp':21}; angularService("$location", function(browser){ - var scope = this, location = {parse:parseUrl, toString:toString}; - var lastHash, lastUrl; + var scope = this, + location = {parse:parseUrl, toString:toString, update:update}, + lastLocation = {}; + + browser.watchUrl(function(url){ + update(url); + scope.$root.$eval(); + }); + this.$onEval(PRIORITY_FIRST, update); + this.$onEval(PRIORITY_LAST, update); + update(browser.getUrl()); + return location; + + function update(href){ + if (href) { + parseUrl(href); + } else { + href = check('href') || checkProtocol(); + var hash = check('hash'); + if (isUndefined(hash)) hash = checkHashPathSearch(); + if (isDefined(hash)) { + href = (href || location.href).split('#')[0]; + href+= '#' + hash; + } + if (isDefined(href)) { + parseUrl(href); + browser.setUrl(href); + } + } + } + + function check(param) { + return lastLocation[param] == location[param] ? undefined : location[param]; + } + + function checkProtocol(){ + if (lastLocation.protocol === location.protocol && + lastLocation.host === location.host && + lastLocation.port === location.port && + lastLocation.path === location.path && + equals(lastLocation.search, location.search)) + return undefined; + var url = toKeyValue(location.search); + var port = (location.port == DEFAULT_PORTS[location.protocol] ? null : location.port); + return location.protocol + '://' + location.host + + (port ? ':' + port : '') + location.path + + (url ? '?' + url : ''); + } + + function checkHashPathSearch(){ + if (lastLocation.hashPath === location.hashPath && + equals(lastLocation.hashSearch, location.hashSearch) ) + return undefined; + var url = toKeyValue(location.hashSearch); + return escape(location.hashPath) + (url ? '?' + url : ''); + } + function parseUrl(url){ if (isDefined(url)) { var match = URL_MATCH.exec(url); if (match) { - location.href = url; + location.href = url.replace('#$', ''); location.protocol = match[1]; location.host = match[3] || ''; - location.port = match[5] || DEFAULT_PORTS[location.href] || null; + location.port = match[5] || DEFAULT_PORTS[location.protocol] || null; location.path = match[6]; location.search = parseKeyValue(match[8]); - location.hash = match[9] || ''; - if (location.hash) - location.hash = location.hash.substr(1); - parseHash(location.hash); + location.hash = match[10] || ''; + match = HASH_MATCH.exec(location.hash); + location.hashPath = unescape(match[1] || ''); + location.hashSearch = parseKeyValue(match[3]); + + copy(location, lastLocation); } } } - function parseHash(hash) { - var match = HASH_MATCH.exec(hash); - location.hashPath = match[1] || ''; - location.hashSearch = parseKeyValue(match[3]); - lastHash = hash; - } + function toString() { - if (lastHash === location.hash) { - var hashKeyValue = toKeyValue(location.hashSearch), - hash = (location.hashPath ? location.hashPath : '') + (hashKeyValue ? '?' + hashKeyValue : ''), - url = location.href.split('#')[0] + '#' + (hash ? hash : ''); - if (url !== location.href) parseUrl(url); - return url; - } else { - parseUrl(location.href.split('#')[0] + '#' + location.hash); - return toString(); - } + update(); + return location.href; } - browser.watchUrl(function(url){ - parseUrl(url); - scope.$root.$eval(); - }); - parseUrl(browser.getUrl()); - this.$onEval(PRIORITY_FIRST, function(){ - if (location.hash != lastHash) { - parseHash(location.hash); - } - }); - this.$onEval(PRIORITY_LAST, function(){ - var url = toString(); - if (lastUrl != url) { - browser.setUrl(url); - lastUrl = url; - } - }); - return location; }, {inject: ['$browser']}); angularService("$log", function($window){ - var console = $window.console, - log = console && console.log || noop; + var console = $window.console || {log: noop, warn: noop, info: noop, error: noop}, + log = console.log || noop; return { - log: log, - warn: console && console.warn || log, - info: console && console.info || log, - error: console && console.error || log + log: bind(console, log), + warn: bind(console, console.warn || log), + info: bind(console, console.info || log), + error: bind(console, console.error || log) }; }, {inject:['$window']}); -angularService("$hover", function(browser) { - var tooltip, self = this, error, width = 300, arrowWidth = 10; +angularService('$exceptionHandler', function($log){ + return function(e) { + $log.error(e); + }; +}, {inject:['$log']}); + +angularService("$hover", function(browser, document) { + var tooltip, self = this, error, width = 300, arrowWidth = 10, body = jqLite(document[0].body);; browser.hover(function(element, show){ if (show && (error = element.attr(NG_EXCEPTION) || element.attr(NG_VALIDATION_ERROR))) { if (!tooltip) { @@ -89,9 +122,9 @@ angularService("$hover", function(browser) { tooltip.callout.append(tooltip.arrow); tooltip.callout.append(tooltip.title); tooltip.callout.append(tooltip.content); - self.$browser.body.append(tooltip.callout); + body.append(tooltip.callout); } - var docRect = self.$browser.body[0].getBoundingClientRect(), + var docRect = body[0].getBoundingClientRect(), elementRect = element[0].getBoundingClientRect(), leftSpace = docRect.right - elementRect.right - arrowWidth; tooltip.title.text(element.hasClass("ng-exception") ? "EXCEPTION:" : "Validation error..."); @@ -119,7 +152,7 @@ angularService("$hover", function(browser) { tooltip = null; } }); -}, {inject:['$browser']}); +}, {inject:['$browser', '$document']}); angularService("$invalidWidgets", function(){ var invalidWidgets = []; @@ -181,7 +214,7 @@ function switchRouteMatcher(on, when, dstName) { return match ? dst : null; } -angularService('$route', function(location, params){ +angularService('$route', function(location){ var routes = {}, onChange = [], matcher = switchRouteMatcher, diff --git a/src/widgets.js b/src/widgets.js index 0b77dbf4..87a302fa 100644 --- a/src/widgets.js +++ b/src/widgets.js @@ -83,8 +83,7 @@ function valueAccessor(scope, element) { elementError(element, NG_VALIDATION_ERROR, null); invalidWidgets.markValid(element); } else { - var error, - validateScope = extend(new (extend(function(){}, {prototype:scope}))(), {$element:element}); + var error, validateScope = inherit(scope, {$element:element}); error = required && !value ? 'Required' : (value ? validator(validateScope, value) : null); @@ -199,14 +198,15 @@ function inputWidget(events, modelAccessor, viewAccessor, initFn) { this.$eval(element.attr('ng:init')||''); // Don't register a handler if we are a button (noopAccessor) and there is no action if (action || modelAccessor !== noopAccessor) { - element.bind(events, function(){ + element.bind(events, function(event){ model.set(view.get()); lastValue = model.get(); scope.$tryEval(action, element); scope.$root.$eval(); // if we have noop initFn than we are just a button, // therefore we want to prevent default action - return initFn != noop; + if(initFn == noop) + event.preventDefault(); }); } view.set(lastValue = model.get()); @@ -223,16 +223,16 @@ function inputWidgetSelector(element){ return INPUT_TYPE[lowercase(element[0].type)] || noop; } -angularWidget('INPUT', inputWidgetSelector); -angularWidget('TEXTAREA', inputWidgetSelector); -angularWidget('BUTTON', inputWidgetSelector); -angularWidget('SELECT', function(element){ +angularWidget('input', inputWidgetSelector); +angularWidget('textarea', inputWidgetSelector); +angularWidget('button', inputWidgetSelector); +angularWidget('select', function(element){ this.descend(true); return inputWidgetSelector.call(this, element); }); -angularWidget('NG:INCLUDE', function(element){ +angularWidget('ng:include', function(element){ var compiler = this, srcExp = element.attr("src"), scopeExp = element.attr("scope") || ''; @@ -266,7 +266,7 @@ angularWidget('NG:INCLUDE', function(element){ } }); -var ngSwitch = angularWidget('NG:SWITCH', function (element){ +var ngSwitch = angularWidget('ng:switch', function (element){ var compiler = this, watchExpr = element.attr("on"), usingExpr = (element.attr("using") || 'equals'), @@ -4,4 +4,4 @@ if [[ $tests = "" ]]; then fi java -jar lib/jstestdriver/JsTestDriver.jar --tests "$tests" -# java -jar lib/jstestdriver/JsTestDriver.jar --tests "$tests" --config jsTestDriver-jquery.conf +#java -jar lib/jstestdriver/JsTestDriver.jar --tests "$tests" --config jsTestDriver-jquery.conf diff --git a/test/AngularSpec.js b/test/AngularSpec.js index de724f03..b4e90175 100644 --- a/test/AngularSpec.js +++ b/test/AngularSpec.js @@ -1,3 +1,7 @@ +beforeEach(function(){ + compileCache = {}; +}); + describe('Angular', function(){ xit('should fire on updateEvents', function(){ var onUpdateView = jasmine.createSpy(); @@ -50,3 +54,30 @@ describe("copy", function(){ }); }); + +describe('equals', function(){ + it('should return true if same object', function(){ + var o = {}; + expect(equals(o, o)).toEqual(true); + expect(equals(1, '1')).toEqual(true); + expect(equals(1, '2')).toEqual(false); + }); + + it('should recurse into object', function(){ + expect(equals({}, {})).toEqual(true); + expect(equals({name:'misko'}, {name:'misko'})).toEqual(true); + expect(equals({name:'misko', age:1}, {name:'misko'})).toEqual(false); + expect(equals({name:'misko'}, {name:'misko', age:1})).toEqual(false); + expect(equals({name:'misko'}, {name:'adam'})).toEqual(false); + expect(equals(['misko'], ['misko'])).toEqual(true); + expect(equals(['misko'], ['adam'])).toEqual(false); + expect(equals(['misko'], ['misko', 'adam'])).toEqual(false); + }); + + it('should ignore $ member variables', function(){ + expect(equals({name:'misko', $id:1}, {name:'misko', $id:2})).toEqual(true); + expect(equals({name:'misko'}, {name:'misko', $id:2})).toEqual(true); + expect(equals({name:'misko', $id:1}, {name:'misko'})).toEqual(true); + }); + +}); diff --git a/test/ApiTest.js b/test/ApiTest.js index 4035cdbb..9f09773d 100644 --- a/test/ApiTest.js +++ b/test/ApiTest.js @@ -18,27 +18,6 @@ ApiTest.prototype.testItShouldReturnSize = function(){ assertEquals(1, angular.Array.size([0])); }; -ApiTest.prototype.testIncludeIf = function() { - var array = []; - var obj = {}; - - angular.Array.includeIf(array, obj, true); - angular.Array.includeIf(array, obj, true); - assertTrue(includes(array, obj)); - assertEquals(1, array.length); - - angular.Array.includeIf(array, obj, false); - assertFalse(includes(array, obj)); - assertEquals(0, array.length); - - angular.Array.includeIf(array, obj, 'x'); - assertTrue(includes(array, obj)); - assertEquals(1, array.length); - angular.Array.includeIf(array, obj, ''); - assertFalse(includes(array, obj)); - assertEquals(0, array.length); -}; - ApiTest.prototype.testSum = function(){ assertEquals(3, angular.Array.sum([{a:"1"}, {a:"2"}], 'a')); }; @@ -48,13 +27,6 @@ ApiTest.prototype.testSumContainingNaN = function(){ assertEquals(1, angular.Array.sum([{a:1}, {a:Number.NaN}], function($){return $.a;})); }; -ApiTest.prototype.testInclude = function(){ - assertTrue(angular.Array.include(['a'], 'a')); - assertTrue(angular.Array.include(['a', 'b'], 'a')); - assertTrue(!angular.Array.include(['c'], 'a')); - assertTrue(!angular.Array.include(['c', 'b'], 'a')); -}; - ApiTest.prototype.testIndex = function(){ assertEquals(angular.Array.indexOf(['a'], 'a'), 0); assertEquals(angular.Array.indexOf(['a', 'b'], 'a'), 0); @@ -80,14 +52,6 @@ ApiTest.prototype.testRemove = function(){ assertEquals(items.length, 0); }; -ApiTest.prototype.testFindById = function() { - var items = [{$id:1}, {$id:2}, {$id:3}]; - assertNull(angular.Array.findById(items, 0)); - assertEquals(items[0], angular.Array.findById(items, 1)); - assertEquals(items[1], angular.Array.findById(items, 2)); - assertEquals(items[2], angular.Array.findById(items, 3)); -}; - ApiTest.prototype.testFilter = function() { var items = ["MIsKO", {name:"shyam"}, ["adam"], 1234]; assertEquals(4, angular.Array.filter(items, "").length); @@ -161,16 +125,6 @@ ApiTest.prototype.testCount = function() { assertEquals(1, angular.Array.count(array, 'name=="a"')); }; -ApiTest.prototype.testFind = function() { - var array = [{name:'a'},{name:'b'},{name:''}]; - var obj = {}; - - assertEquals(undefined, angular.Array.find(array, 'false')); - assertEquals('default', angular.Array.find(array, 'false', 'default')); - assertEquals('a', angular.Array.find(array, 'name == "a"').name); - assertEquals('', angular.Array.find(array, 'name == ""').name); -}; - ApiTest.prototype.testItShouldSortArray = function() { assertEquals([2,15], angular.Array.orderBy([15,2])); assertEquals(["a","B", "c"], angular.Array.orderBy(["c","B", "a"])); @@ -211,33 +165,6 @@ ApiTest.prototype.testQuoteUnicode = function(){ assertEquals('"abc\\u00a0def"', angular.String.quoteUnicode('abc\u00A0def')); }; -ApiTest.prototype.testMerge = function() { - var array = [{name:"misko"}]; - angular.Array.merge(array, 0, {name:"", email:"email1"}); - angular.Array.merge(array, 1, {name:"adam", email:"email2"}); - assertJsonEquals([{"email":"email1","name":"misko"},{"email":"email2","name":"adam"}], array); -}; - -ApiTest.prototype.testOrderByToggle = function() { - var orderByToggle = angular.Array.orderByToggle; - var predicate = []; - assertEquals(['+a'], orderByToggle(predicate, 'a')); - assertEquals(['-a'], orderByToggle(predicate, 'a')); - - assertEquals(['-a', '-b'], orderByToggle(['-b', 'a'], 'a')); -}; - -ApiTest.prototype.testOrderByToggle = function() { - var orderByDirection = angular.Array.orderByDirection; - assertEquals("", orderByDirection(['+a','b'], 'x')); - assertEquals("", orderByDirection(['+a','b'], 'b')); - assertEquals('ng-ascend', orderByDirection(['a','b'], 'a')); - assertEquals('ng-ascend', orderByDirection(['+a','b'], 'a')); - assertEquals('ng-descend', orderByDirection(['-a','b'], 'a')); - assertEquals('up', orderByDirection(['+a','b'], 'a', 'up', 'down')); - assertEquals('down', orderByDirection(['-a','b'], 'a', 'up', 'down')); -}; - ApiTest.prototype.testDateToUTC = function(){ var date = new Date("Sep 10 2003 13:02:03 GMT"); assertEquals("date", angular.Object.typeOf(date)); diff --git a/test/BinderTest.js b/test/BinderTest.js index 1b1201fa..f38383ae 100644 --- a/test/BinderTest.js +++ b/test/BinderTest.js @@ -407,6 +407,10 @@ BinderTest.prototype.testBindClass = function() { BinderTest.prototype.testBindClassEvenOdd = function() { var x = this.compile('<div><div ng:repeat="i in [0,1]" ng:class-even="\'e\'" ng:class-odd="\'o\'"/></div>'); x.scope.$eval(); + var d1 = jqLite(x.node[0].childNodes[1]); + var d2 = jqLite(x.node[0].childNodes[2]); + expect(d1.hasClass('o')).toBeTruthy(); + expect(d2.hasClass('e')).toBeTruthy(); assertEquals( '<div><#comment></#comment>' + '<div class="o" ng:class-even="\'e\'" ng:class-odd="\'o\'" ng:repeat-index="0"></div>' + @@ -472,13 +476,6 @@ BinderTest.prototype.testRepeaterShouldBindInputsDefaults = function () { assertEquals("misko", c.scope.$eval('items[1].name')); }; -BinderTest.prototype.testRepeaterShouldCreateArray = function () { - var c = this.compile('<input value="123" name="item.name" ng:repeat="item in items">'); - c.scope.$eval(); - - assertEquals(0, c.scope.$get('items').length); -}; - BinderTest.prototype.testShouldTemplateBindPreElements = function () { var c = this.compile('<pre>Hello {{name}}!</pre>'); c.scope.$set("name", "World"); diff --git a/test/BrowserSpecs.js b/test/BrowserSpecs.js index 3ce158b4..99632928 100644 --- a/test/BrowserSpecs.js +++ b/test/BrowserSpecs.js @@ -1,10 +1,14 @@ describe('browser', function(){ - var browser, location; + var browser, location, head; beforeEach(function(){ location = {href:"http://server", hash:""}; - browser = new Browser(location, {}); + head = { + scripts: [], + append: function(node){head.scripts.push(node);} + }; + browser = new Browser(location, jqLite(window.document), head); browser.setTimeout = noop; }); @@ -45,4 +49,22 @@ describe('browser', function(){ }); }); + describe('xhr', function(){ + describe('JSON', function(){ + it('should add script tag for request', function() { + var log = ""; + browser.xhr('JSON', 'http://example.org/path?cb=JSON_CALLBACK', function(code, data){ + log += code + ':' + data + ';'; + }); + expect(head.scripts.length).toEqual(1); + var url = head.scripts[0].src.split('?cb='); + expect(url[0]).toEqual('http://example.org/path'); + expect(typeof window[url[1]]).toEqual('function'); + window[url[1]]('data'); + expect(log).toEqual('200:data;'); + expect(typeof window[url[1]]).toEqual('undefined'); + }); + }); + }); + }); diff --git a/test/CompilerSpec.js b/test/CompilerSpec.js index da354ea5..1091337b 100644 --- a/test/CompilerSpec.js +++ b/test/CompilerSpec.js @@ -1,5 +1,5 @@ describe('compiler', function(){ - var compiler, textMarkup, directives, widgets, compile, log; + var compiler, markup, directives, widgets, compile, log; beforeEach(function(){ log = ""; @@ -20,10 +20,10 @@ describe('compiler', function(){ } }; - textMarkup = []; + markup = []; attrMarkup = []; - widgets = {}; - compiler = new Compiler(textMarkup, attrMarkup, directives, widgets); + widgets = extensionMap({}, 'widget'); + compiler = new Compiler(markup, attrMarkup, directives, widgets); compile = function(html){ var e = jqLite("<div>" + html + "</div>"); var scope = compiler.compile(e)(e); @@ -94,7 +94,7 @@ describe('compiler', function(){ }); it('should process markup before directives', function(){ - textMarkup.push(function(text, textNode, parentNode) { + markup.push(function(text, textNode, parentNode) { if (text == 'middle') { expect(textNode.text()).toEqual(text); parentNode.attr('hello', text); @@ -126,7 +126,7 @@ describe('compiler', function(){ this.directives(true); return noop; }; - textMarkup.push(function(text, textNode, parent){ + markup.push(function(text, textNode, parent){ if (text == '{{1+2}}') parent.text('3'); }); diff --git a/test/FiltersTest.js b/test/FiltersTest.js index 903a7a2f..d5484fd0 100644 --- a/test/FiltersTest.js +++ b/test/FiltersTest.js @@ -41,54 +41,6 @@ FiltersTest.prototype.testJson = function () { assertEquals(toJson({a:"b"}, true), angular.filter.json.call({$element:jqLite('<div></div>')}, {a:"b"})); }; -FiltersTest.prototype.testPackageTracking = function () { - var assert = function(title, trackingNo) { - var val = angular.filter.trackPackage(trackingNo, title); - assertNotNull("Did Not Match: " + trackingNo, val); - assertEquals(title + ": " + trim(trackingNo), val.text()); - assertNotNull(val.attr('href')); - }; - assert('UPS', ' 1Z 999 999 99 9999 999 9 '); - assert('UPS', '1ZW5w5220379084747'); - - assert('FedEx', '418822131061812'); - assert('FedEx', '9612019 5935 3267 2473 738'); - assert('FedEx', '9612019593532672473738'); - assert('FedEx', '235354667129449'); - assert('FedEx', '915368880571'); - assert('FedEx', '901712142390'); - assert('FedEx', '297391510063413'); - - assert('USPS', '9101 8052 1390 7402 4335 49'); - assert('USPS', '9101010521297963339560'); - assert('USPS', '9102901001301038667029'); - assert('USPS', '910 27974 4490 3000 8916 56'); - assert('USPS', '9102801438635051633253'); -}; - -FiltersTest.prototype.testLink = function() { - var assert = function(text, url, obj){ - var val = angular.filter.link(obj); - assertEquals('<a href="' + url + '">' + text + '</a>', sortedHtml(val)); - }; - assert("url", "url", "url"); - assert("hello", "url", {text:"hello", url:"url"}); - assert("a@b.com", "mailto:a@b.com", "a@b.com"); -}; - -FiltersTest.prototype.testImage = function(){ - assertEquals(null, angular.filter.image()); - assertEquals(null, angular.filter.image({})); - assertEquals(null, angular.filter.image("")); - assertEquals('http://localhost/abc', angular.filter.image({url:"http://localhost/abc"}).attr('src')); -}; - -FiltersTest.prototype.testQRcode = function() { - assertEquals( - 'http://chart.apis.google.com/chart?chl=Hello%20world&chs=200x200&cht=qr', - angular.filter.qrcode('Hello world').attr('src')); -}; - FiltersTest.prototype.testLowercase = function() { assertEquals('abc', angular.filter.lowercase('AbC')); assertEquals(null, angular.filter.lowercase(null)); @@ -99,30 +51,6 @@ FiltersTest.prototype.testUppercase = function() { assertEquals(null, angular.filter.uppercase(null)); }; -FiltersTest.prototype.testLineCount = function() { - assertEquals(1, angular.filter.linecount(null)); - assertEquals(1, angular.filter.linecount('')); - assertEquals(1, angular.filter.linecount('a')); - assertEquals(2, angular.filter.linecount('a\nb')); - assertEquals(3, angular.filter.linecount('a\nb\nc')); -}; - -FiltersTest.prototype.testIf = function() { - assertEquals('A', angular.filter['if']('A', true)); - assertEquals(undefined, angular.filter['if']('A', false)); -}; - -FiltersTest.prototype.testUnless = function() { - assertEquals('A', angular.filter.unless('A', false)); - assertEquals(undefined, angular.filter.unless('A', true)); -}; - -FiltersTest.prototype.testGoogleChartApiEncode = function() { - assertEquals( - 'http://chart.apis.google.com/chart?chl=Hello world&chs=200x200&cht=qr', - angular.filter.googleChartApi.encode({cht:"qr", chl:"Hello world"}).attr('src')); -}; - FiltersTest.prototype.testHtml = function() { var html = angular.filter.html("a<b>c</b>d"); expect(html instanceof HTML).toBeTruthy(); @@ -140,4 +68,3 @@ FiltersTest.prototype.testLinky = function() { assertEquals(undefined, linky(undefined)); }; - diff --git a/test/JsonTest.js b/test/JsonTest.js index 1ed56da8..f6da26b5 100644 --- a/test/JsonTest.js +++ b/test/JsonTest.js @@ -74,6 +74,13 @@ JsonTest.prototype.testItShouldPreventRecursion = function () { assertEquals('{"a":"b","recursion":RECURSION}', angular.toJson(obj)); }; +JsonTest.prototype.testItShouldSerializeOnlyOwnProperties = function() { + var parent = createScope(); + var child = createScope(parent); + child.c = 'c'; + expect(angular.toJson(child)).toEqual('{"c":"c"}'); +}; + JsonTest.prototype.testItShouldSerializeSameObjectsMultipleTimes = function () { var obj = {a:'b'}; assertEquals('{"A":{"a":"b"},"B":{"a":"b"}}', angular.toJson({A:obj, B:obj})); @@ -82,3 +89,7 @@ JsonTest.prototype.testItShouldSerializeSameObjectsMultipleTimes = function () { JsonTest.prototype.testItShouldNotSerializeUndefinedValues = function () { assertEquals('{}', angular.toJson({A:undefined})); }; + +JsonTest.prototype.testItShouldParseFloats = function () { + expect(fromJson("{value:2.55, name:'misko'}")).toEqual({value:2.55, name:'misko'}); +}; diff --git a/test/ParserTest.js b/test/ParserTest.js index 7ba65f18..d7fd2f94 100644 --- a/test/ParserTest.js +++ b/test/ParserTest.js @@ -147,6 +147,11 @@ LexerTest.prototype.testStatements = function(){ assertEquals(tokens[3].text, ';'); }; +LexerTest.prototype.testNumber = function(){ + var tokens = new Lexer("0.5").parse(); + expect(tokens[0].text).toEqual(0.5); +}; + ParserTest = TestCase('ParserTest'); ParserTest.prototype.testExpressions = function(){ diff --git a/test/ResourceSpec.js b/test/ResourceSpec.js index 4882e70e..546e9aec 100644 --- a/test/ResourceSpec.js +++ b/test/ResourceSpec.js @@ -28,6 +28,18 @@ describe("resource", function() { resource.route('URL').query(); }); + it('should ignore slashes of undefinend parameters', function(){ + var R = resource.route('/Path/:a/:b/:c'); + xhr.expectGET('/Path').respond({}); + xhr.expectGET('/Path/1').respond({}); + xhr.expectGET('/Path/2/3').respond({}); + xhr.expectGET('/Path/4/5/6').respond({}); + R.get({}); + R.get({a:1}); + R.get({a:2, b:3}); + R.get({a:4, b:5, c:6}); + }); + it("should build resource with default param", function(){ xhr.expectGET('/Order/123/Line/456.visa?minimum=0.05').respond({id:'abc'}); var LineItem = resource.route('/Order/:orderId/Line/:id:verb', {orderId: '123', id: '@id.key', verb:'.visa', minimum:0.05}); @@ -102,6 +114,13 @@ describe("resource", function() { CreditCard.charge({id:123, amount:10},{auth:'abc'}, callback); }); + it('should post charge verb on instance', function(){ + xhr.expectPOST('/CreditCard/123!charge?amount=10', {id:{key:123}, name:'misko'}).respond({success:'ok'}); + + var card = new CreditCard({id:{key:123}, name:'misko'}); + card.$charge({amount:10}, callback); + }); + it('should create on save', function(){ xhr.expectPOST('/CreditCard', {name:'misko'}).respond({id:123}); var cc = new CreditCard(); diff --git a/test/ScopeSpec.js b/test/ScopeSpec.js index d93400e5..ea63fea4 100644 --- a/test/ScopeSpec.js +++ b/test/ScopeSpec.js @@ -15,28 +15,42 @@ describe('scope/model', function(){ expect(model.$root).toEqual(model); }); + it('should return noop function when LHS is undefined', function(){ + var model = createScope(); + expect(model.$eval('x.$filter()')).toEqual(undefined); + }); + describe('$eval', function(){ - it('should eval function with correct this and pass arguments', function(){ - var model = createScope(); - model.$eval(function(name){ - this.name = name; - }, 'works'); + var model; + + beforeEach(function(){model = createScope();}); + + it('should eval function with correct this', function(){ + model.$eval(function(){ + this.name = 'works'; + }); expect(model.name).toEqual('works'); }); it('should eval expression with correct this', function(){ - var model = createScope(); model.$eval('name="works"'); expect(model.name).toEqual('works'); }); it('should do nothing on empty string and not update view', function(){ - var model = createScope(); var onEval = jasmine.createSpy('onEval'); model.$onEval(onEval); model.$eval(''); expect(onEval).wasNotCalled(); }); + + it('should ignore none string/function', function(){ + model.$eval(null); + model.$eval({}); + model.$tryEval(null); + model.$tryEval({}); + }); + }); describe('$watch', function(){ diff --git a/test/angular-mocks.js b/test/angular-mocks.js index 8838b2cd..bac2e800 100644 --- a/test/angular-mocks.js +++ b/test/angular-mocks.js @@ -66,6 +66,7 @@ function MockBrowser() { self.xhr.expectPOST = angular.bind(self, self.xhr.expect, 'POST'); self.xhr.expectDELETE = angular.bind(self, self.xhr.expect, 'DELETE'); self.xhr.expectPUT = angular.bind(self, self.xhr.expect, 'PUT'); + self.xhr.expectJSON = angular.bind(self, self.xhr.expect, 'JSON'); self.xhr.flush = function() { while(requests.length) { requests.pop()(); diff --git a/test/delete/ScopeTest.js b/test/delete/ScopeTest.js deleted file mode 100644 index 24febf19..00000000 --- a/test/delete/ScopeTest.js +++ /dev/null @@ -1,145 +0,0 @@ -ScopeTest = TestCase('ScopeTest'); - -ScopeTest.prototype.testGetScopeRetrieval = function(){ - var scope = {}; - var form = jQuery("<a><b><c></c></b></a>"); - form.data('scope', scope); - var c = form.find('c'); - assertTrue(scope === c.scope()); -}; - -ScopeTest.prototype.testGetScopeRetrievalIntermediateNode = function(){ - var scope = {}; - var form = jQuery("<a><b><c></c></b></a>"); - form.find("b").data('scope', scope); - var b = form.find('b'); - assertTrue(scope === b.scope()); -}; - -ScopeTest.prototype.testNoScopeDoesNotCauseInfiniteRecursion = function(){ - var form = jQuery("<a><b><c></c></b></a>"); - var c = form.find('c'); - assertTrue(!c.scope()); -}; - -ScopeTest.prototype.testScopeEval = function(){ - var scope = new Scope({b:345}); - assertEquals(scope.eval('b = 123'), 123); - assertEquals(scope.get('b'), 123); -}; - -ScopeTest.prototype.testScopeFromPrototype = function(){ - var scope = new Scope({b:123}); - scope.eval('a = b'); - scope.eval('b = 456'); - assertEquals(scope.get('a'), 123); - assertEquals(scope.get('b'), 456); -}; - -ScopeTest.prototype.testSetScopeGet = function(){ - var scope = new Scope(); - assertEquals(987, scope.set('a', 987)); - assertEquals(scope.get('a'), 987); - assertEquals(scope.eval('a'), 987); -}; - -ScopeTest.prototype.testGetChain = function(){ - var scope = new Scope({a:{b:987}}); - assertEquals(scope.get('a.b'), 987); - assertEquals(scope.eval('a.b'), 987); -}; - -ScopeTest.prototype.testGetUndefinedChain = function(){ - var scope = new Scope(); - assertEquals(typeof scope.get('a.b'), 'undefined'); -}; - -ScopeTest.prototype.testSetChain = function(){ - var scope = new Scope({a:{}}); - scope.set('a.b', 987); - assertEquals(scope.get('a.b'), 987); - assertEquals(scope.eval('a.b'), 987); -}; - -ScopeTest.prototype.testSetGetOnChain = function(){ - var scope = new Scope(); - scope.set('a.b', 987); - assertEquals(scope.get('a.b'), 987); - assertEquals(scope.eval('a.b'), 987); -}; - -ScopeTest.prototype.testGlobalFunctionAccess =function(){ - window['scopeAddTest'] = function (a, b) {return a+b;}; - var scope = new Scope({window:window}); - assertEquals(scope.eval('window.scopeAddTest(1,2)'), 3); - - scope.set('add', function (a, b) {return a+b;}); - assertEquals(scope.eval('add(1,2)'), 3); - - scope.set('math.add', function (a, b) {return a+b;}); - assertEquals(scope.eval('math.add(1,2)'), 3); -}; - -ScopeTest.prototype.testValidationEval = function(){ - expectAsserts(4); - var scope = new Scope(); - scope.set("name", "misko"); - angular.validator.testValidator = function(value, expect){ - assertEquals("misko", this.name); - return value == expect ? null : "Error text"; - }; - - assertEquals("Error text", scope.validate("testValidator:'abc'", 'x')); - assertEquals(null, scope.validate("testValidator:'abc'", 'abc')); - - delete angular.validator['testValidator']; -}; - -ScopeTest.prototype.testCallingNonExistantMethodShouldProduceFriendlyException = function() { - expectAsserts(1); - var scope = new Scope({obj:{}}); - try { - scope.eval("obj.iDontExist()"); - fail(); - } catch (e) { - assertEquals("Expression 'obj.iDontExist' is not a function.", e); - } -}; - -ScopeTest.prototype.testAccessingWithInvalidPathShouldThrowError = function() { - var scope = new Scope(); - try { - scope.get('a.{{b}}'); - fail(); - } catch (e) { - assertEquals("Expression 'a.{{b}}' is not a valid expression for accesing variables.", e); - } -}; - -ScopeTest.prototype.testItShouldHave$parent = function() { - var parent = new Scope({}, "ROOT"); - var child = new Scope(parent.state); - assertSame("parent", child.state.$parent, parent.state); - assertSame("root", child.state.$root, parent.state); -}; - -ScopeTest.prototype.testItShouldHave$root = function() { - var scope = new Scope({}, "ROOT"); - assertSame(scope.state.$root, scope.state); -}; - -ScopeTest.prototype.testItShouldBuildPathOnUndefined = function(){ - var scope = new Scope({}, "ROOT"); - scope.setEval("a.$b.c", 1); - assertJsonEquals({$b:{c:1}}, scope.get("a")); -}; - -ScopeTest.prototype.testItShouldMapUnderscoreFunctions = function(){ - var scope = new Scope({}, "ROOT"); - scope.set("a", [1,2,3]); - assertEquals('function', typeof scope.get("a.$size")); - scope.eval("a.$includeIf(4,true)"); - assertEquals(4, scope.get("a.$size")()); - assertEquals(4, scope.eval("a.$size()")); - assertEquals('undefined', typeof scope.get("a.dontExist")); -}; diff --git a/test/delete/WidgetsTest.js b/test/delete/WidgetsTest.js deleted file mode 100644 index ccc87afd..00000000 --- a/test/delete/WidgetsTest.js +++ /dev/null @@ -1,268 +0,0 @@ -WidgetTest = TestCase('WidgetTest'); - -WidgetTest.prototype.testRequired = function () { - var view = $('<input name="a" ng:required>'); - var scope = new Scope({$invalidWidgets:[]}); - var cntl = new TextController(view[0], 'a', angularFormatter.noop); - cntl.updateView(scope); - assertTrue(view.hasClass('ng-validation-error')); - assertEquals("Required Value", view.attr('ng-error')); - scope.set('a', 'A'); - cntl.updateView(scope); - assertFalse(view.hasClass('ng-validation-error')); - assertEquals("undefined", typeof view.attr('ng-error')); -}; - -WidgetTest.prototype.testValidator = function () { - var view = $('<input name="a" ng:validate="testValidator:\'ABC\'">'); - var scope = new Scope({$invalidWidgets:[]}); - var cntl = new TextController(view[0], 'a', angularFormatter.noop); - angular.validator.testValidator = function(value, expect){ - return value == expect ? false : "Error text"; - }; - - scope.set('a', ''); - cntl.updateView(scope); - assertEquals(view.hasClass('ng-validation-error'), false); - assertEquals(null, view.attr('ng-error')); - - scope.set('a', 'X'); - cntl.updateView(scope); - assertEquals(view.hasClass('ng-validation-error'), true); - assertEquals(view.attr('ng-error'), "Error text"); - assertEquals("Error text", view.attr('ng-error')); - - scope.set('a', 'ABC'); - cntl.updateView(scope); - assertEquals(view.hasClass('ng-validation-error'), false); - assertEquals(view.attr('ng-error'), null); - assertEquals(null, view.attr('ng-error')); - - delete angular.validator['testValidator']; -}; - -WidgetTest.prototype.testRequiredValidator = function () { - var view = $('<input name="a" ng:required ng:validate="testValidator:\'ABC\'">'); - var scope = new Scope({$invalidWidgets:[]}); - var cntl = new TextController(view[0], 'a', angularFormatter.noop); - angular.validator.testValidator = function(value, expect){ - return value == expect ? null : "Error text"; - }; - - scope.set('a', ''); - cntl.updateView(scope); - assertEquals(view.hasClass('ng-validation-error'), true); - assertEquals("Required Value", view.attr('ng-error')); - - scope.set('a', 'X'); - cntl.updateView(scope); - assertEquals(view.hasClass('ng-validation-error'), true); - assertEquals("Error text", view.attr('ng-error')); - - scope.set('a', 'ABC'); - cntl.updateView(scope); - assertEquals(view.hasClass('ng-validation-error'), false); - assertEquals(null, view.attr('ng-error')); - - delete angular.validator['testValidator']; -}; - -TextControllerTest = TestCase("TextControllerTest"); - -TextControllerTest.prototype.testDatePicker = function() { - var input = $('<input type="text" ng-widget="datepicker">'); - input.data('scope', new Scope()); - var body = $(document.body); - body.append(input); - var binder = new Binder(input[0], new WidgetFactory()); - assertTrue('before', input.data('datepicker') === undefined); - binder.compile(); - assertTrue('after', input.data('datepicker') !== null); - assertTrue(body.html(), input.hasClass('hasDatepicker')); -}; - -RepeaterUpdaterTest = TestCase("RepeaterUpdaterTest"); - -RepeaterUpdaterTest.prototype.testRemoveThenAdd = function() { - var view = $("<div><span/></div>"); - var template = function () { - return $("<li/>"); - }; - var repeater = new RepeaterUpdater(view.find("span"), "a in b", template, ""); - var scope = new Scope(); - scope.set('b', [1,2]); - - repeater.updateView(scope); - - scope.set('b', []); - repeater.updateView(scope); - - scope.set('b', [1]); - repeater.updateView(scope); - assertEquals(1, view.find("li").size()); -}; - -RepeaterUpdaterTest.prototype.testShouldBindWidgetOnRepeaterClone = function(){ - //fail(); -}; - -RepeaterUpdaterTest.prototype.testShouldThrowInformativeSyntaxError= function(){ - expectAsserts(1); - try { - var repeater = new RepeaterUpdater(null, "a=b"); - } catch (e) { - assertEquals("Expected ng:repeat in form of 'item in collection' but got 'a=b'.", e); - } -}; - -SelectControllerTest = TestCase("SelectControllerTest"); - -SelectControllerTest.prototype.testShouldUpdateModelNullOnNothingSelected = function(){ - var scope = new Scope(); - var view = {selectedIndex:-1, options:[]}; - var cntl = new SelectController(view, 'abc'); - cntl.updateModel(scope); - assertNull(scope.get('abc')); -}; - -SelectControllerTest.prototype.testShouldUpdateModelWhenNothingSelected = function(){ - var scope = new Scope(); - var view = {value:'123'}; - var cntl = new SelectController(view, 'abc'); - cntl.updateView(scope); - assertEquals("123", scope.get('abc')); -}; - -BindUpdaterTest = TestCase("BindUpdaterTest"); - -BindUpdaterTest.prototype.testShouldDisplayNothingForUndefined = function () { - var view = $('<span />'); - var controller = new BindUpdater(view[0], "{{a}}"); - var scope = new Scope(); - - scope.set('a', undefined); - controller.updateView(scope); - assertEquals("", view.text()); - - scope.set('a', null); - controller.updateView(scope); - assertEquals("", view.text()); -}; - -BindUpdaterTest.prototype.testShouldDisplayJsonForNonStrings = function () { - var view = $('<span />'); - var controller = new BindUpdater(view[0], "{{obj}}"); - - controller.updateView(new Scope({obj:[]})); - assertEquals("[]", view.text()); - - controller.updateView(new Scope({obj:{text:'abc'}})); - assertEquals('abc', fromJson(view.text()).text); -}; - - -BindUpdaterTest.prototype.testShouldInsertHtmlNode = function () { - var view = $('<span />'); - var controller = new BindUpdater(view[0], "<fake>&{{obj}}</fake>"); - var scope = new Scope(); - - scope.set("obj", $('<div>myDiv</div>')[0]); - controller.updateView(scope); - assertEquals("<fake>&myDiv</fake>", view.text()); -}; - - -BindUpdaterTest.prototype.testShouldDisplayTextMethod = function () { - var view = $('<div />'); - var controller = new BindUpdater(view[0], "{{obj}}"); - var scope = new Scope(); - - scope.set("obj", new angular.filter.Meta({text:function(){return "abc";}})); - controller.updateView(scope); - assertEquals("abc", view.text()); - - scope.set("obj", new angular.filter.Meta({text:"123"})); - controller.updateView(scope); - assertEquals("123", view.text()); - - scope.set("obj", {text:"123"}); - controller.updateView(scope); - assertEquals("123", fromJson(view.text()).text); -}; - -BindUpdaterTest.prototype.testShouldDisplayHtmlMethod = function () { - var view = $('<div />'); - var controller = new BindUpdater(view[0], "{{obj}}"); - var scope = new Scope(); - - scope.set("obj", new angular.filter.Meta({html:function(){return "a<div>b</div>c";}})); - controller.updateView(scope); - assertEquals("abc", view.text()); - - scope.set("obj", new angular.filter.Meta({html:"1<div>2</div>3"})); - controller.updateView(scope); - assertEquals("123", view.text()); - - scope.set("obj", {html:"123"}); - controller.updateView(scope); - assertEquals("123", fromJson(view.text()).html); -}; - -BindUpdaterTest.prototype.testUdateBoolean = function() { - var view = $('<div />'); - var controller = new BindUpdater(view[0], "{{true}}, {{false}}"); - controller.updateView(new Scope()); - assertEquals('true, false', view.text()); -}; - -BindAttrUpdaterTest = TestCase("BindAttrUpdaterTest"); - -BindAttrUpdaterTest.prototype.testShouldLoadBlankImageWhenBindingIsUndefined = function () { - var view = $('<img />'); - var controller = new BindAttrUpdater(view[0], {src: '{{imageUrl}}'}); - - var scope = new Scope(); - scope.set('imageUrl', undefined); - scope.set('$config.blankImage', 'http://server/blank.gif'); - - controller.updateView(scope); - assertEquals("http://server/blank.gif", view.attr('src')); -}; - -RepeaterUpdaterTest.prototype.testShouldNotDieWhenRepeatExpressionIsNull = function() { - var rep = new RepeaterUpdater(null, "$item in items", null, null); - var scope = new Scope(); - scope.set('items', undefined); - rep.updateView(scope); -}; - -RepeaterUpdaterTest.prototype.testShouldIterateOverKeys = function() { - var rep = new RepeaterUpdater(null, "($k,_v) in items", null, null); - assertEquals("items", rep.iteratorExp); - assertEquals("_v", rep.valueExp); - assertEquals("$k", rep.keyExp); -}; - -EvalUpdaterTest = TestCase("EvalUpdaterTest"); -EvalUpdaterTest.prototype.testEvalThrowsException = function(){ - var view = $('<div/>'); - var eval = new EvalUpdater(view[0], 'undefined()'); - - eval.updateView(new Scope()); - assertTrue(!!view.attr('ng-error')); - assertTrue(view.hasClass('ng-exception')); - - eval.exp = "1"; - eval.updateView(new Scope()); - assertFalse(!!view.attr('ng-error')); - assertFalse(view.hasClass('ng-exception')); -}; - -RadioControllerTest = TestCase("RadioController"); -RadioControllerTest.prototype.testItShouldTreatTrueStringAsBoolean = function () { - var view = $('<input type="radio" name="select" value="true"/>'); - var radio = new RadioController(view[0], 'select'); - var scope = new Scope({select:true}); - radio.updateView(scope); - assertTrue(view[0].checked); -}; diff --git a/test/directivesSpec.js b/test/directivesSpec.js index ef4814bf..f0eb5c09 100644 --- a/test/directivesSpec.js +++ b/test/directivesSpec.js @@ -29,35 +29,60 @@ describe("directives", function(){ expect(scope.a).toEqual(2); }); - it('should ng:bind', function() { - var scope = compile('<div ng:bind="a"></div>'); - expect(element.text()).toEqual(''); - scope.a = 'misko'; - scope.$eval(); - expect(element.text()).toEqual('misko'); - }); + describe('ng:bind', function(){ + it('should set text', function() { + var scope = compile('<div ng:bind="a"></div>'); + expect(element.text()).toEqual(''); + scope.a = 'misko'; + scope.$eval(); + expect(element.text()).toEqual('misko'); + }); - it('should ng:bind html', function() { - var scope = compile('<div ng:bind="html|html"></div>'); - scope.html = '<div>hello</div>'; - scope.$eval(); - expect(lowercase(element.html())).toEqual('<div>hello</div>'); - }); + it('should set html', function() { + var scope = compile('<div ng:bind="html|html"></div>'); + scope.html = '<div>hello</div>'; + scope.$eval(); + expect(lowercase(element.html())).toEqual('<div>hello</div>'); + }); + + it('should set element element', function() { + angularFilter.myElement = function() { + return jqLite('<a>hello</a>'); + }; + var scope = compile('<div ng:bind="0|myElement"></div>'); + scope.$eval(); + expect(lowercase(element.html())).toEqual('<a>hello</a>'); + }); + + it('should have $element set to current bind element', function(){ + angularFilter.myFilter = function(){ + this.$element.text('HELLO'); + }; + var scope = compile('<div>before<div ng:bind="0|myFilter"></div>after</div>'); + expect(scope.$element.text()).toEqual("beforeHELLOafter"); + }); - it('should ng:bind element', function() { - angularFilter.myElement = function() { - return jqLite('<a>hello</a>'); - }; - var scope = compile('<div ng:bind="0|myElement"></div>'); - scope.$eval(); - expect(lowercase(element.html())).toEqual('<a>hello</a>'); }); - it('should ng:bind-template', function() { - var scope = compile('<div ng:bind-template="Hello {{name}}!"></div>'); - scope.$set('name', 'Misko'); - scope.$eval(); - expect(element.text()).toEqual('Hello Misko!'); + describe('ng:bind-template', function(){ + it('should ng:bind-template', function() { + var scope = compile('<div ng:bind-template="Hello {{name}}!"></div>'); + scope.$set('name', 'Misko'); + scope.$eval(); + expect(element.text()).toEqual('Hello Misko!'); + }); + + it('should have $element set to current bind element', function(){ + var innerText = 'blank'; + angularFilter.myFilter = function(text){ + innerText = this.$element.text(); + return text; + }; + var scope = compile('<div>before<span ng:bind-template="{{\'HELLO\'|myFilter}}">INNER</span>after</div>'); + expect(scope.$element.text()).toEqual("beforeHELLOafter"); + expect(innerText).toEqual('INNER'); + }); + }); it('should ng:bind-attr', function(){ @@ -115,11 +140,6 @@ describe("directives", function(){ expect(element.text()).toEqual('misko:swe;shyam:set;'); }); - it('should set ng:repeat to [] if undefinde', function(){ - var scope = compile('<ul><li ng:repeat="item in items"></li></ul>'); - expect(scope.items).toEqual([]); - }); - it('should error on wrong parsing of ng:repeat', function(){ var scope = compile('<ul><li ng:repeat="i dont parse"></li></ul>'); var log = ""; @@ -140,13 +160,15 @@ describe("directives", function(){ expect(scope.$get('count')).toEqual(1); }); - it('should ng:click', function(){ - var scope = compile('<div ng:click="clicked = true"></div>'); - scope.$eval(); - expect(scope.$get('clicked')).toBeFalsy(); + describe('ng:click', function(){ + it('should fire event', function(){ + var scope = compile('<div ng:click="clicked = true"></div>'); + scope.$eval(); + expect(scope.$get('clicked')).toBeFalsy(); - element.trigger('click'); - expect(scope.$get('clicked')).toEqual(true); + element.trigger('click'); + expect(scope.$get('clicked')).toEqual(true); + }); }); it('should ng:class', function(){ @@ -168,16 +190,35 @@ describe("directives", function(){ expect(e2.hasClass('even')).toBeTruthy(); }); - it('should ng:style', function(){ - var scope = compile('<div ng:style="{color:\'red\'}"></div>'); - scope.$eval(); - expect(element.css('color')).toEqual('red'); + describe('ng:style', function(){ + it('should set', function(){ + var scope = compile('<div ng:style="{color:\'red\'}"></div>'); + scope.$eval(); + expect(element.css('color')).toEqual('red'); + }); + + it('should silently ignore undefined style', function() { + var scope = compile('<div ng:style="myStyle"></div>'); + scope.$eval(); + expect(element.hasClass('ng-exception')).toBeFalsy(); + }); + + it('should preserve and remove previous style', function(){ + var scope = compile('<div style="color:red;" ng:style="myStyle"></div>'); + scope.$eval(); + expect(getStyle(element)).toEqual({color:'red'}); + scope.myStyle = {color:'blue', width:'10px'}; + scope.$eval(); + expect(getStyle(element)).toEqual({color:'blue', width:'10px'}); + scope.myStyle = {}; + scope.$eval(); + expect(getStyle(element)).toEqual({color:'red'}); + }); }); it('should silently ignore undefined ng:style', function() { var scope = compile('<div ng:style="myStyle"></div>'); scope.$eval(); - dump(sortedHtml(element)); expect(element.hasClass('ng-exception')).toBeFalsy(); }); diff --git a/test/moveToAngularCom/Base64Test.js b/test/moveToAngularCom/Base64Test.js deleted file mode 100644 index a9353186..00000000 --- a/test/moveToAngularCom/Base64Test.js +++ /dev/null @@ -1,5 +0,0 @@ -Base64Test = TestCase('Base64Test'); - -Base64Test.prototype.testEncodeDecode = function(){ - assertEquals(Base64.decode(Base64.encode('hello')), 'hello'); -}; diff --git a/test/moveToAngularCom/DataStoreTest.js b/test/moveToAngularCom/DataStoreTest.js deleted file mode 100644 index 87c5be2e..00000000 --- a/test/moveToAngularCom/DataStoreTest.js +++ /dev/null @@ -1,616 +0,0 @@ -DataStoreTest = TestCase('DataStoreTest'); - -DataStoreTest.prototype.testSavePostsToServer = function(){ - expectAsserts(10); - var response; - var post = function(data, callback){ - var method = data[0][0]; - var posted = data[0][2]; - assertEquals("POST", method); - assertEquals("abc", posted.$entity); - assertEquals("123", posted.$id); - assertEquals("1", posted.$version); - assertFalse('function' == typeof posted.save); - response = fromJson(toJson(posted)); - response.$entity = "abc"; - response.$id = "123"; - response.$version = "2"; - callback(200, [response]); - }; - var datastore = new DataStore(post); - var model = datastore.entity('abc', {name: "value"})(); - model.$id = "123"; - model.$version = "1"; - - datastore.save(model, function(obj){ - assertTrue(obj === model); - assertEquals(obj.$entity, "abc"); - assertEquals(obj.$id, "123"); - assertEquals(obj.$version, "2"); - assertEquals(obj.name, "value"); - obj.after = true; - }); - datastore.flush(); -}; - -DataStoreTest.prototype.testLoadGetsFromServer = function(){ - expectAsserts(12); - var post = function(data, callback){ - var method = data[0][0]; - var path = data[0][1]; - assertEquals("GET", method); - assertEquals("abc/1", path); - response = [{$entity:'abc', $id:'1', $version:'2', key:"value"}]; - callback(200, response); - }; - var datastore = new DataStore(post); - - var model = datastore.entity("abc", {merge:true})(); - assertEquals(datastore.load(model, '1', function(obj){ - assertEquals(obj.$entity, "abc"); - assertEquals(obj.$id, "1"); - assertEquals(obj.$version, "2"); - assertEquals(obj.key, "value"); - }), model); - datastore.flush(); - assertEquals(model.$entity, "abc"); - assertEquals(model.$id, "1"); - assertEquals(model.$version, "2"); - assertEquals(model.key, "value"); - assertEquals(model.merge, true); -}; - -DataStoreTest.prototype.testRemove = function(){ - expectAsserts(8); - var response; - var post = function(data, callback){ - var method = data[0][0]; - var posted = data[0][2]; - assertEquals("DELETE", method); - assertEquals("abc", posted.$entity); - assertEquals("123", posted.$id); - assertEquals("1", posted.$version); - assertFalse('function' == typeof posted.save); - response = fromJson(toJson(posted)); - response.$entity = "abc"; - response.$id = "123"; - response.$version = "2"; - callback(200, [response]); - }; - var model; - var datastore = new DataStore(post); - model = datastore.entity('abc', {name: "value"})(); - model.$id = "123"; - model.$version = "1"; - - datastore.remove(model, function(obj){ - assertEquals(obj.$id, "123"); - assertEquals(obj.$version, "2"); - assertEquals(obj.name, "value"); - obj.after = true; - }); - datastore.flush(); - -}; - - -DataStoreTest.prototype.test401ResponseDoesNotCallCallback = function(){ - expectAsserts(1); - var post = function(data, callback) { - callback(200, {$status_code: 401}); - }; - - var datastore = new DataStore(post, {login:function(){ - assertTrue(true); - }}); - - var onLoadAll = function(){ - assertTrue(false, "onLoadAll should not be called when response is status 401"); - }; - datastore.bulkRequest.push({}); - datastore.flush(); - datastore.loadAll({type: "A"}, onLoadAll); -}; - -DataStoreTest.prototype.test403ResponseDoesNotCallCallback = function(){ - expectAsserts(1); - var post = function(data, callback) { - callback(200, [{$status_code: 403}]); - }; - - var datastore = new DataStore(post, {notAuthorized:function(){ - assertTrue(true); - }}); - - var onLoadAll = function(){ - assertTrue(false, "onLoadAll should not be called when response is status 403"); - }; - datastore.bulkRequest.push({}); - datastore.flush(); - datastore.loadAll({type: "A"}, onLoadAll); -}; - -DataStoreTest.prototype.testLoadCalledWithoutIdShouldBeNoop = function(){ - expectAsserts(2); - var post = function(url, callback){ - assertTrue(false); - }; - var datastore = new DataStore(post); - var model = datastore.entity("abc")(); - assertEquals(datastore.load(model, undefined), model); - assertEquals(model.$entity, "abc"); -}; - -DataStoreTest.prototype.testEntityFactory = function(){ - var ds = new DataStore(); - var Recipe = ds.entity("Recipe", {a:1, b:2}); - assertEquals(Recipe.title, "Recipe"); - assertEquals(Recipe.defaults.a, 1); - assertEquals(Recipe.defaults.b, 2); - - var recipe = Recipe(); - assertEquals(recipe.$entity, "Recipe"); - assertEquals(recipe.a, 1); - assertEquals(recipe.b, 2); - - recipe = new Recipe(); - assertEquals(recipe.$entity, "Recipe"); - assertEquals(recipe.a, 1); - assertEquals(recipe.b, 2); -}; - -DataStoreTest.prototype.testEntityFactoryNoDefaults = function(){ - var ds = new DataStore(); - var Recipe = ds.entity("Recipe"); - assertEquals(Recipe.title, "Recipe"); - - recipe = new Recipe(); - assertEquals(recipe.$entity, "Recipe"); -}; - -DataStoreTest.prototype.testEntityFactoryWithInitialValues = function(){ - var ds = new DataStore(); - var Recipe = ds.entity("Recipe"); - - var recipe = Recipe({name: "name"}); - assertEquals("name", recipe.name); -}; - -DataStoreTest.prototype.testEntityLoad = function(){ - var ds = new DataStore(); - var Recipe = ds.entity("Recipe", {a:1, b:2}); - ds.load = function(instance, id, callback){ - callback.apply(instance); - return instance; - }; - var instance = null; - var recipe2 = Recipe.load("ID", function(){ - instance = this; - }); - assertTrue(recipe2 === instance); -}; - -DataStoreTest.prototype.testSaveScope = function(){ - var ds = new DataStore(); - var log = ""; - var Person = ds.entity("Person"); - var person1 = Person({name:"A", $entity:"Person", $id:"1", $version:"1"}, ds); - person1.$$anchor = "A"; - var person2 = Person({name:"B", $entity:"Person", $id:"2", $version:"2"}, ds); - person2.$$anchor = "B"; - var anchor = {}; - ds.anchor = anchor; - ds._jsonRequest = function(request, callback){ - log += "save(" + request[2].$id + ");"; - callback({$id:request[2].$id}); - }; - ds.saveScope({person1:person1, person2:person2, - ignoreMe:{name: "ignore", save:function(callback){callback();}}}, function(){ - log += "done();"; - }); - assertEquals("save(1);save(2);done();", log); - assertEquals(1, anchor.A); - assertEquals(2, anchor.B); -}; - -DataStoreTest.prototype.testEntityLoadAllRows = function(){ - var ds = new DataStore(); - var Recipe = ds.entity("Recipe"); - var list = []; - ds.loadAll = function(entity, callback){ - assertTrue(Recipe === entity); - callback.apply(list); - return list; - }; - var items = Recipe.all(function(){ - assertTrue(list === this); - }); - assertTrue(items === list); -}; - -DataStoreTest.prototype.testLoadAll = function(){ - expectAsserts(8); - var post = function(data, callback){ - assertEquals("GET", data[0][0]); - assertEquals("A", data[0][1]); - callback(200, [[{$entity:'A', $id:'1'},{$entity:'A', $id:'2'}]]); - }; - var datastore = new DataStore(post); - var list = datastore.entity("A").all(function(){ - assertTrue(true); - }); - datastore.flush(); - assertEquals(list.length, 2); - assertEquals(list[0].$entity, "A"); - assertEquals(list[0].$id, "1"); - assertEquals(list[1].$entity, "A"); - assertEquals(list[1].$id, "2"); -}; - -DataStoreTest.prototype.testQuery = function(){ - expectAsserts(5); - var post = function(data, callback) { - assertEquals("GET", data[0][0]); - assertEquals("Employee/managerId=123abc", data[0][1]); - callback(200, [[{$entity:"Employee", $id: "456", managerId: "123ABC"}]]); - - }; - var datastore = new DataStore(post); - var Employee = datastore.entity("Employee"); - var list = Employee.query('managerId', "123abc", function(){ - assertTrue(true); - }); - datastore.flush(); - assertJsonEquals([[{$entity:"Employee", $id: "456", managerId: "123ABC"}]], datastore._cache.$collections); - assertEquals(list[0].$id, "456"); -}; - -DataStoreTest.prototype.testLoadingDocumentRefreshesExistingArrays = function() { - expectAsserts(12); - var post; - var datastore = new DataStore(function(r, c){post(r,c);}); - var Book = datastore.entity('Book'); - post = function(req, callback) { - callback(200, [[{$id:1, $entity:"Book", name:"Moby"}, - {$id:2, $entity:"Book", name:"Dick"}]]); - }; - var allBooks = Book.all(); - datastore.flush(); - var queryBooks = Book.query("a", "b"); - datastore.flush(); - assertEquals("Moby", allBooks[0].name); - assertEquals("Dick", allBooks[1].name); - assertEquals("Moby", queryBooks[0].name); - assertEquals("Dick", queryBooks[1].name); - - post = function(req, callback) { - assertEquals('[["GET","Book/1"]]', toJson(req)); - callback(200, [{$id:1, $entity:"Book", name:"Moby Dick"}]); - }; - var book = Book.load(1); - datastore.flush(); - assertEquals("Moby Dick", book.name); - assertEquals("Moby Dick", allBooks[0].name); - assertEquals("Moby Dick", queryBooks[0].name); - - post = function(req, callback) { - assertEquals('POST', req[0][0]); - callback(200, [{$id:1, $entity:"Book", name:"The Big Fish"}]); - }; - book.$save(); - datastore.flush(); - assertEquals("The Big Fish", book.name); - assertEquals("The Big Fish", allBooks[0].name); - assertEquals("The Big Fish", queryBooks[0].name); -}; - -DataStoreTest.prototype.testEntityProperties = function() { - expectAsserts(2); - var datastore = new DataStore(); - var callback = {}; - - datastore._jsonRequest = function(request, callbackFn) { - assertJsonEquals(["GET", "Cheese/$properties"], request); - assertEquals(callback, callbackFn); - }; - - var Cheese = datastore.entity("Cheese"); - Cheese.properties(callback); - -}; - -DataStoreTest.prototype.testLoadInstanceIsNotFromCache = function() { - var post; - var datastore = new DataStore(function(r, c){post(r,c);}); - var Book = datastore.entity('Book'); - - post = function(req, callback) { - assertEquals('[["GET","Book/1"]]', toJson(req)); - callback(200, [{$id:1, $entity:"Book", name:"Moby Dick"}]); - }; - var book = Book.load(1); - datastore.flush(); - assertEquals("Moby Dick", book.name); - assertFalse(book === datastore._cache['Book/1']); -}; - -DataStoreTest.prototype.testLoadStarsIsNewDocument = function() { - var datastore = new DataStore(); - var Book = datastore.entity('Book'); - var book = Book.load('*'); - assertEquals('Book', book.$entity); -}; - -DataStoreTest.prototype.testUndefinedEntityReturnsNullValueObject = function() { - var datastore = new DataStore(); - var Entity = datastore.entity(undefined); - var all = Entity.all(); - assertEquals(0, all.length); -}; - -DataStoreTest.prototype.testFetchEntities = function(){ - expectAsserts(6); - var post = function(data, callback){ - assertJsonEquals(["GET", "$entities"], data[0]); - callback(200, [{A:0, B:0}]); - }; - var datastore = new DataStore(post); - var entities = datastore.entities(function(){ - assertTrue(true); - }); - datastore.flush(); - assertJsonEquals([], datastore.bulkRequest); - assertEquals(2, entities.length); - assertEquals("A", entities[0].title); - assertEquals("B", entities[1].title); -}; - -DataStoreTest.prototype.testItShouldMigrateSchema = function() { - var datastore = new DataStore(); - var Entity = datastore.entity("Entity", {a:[], user:{name:"Misko", email:""}}); - var doc = Entity().$loadFrom({b:'abc', user:{email:"misko@hevery.com"}}); - assertFalse( - toJson({a:[], b:'abc', user:{name:"Misko", email:"misko@hevery.com"}}) == - toJson(doc)); - doc.$migrate(); - assertEquals( - toJson({a:[], b:'abc', user:{name:"Misko", email:"misko@hevery.com"}}), - toJson(doc)); -}; - -DataStoreTest.prototype.testItShouldCollectRequestsForBulk = function() { - var ds = new DataStore(); - var Book = ds.entity("Book"); - var Library = ds.entity("Library"); - Book.all(); - Library.load("123"); - assertEquals(2, ds.bulkRequest.length); - assertJsonEquals(["GET", "Book"], ds.bulkRequest[0]); - assertJsonEquals(["GET", "Library/123"], ds.bulkRequest[1]); -}; - -DataStoreTest.prototype.testEmptyFlushShouldDoNothing = function () { - var ds = new DataStore(function(){ - fail("expecting noop"); - }); - ds.flush(); -}; - -DataStoreTest.prototype.testFlushShouldCallAllCallbacks = function() { - var log = ""; - function post(request, callback){ - log += 'BulkRequest:' + toJson(request) + ';'; - callback(200, [[{$id:'ABC'}], {$id:'XYZ'}]); - } - var ds = new DataStore(post); - var Book = ds.entity("Book"); - var Library = ds.entity("Library"); - Book.all(function(instance){ - log += toJson(instance) + ';'; - }); - Library.load("123", function(instance){ - log += toJson(instance) + ';'; - }); - assertEquals("", log); - ds.flush(); - assertJsonEquals([], ds.bulkRequest); - assertEquals('BulkRequest:[["GET","Book"],["GET","Library/123"]];[{"$id":"ABC"}];{"$id":"XYZ"};', log); -}; - -DataStoreTest.prototype.testSaveOnNotLoggedInRetriesAfterLoggin = function(){ - var log = ""; - var book; - var ds = new DataStore(null, {login:function(c){c();}}); - ds.post = function (request, callback){ - assertJsonEquals([["POST", "", book]], request); - ds.post = function(request, callback){ - assertJsonEquals([["POST", "", book]], request); - ds.post = function(){fail("too much recursion");}; - callback(200, [{saved:"ok"}]); - }; - callback(200, {$status_code:401}); - }; - book = ds.entity("Book")({name:"misko"}); - book.$save(); - ds.flush(); - assertJsonEquals({saved:"ok"}, book); -}; - -DataStoreTest.prototype.testItShouldRemoveItemFromCollectionWhenDeleted = function() { - expectAsserts(6); - var ds = new DataStore(); - ds.post = function(request, callback){ - assertJsonEquals([["GET", "Book"]], request); - callback(200, [[{name:"Moby Dick", $id:123, $entity:'Book'}]]); - }; - var Book = ds.entity("Book"); - var books = Book.all(); - ds.flush(); - assertJsonEquals([[{name:"Moby Dick", $id:123, $entity:'Book'}]], ds._cache.$collections); - assertDefined(ds._cache['Book/123']); - var book = Book({$id:123}); - ds.post = function(request, callback){ - assertJsonEquals([["DELETE", "", book]], request); - callback(200, [book]); - }; - ds.remove(book); - ds.flush(); - assertUndefined(ds._cache['Book/123']); - assertJsonEquals([[]],ds._cache.$collections); -}; - -DataStoreTest.prototype.testItShouldAddToAll = function() { - expectAsserts(8); - var ds = new DataStore(); - ds.post = function(request, callback){ - assertJsonEquals([["GET", "Book"]], request); - callback(200, [[]]); - }; - var Book = ds.entity("Book"); - var books = Book.all(); - assertEquals(0, books.length); - ds.flush(); - var moby = Book({name:'moby'}); - moby.$save(); - ds.post = function(request, callback){ - assertJsonEquals([["POST", "", moby]], request); - moby.$id = '123'; - callback(200, [moby]); - }; - ds.flush(); - assertEquals(1, books.length); - assertEquals(moby, books[0]); - - moby.$save(); - ds.flush(); - assertEquals(1, books.length); - assertEquals(moby, books[0]); -}; - -DataStoreTest.prototype.testItShouldReturnCreatedDocumentCountByUser = function(){ - expectAsserts(2); - var datastore = new DataStore( - function(request, callback){ - assertJsonEquals([["GET", "$users"]], request); - callback(200, [{misko:1, adam:1}]); - }); - var users = datastore.documentCountsByUser(); - assertJsonEquals({misko:1, adam:1}, users); -}; - - -DataStoreTest.prototype.testItShouldReturnDocumentIdsForUeserByEntity = function(){ - expectAsserts(2); - var datastore = new DataStore( - function(request, callback){ - assertJsonEquals([["GET", "$users/misko@hevery.com"]], request); - callback(200, [{Book:["1"], Library:["2"]}]); - }); - var users = datastore.userDocumentIdsByEntity("misko@hevery.com"); - assertJsonEquals({Book:["1"], Library:["2"]}, users); -}; - -DataStoreTest.prototype.testItShouldReturnNewInstanceOn404 = function(){ - expectAsserts(7); - var log = ""; - var datastore = new DataStore( - function(request, callback){ - assertJsonEquals([["GET", "User/misko"]], request); - callback(200, [{$status_code:404}]); - }); - var User = datastore.entity("User", {admin:false}); - var user = User.loadOrCreate('misko', function(i){log+="cb "+i.$id+";";}); - datastore.flush(); - assertEquals("misko", user.$id); - assertEquals("User", user.$entity); - assertEquals(false, user.admin); - assertEquals("undefined", typeof user.$secret); - assertEquals("undefined", typeof user.$version); - assertEquals("cb misko;", log); -}; - -DataStoreTest.prototype.testItShouldReturnNewInstanceOn404 = function(){ - var log = ""; - var datastore = new DataStore( - function(request, callback){ - assertJsonEquals([["GET", "User/misko"],["GET", "User/adam"]], request); - callback(200, [{$id:'misko'},{$id:'adam'}]); - }); - var User = datastore.entity("User"); - var users = User.loadMany(['misko', 'adam'], function(i){log+="cb "+toJson(i)+";";}); - datastore.flush(); - assertEquals("misko", users[0].$id); - assertEquals("adam", users[1].$id); - assertEquals('cb [{"$id":"misko"},{"$id":"adam"}];', log); -}; - -DataStoreTest.prototype.testItShouldCreateJoinAndQuery = function() { - var datastore = new DataStore(); - var Invoice = datastore.entity("Invoice"); - var Customer = datastore.entity("Customer"); - var InvoiceWithCustomer = datastore.join({ - invoice:{join:Invoice}, - customer:{join:Customer, on:"invoice.customer"} - }); - var invoiceWithCustomer = InvoiceWithCustomer.query("invoice.month", 1); - assertEquals([], invoiceWithCustomer); - assertJsonEquals([["GET", "Invoice/month=1"]], datastore.bulkRequest); - var request = datastore.bulkRequest.shift(); - request.$$callback([{$id:1, customer:1},{$id:2, customer:1},{$id:3, customer:3}]); - assertJsonEquals([["GET","Customer/1"],["GET","Customer/3"]], datastore.bulkRequest); - datastore.bulkRequest.shift().$$callback({$id:1}); - datastore.bulkRequest.shift().$$callback({$id:3}); - assertJsonEquals([ - {invoice:{$id:1,customer:1},customer:{$id:1}}, - {invoice:{$id:2,customer:1},customer:{$id:1}}, - {invoice:{$id:3,customer:3},customer:{$id:3}}], invoiceWithCustomer); -}; - -DataStoreTest.prototype.testItShouldThrowIfMoreThanOneEntityIsPrimary = function() { - var datastore = new DataStore(); - var Invoice = datastore.entity("Invoice"); - var Customer = datastore.entity("Customer"); - assertThrows("Exactly one entity needs to be primary.", function(){ - datastore.join({ - invoice:{join:Invoice}, - customer:{join:Customer} - }); - }); -}; - -DataStoreTest.prototype.testItShouldThrowIfLoopInReferences = function() { - var datastore = new DataStore(); - var Invoice = datastore.entity("Invoice"); - var Customer = datastore.entity("Customer"); - assertThrows("Infinite loop in join: invoice -> customer", function(){ - datastore.join({ - invoice:{join:Invoice, on:"customer.invoice"}, - customer:{join:Customer, on:"invoice.customer"} - }); - }); -}; - -DataStoreTest.prototype.testItShouldThrowIfReferenceToNonExistantJoin = function() { - var datastore = new DataStore(); - var Invoice = datastore.entity("Invoice"); - var Customer = datastore.entity("Customer"); - assertThrows("Named entity 'x' is undefined.", function(){ - datastore.join({ - invoice:{join:Invoice, on:"x.invoice"}, - customer:{join:Customer, on:"invoice.customer"} - }); - }); -}; - -DataStoreTest.prototype.testItShouldThrowIfQueryOnNonPrimary = function() { - var datastore = new DataStore(); - var Invoice = datastore.entity("Invoice"); - var Customer = datastore.entity("Customer"); - var InvoiceWithCustomer = datastore.join({ - invoice:{join:Invoice}, - customer:{join:Customer, on:"invoice.customer"} - }); - assertThrows("Named entity 'customer' is not a primary entity.", function(){ - InvoiceWithCustomer.query("customer.month", 1); - }); -}; diff --git a/test/moveToAngularCom/EntityDeclarationTest.js b/test/moveToAngularCom/EntityDeclarationTest.js deleted file mode 100644 index 28986ea8..00000000 --- a/test/moveToAngularCom/EntityDeclarationTest.js +++ /dev/null @@ -1,50 +0,0 @@ -EntityDeclarationTest = TestCase('EntityDeclarationTest'); - -EntityDeclarationTest.prototype.testEntityTypeOnly = function(){ - expectAsserts(2); - var datastore = {entity:function(name){ - assertEquals("Person", name); - }}; - var scope = new Scope(); - var init = scope.entity("Person", datastore); - assertEquals("", init); -}; - -EntityDeclarationTest.prototype.testWithDefaults = function(){ - expectAsserts(4); - var datastore = {entity:function(name, init){ - assertEquals("Person", name); - assertEquals("=a:", init.a); - assertEquals(0, init.b.length); - }}; - var scope = new Scope(); - var init = scope.entity('Person:{a:"=a:", b:[]}', datastore); - assertEquals("", init); -}; - -EntityDeclarationTest.prototype.testWithName = function(){ - expectAsserts(2); - var datastore = {entity:function(name, init){ - assertEquals("Person", name); - return function (){ return {}; }; - }}; - var scope = new Scope(); - var init = scope.entity('friend=Person', datastore); - assertEquals("$anchor.friend:{friend=Person.load($anchor.friend);friend.$$anchor=\"friend\";};", init); -}; - -EntityDeclarationTest.prototype.testMultipleEntities = function(){ - expectAsserts(3); - var expect = ['Person', 'Book']; - var i=0; - var datastore = {entity:function(name, init){ - assertEquals(expect[i], name); - i++; - return function (){ return {}; }; - }}; - var scope = new Scope(); - var init = scope.entity('friend=Person;book=Book;', datastore); - assertEquals("$anchor.friend:{friend=Person.load($anchor.friend);friend.$$anchor=\"friend\";};" + - "$anchor.book:{book=Book.load($anchor.book);book.$$anchor=\"book\";};", - init); -}; diff --git a/test/moveToAngularCom/FileControllerTest.js b/test/moveToAngularCom/FileControllerTest.js deleted file mode 100644 index 75c924e6..00000000 --- a/test/moveToAngularCom/FileControllerTest.js +++ /dev/null @@ -1,98 +0,0 @@ -FileControllerTest = TestCase('FileControllerTest'); - -FileControllerTest.prototype.XtestOnSelectUpdateView = function(){ - var view = jQuery('<span><a/><span/></span>'); - var swf = {}; - var controller = new FileController(view, null, swf); - swf.uploadFile = function(path){}; - controller.select('A', 9, '9 bytes'); - assertEquals(view.find('a').text(), "A"); - assertEquals(view.find('span').text(), "9 bytes"); -}; - -FileControllerTest.prototype.XtestUpdateModelView = function(){ - var view = FileController.template(''); - var input = $('<input name="value.input">'); - var controller; - var scope = new Scope({value:{}, $binder:{updateView:function(){ - controller.updateView(scope); - }}}); - view.data('scope', scope); - controller = new FileController(view, 'value.input', null, "http://server_base"); - var value = '{"text":"A", "size":123, "id":"890"}'; - controller.uploadCompleteData(value); - controller.updateView(scope); - assertEquals(scope.get('value.input.text'), 'A'); - assertEquals(scope.get('value.input.size'), 123); - assertEquals(scope.get('value.input.id'), '890'); - assertEquals(scope.get('value.input.url'), 'http://server_base/_attachments/890/A'); - assertEquals(view.find('a').text(), "A"); - assertEquals(view.find('a').attr('href'), "http://server_base/_attachments/890/A"); - assertEquals(view.find('span').text(), "123 bytes"); -}; - -FileControllerTest.prototype.XtestFileUpload = function(){ - expectAsserts(1); - var swf = {}; - var controller = new FileController(null, null, swf, "http://server_base"); - swf.uploadFile = function(path){ - assertEquals("http://server_base/_attachments", path); - }; - controller.name = "Name"; - controller.upload(); -}; - -FileControllerTest.prototype.XtestFileUploadNoFileIsNoop = function(){ - expectAsserts(0); - var swf = {uploadFile:function(path){ - fail(); - }}; - var controller = new FileController(null, swf); - controller.upload("basePath", null); -}; - -FileControllerTest.prototype.XtestRemoveAttachment = function(){ - var doc = FileController.template(); - var input = $('<input name="file">'); - var scope = new Scope(); - input.data('scope', scope); - var controller = new FileController(doc, 'file', null, null); - controller.updateView(scope); - assertEquals(false, doc.find('input').attr('checked')); - - scope.set('file', {url:'url', size:123}); - controller.updateView(scope); - assertEquals(true, doc.find('input').attr('checked')); - - doc.find('input').attr('checked', false); - controller.updateModel(scope); - assertNull(scope.get('file')); - - doc.find('input').attr('checked', true); - controller.updateModel(scope); - assertEquals('url', scope.get('file.url')); - assertEquals(123, scope.get('file.size')); -}; - -FileControllerTest.prototype.XtestShouldEmptyOutOnUndefined = function () { - var view = FileController.template('hello'); - var controller = new FileController(view, 'abc', null, null); - - var scope = new Scope(); - scope.set('abc', {text: 'myname', url: 'myurl', size: 1234}); - - controller.updateView(scope); - assertEquals("myurl", view.find('a').attr('href')); - assertEquals("myname", view.find('a').text()); - assertEquals(true, view.find('input').is(':checked')); - assertEquals("1.2 KB", view.find('span').text()); - - scope.set('abc', undefined); - controller.updateView(scope); - assertEquals("myurl", view.find('a').attr('href')); - assertEquals("myname", view.find('a').text()); - assertEquals(false, view.find('input').is(':checked')); - assertEquals("1.2 KB", view.find('span').text()); -}; - - diff --git a/test/moveToAngularCom/MiscTest.js b/test/moveToAngularCom/MiscTest.js deleted file mode 100644 index aa0e1186..00000000 --- a/test/moveToAngularCom/MiscTest.js +++ /dev/null @@ -1,35 +0,0 @@ -BinderTest.prototype.testExpandEntityTagWithName = function(){ - var c = this.compile('<div ng-entity="friend=Person"/>'); - assertEquals( - '<div ng-entity="friend=Person" ng-watch="$anchor.friend:{friend=Person.load($anchor.friend);friend.$$anchor=\"friend\";};"></div>', - sortedHtml(c.node)); - assertEquals("Person", c.scope.$get("friend.$entity")); - assertEquals("friend", c.scope.$get("friend.$$anchor")); -}; - -BinderTest.prototype.testExpandSubmitButtonToAction = function(){ - var html = this.compileToHtml('<input type="submit" value="Save">'); - assertTrue(html, html.indexOf('ng-action="$save()"') > 0 ); - assertTrue(html, html.indexOf('ng-bind-attr="{"disabled":"{{$invalidWidgets}}"}"') > 0 ); -}; - -BinderTest.prototype.testReplaceFileUploadWithSwf = function(){ - expectAsserts(1); - var form = jQuery("body").append('<div id="testTag"><input type="file"></div>'); - form.data('scope', new Scope()); - var factory = {}; - var binder = new Binder(form.get(0), factory, new MockLocation()); - factory.createController = function(node){ - assertEquals(node.attr('type'), 'file'); - return {updateModel:function(){}}; - }; - binder.compile(); - jQuery("#testTag").remove(); -}; - -BinderTest.prototype.testExpandEntityTagWithDefaults = function(){ - assertEquals( - '<div ng-entity="Person:{a:\"a\"}" ng-watch=""></div>', - this.compileToHtml('<div ng-entity=\'Person:{a:"a"}\'/>')); -}; - diff --git a/test/moveToAngularCom/ModelTest.js b/test/moveToAngularCom/ModelTest.js deleted file mode 100644 index dbd97778..00000000 --- a/test/moveToAngularCom/ModelTest.js +++ /dev/null @@ -1,84 +0,0 @@ -ModelTest = TestCase('ModelTest'); - -ModelTest.prototype.testLoadSaveOperations = function(){ - var m1 = new DataStore().entity('A')(); - m1.a = 1; - - var m2 = {b:1}; - - m1.$loadFrom(m2); - - assertTrue(!m1.a); - assertEquals(m1.b, 1); -}; - -ModelTest.prototype.testLoadfromDoesNotClobberFunctions = function(){ - var m1 = new DataStore().entity('A')(); - m1.id = function(){return 'OK';}; - m1.$loadFrom({id:null}); - assertEquals(m1.id(), 'OK'); - - m1.b = 'OK'; - m1.$loadFrom({b:function(){}}); - assertEquals(m1.b, 'OK'); -}; - -ModelTest.prototype.testDataStoreDoesNotGetClobbered = function(){ - var ds = new DataStore(); - var m = ds.entity('A')(); - assertTrue(m.$$entity.datastore === ds); - m.$loadFrom({}); - assertTrue(m.$$entity.datastore === ds); -}; - -ModelTest.prototype.testManagedModelDelegatesMethodsToDataStore = function(){ - expectAsserts(7); - var datastore = new DataStore(); - var model = datastore.entity("A", {a:1})(); - var fn = {}; - datastore.save = function(instance, callback) { - assertTrue(model === instance); - assertTrue(callback === fn); - }; - datastore.remove = function(instance, callback) { - assertTrue(model === instance); - assertTrue(callback === fn); - }; - datastore.load = function(instance, id, callback) { - assertTrue(model === instance); - assertTrue(id === "123"); - assertTrue(callback === fn); - }; - model.$save(fn); - model.$delete(fn); - model.$loadById("123", fn); -}; - -ModelTest.prototype.testManagedModelCanBeForcedToFlush = function(){ - expectAsserts(6); - var datastore = new DataStore(); - var model = datastore.entity("A", {a:1})(); - - datastore.save = function(instance, callback) { - assertTrue(model === instance); - assertTrue(callback === undefined); - }; - datastore.remove = function(instance, callback) { - assertTrue(model === instance); - assertTrue(callback === undefined); - }; - datastore.flush = function(){ - assertTrue(true); - }; - model.$save(true); - model.$delete(true); -}; - - -ModelTest.prototype.testItShouldMakeDeepCopyOfInitialValues = function (){ - var initial = {a:[]}; - var entity = new DataStore().entity("A", initial); - var model = entity(); - model.a.push(1); - assertEquals(0, entity().a.length); -}; diff --git a/test/moveToAngularCom/ServerTest.js b/test/moveToAngularCom/ServerTest.js deleted file mode 100644 index 02fab84c..00000000 --- a/test/moveToAngularCom/ServerTest.js +++ /dev/null @@ -1,42 +0,0 @@ -ServerTest = TestCase("ServerTest"); -ServerTest.prototype.testBreakLargeRequestIntoPackets = function() { - var log = ""; - var server = new Server("http://server", function(url){ - log += "|" + url; - }); - server.maxSize = 30; - server.uuid = "uuid"; - server.request("POST", "/data/database", {}, function(code, r){ - assertEquals(200, code); - assertEquals("response", r); - }); - angularCallbacks.uuid0("response"); - assertEquals( - "|http://server/$/uuid0/2/1?h=eyJtIjoiUE9TVCIsInAiOnt9LCJ1Ij" + - "|http://server/$/uuid0/2/2?h=oiL2RhdGEvZGF0YWJhc2UifQ==", - log); -}; - -ServerTest.prototype.testItShouldEncodeUsingUrlRules = function() { - var server = new Server("http://server"); - assertEquals("fn5-fn5-", server.base64url("~~~~~~")); - assertEquals("fn5_fn5_", server.base64url("~~\u007f~~\u007f")); -}; - -FrameServerTest = TestCase("FrameServerTest"); - -FrameServerTest.prototype = { - testRead:function(){ - var window = {name:'$DATASET:"MyData"'}; - var server = new FrameServer(window); - server.read(); - assertEquals("MyData", server.data); - }, - testWrite:function(){ - var window = {}; - var server = new FrameServer(window); - server.data = "TestData"; - server.write(); - assertEquals('$DATASET:"TestData"', window.name); - } -}; diff --git a/test/moveToAngularCom/UsersTest.js b/test/moveToAngularCom/UsersTest.js deleted file mode 100644 index f0ff545a..00000000 --- a/test/moveToAngularCom/UsersTest.js +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (C) 2008,2009 BRAT Tech LLC - -UsersTest = TestCase("UsersTest"); - -UsersTest.prototype = { - setUp:function(){}, - - tearDown:function(){}, - - testItShouldFetchCurrentUser:function(){ - expectAsserts(5); - var user; - var users = new Users({request:function(method, url, request, callback){ - assertEquals("GET", method); - assertEquals("/account.json", url); - assertEquals("{}", toJson(request)); - callback(200, {$status_code:200, user:{name:'misko'}}); - }}); - users.fetchCurrentUser(function(u){ - user = u; - assertEquals("misko", u.name); - assertEquals("misko", users.current.name); - }); - } - -}; diff --git a/test/scenario/DSLSpec.js b/test/scenario/DSLSpec.js index 5aac9752..f8606641 100644 --- a/test/scenario/DSLSpec.js +++ b/test/scenario/DSLSpec.js @@ -1,39 +1,39 @@ describe("DSL", function() { - var lastStep, executeStep, lastDocument; + var lastDocument, executeFuture, Expect; beforeEach(function() { - lastStep = null; - $scenario = { - addStep: function(name, stepFunction) { - lastStep = { name:name, fn: stepFunction}; - } - }; - executeStep = function(step, html, callback) { - lastDocument =_jQuery('<div>' + html + '</div>'); + setUpContext(); + executeFuture = function(future, html, callback) { + lastDocument = _jQuery('<div>' + html + '</div>'); + lastFrame = _jQuery('<iframe>' + lastDocument + '</iframe>'); _jQuery(document.body).append(lastDocument); var specThis = { testWindow: window, - testDocument: lastDocument + testDocument: lastDocument, + testFrame: lastFrame, + jQuery: _jQuery }; - step.fn.call(specThis, callback || noop); + future.behavior.call(specThis, callback || noop); }; + Expect = _window.expect; }); describe("input", function() { var input = angular.scenario.dsl.input; + it('should enter', function() { - input('name').enter('John'); - expect(lastStep.name).toEqual("Set input text of 'name' to 'John'"); - executeStep(lastStep, '<input type="text" name="name" />'); + var future = input('name').enter('John'); + expect(future.name).toEqual("input 'name' enter 'John'"); + executeFuture(future, '<input type="text" name="name" />'); expect(lastDocument.find('input').val()).toEqual('John'); }); it('should select', function() { - input('gender').select('female'); - expect(lastStep.name).toEqual("Select radio 'gender' to 'female'"); - executeStep(lastStep, + var future = input('gender').select('female'); + expect(future.name).toEqual("input 'gender' select 'female'"); + executeFuture(future, '<input type="radio" name="0@gender" value="male" checked/>' + '<input type="radio" name="0@gender" value="female"/>'); expect(lastDocument.find(':radio:checked').length).toEqual(1); @@ -41,15 +41,145 @@ describe("DSL", function() { }); }); - describe('expect', function() { - var dslExpect = angular.scenario.dsl.expect; - describe('repeater', function() { - it('should check the count of repeated elements', function() { - dslExpect.repeater('.repeater-row').count.toEqual(2); - expect(lastStep.name).toEqual("Expect that there are 2 items in Repeater with selector '.repeater-row'"); - var html = "<div class='repeater-row'>a</div><div class='repeater-row'>b</div>"; - executeStep(lastStep, html); + describe('browser', function() { + var browser = angular.scenario.dsl.browser; + it('shoud return true if location with empty hash provided is same ' + + 'as location of the page', function() { + browser.location.href = "http://server"; + expect(browser.location.toEqual("http://server")).toEqual(true); + }); + it('shoud return true if location with hash provided is same ' + + 'as location of the page', function() { + browser.location.href = "http://server"; + browser.location.hash = "hashPath"; + expect(browser.location.toEqual("http://server/#/hashPath")) + .toEqual(true); + }); + it('should return true if the location provided is the same as which ' + + 'browser navigated to', function() { + var future = browser.navigateTo("http://server/#/hashPath"); + expect(future.name).toEqual("Navigate to: http://server/#/hashPath"); + executeFuture(future, '<input type="text" name="name" />'); + expect(browser.location.toEqual("http://server/#/hashPath")) + .toEqual(true); + expect(browser.location.toEqual("http://server/")) + .toEqual(false); + + future = browser.navigateTo("http://server/"); + expect(future.name).toEqual("Navigate to: http://server/"); + executeFuture(future, '<input type="text" name="name" />'); + expect(browser.location.toEqual("http://server/")) + .toEqual(true); + }); + }); + + describe('repeater', function() { + + var repeater = angular.scenario.dsl.repeater; + var html; + beforeEach(function() { + html = "<table>" + + "<tr class='epic'>" + + "<td class='hero-name'>" + + "<span ng:bind='hero'>John Marston</span>" + + "</td>" + + "<td class='game-name'>" + + "<span ng:bind='game'>Red Dead Redemption</span>" + + "</td>" + + "</tr>" + + "<tr class='epic'>" + + "<td class='hero-name'>" + + "<span ng:bind='hero'>Nathan Drake</span>" + + "</td>" + + "<td class='game-name'>" + + "<span ng:bind='game'>Uncharted</span>" + + "</td>" + + "</tr>" + + "</table>"; + }); + it('should count', function() { + var future = repeater('.repeater-row').count(); + expect(future.name).toEqual("repeater '.repeater-row' count"); + executeFuture(future, + "<div class='repeater-row'>a</div>" + + "<div class='repeater-row'>b</div>", + function(value) { + future.fulfill(value); }); + expect(future.fulfilled).toBeTruthy(); + expect(future.value).toEqual(2); + }); + + function assertFutureState(future, expectedName, expectedValue) { + expect(future.name).toEqual(expectedName); + executeFuture(future, html, function(value) { + future.fulfill(value); + }); + expect(future.fulfilled).toBeTruthy(); + expect(future.value).toEqual(expectedValue); + } + it('should collect bindings', function() { + assertFutureState(repeater('.epic').collect('{{hero}}'), + "repeater '.epic' collect '{{hero}}'", + ['John Marston', 'Nathan Drake']); + assertFutureState(repeater('.epic').collect('{{game}}'), + "repeater '.epic' collect '{{game}}'", + ['Red Dead Redemption', 'Uncharted']); + }); + it('should collect normal selectors', function() { + assertFutureState(repeater('.epic').collect('.hero-name'), + "repeater '.epic' collect '.hero-name'", + ['John Marston', 'Nathan Drake']); + assertFutureState(repeater('.epic').collect('.game-name'), + "repeater '.epic' collect '.game-name'", + ['Red Dead Redemption', 'Uncharted']); + }); + it('should collect normal attributes', function() { + //TODO(shyamseshadri) : Left as an exercise to the user + }); + }); + + describe('element', function() { + var element = angular.scenario.dsl.element; + var html; + beforeEach(function() { + html = '<div class="container">' + + '<div class="reports-detail">' + + '<span class="desc">Description : ' + + '<span ng:bind="report.description">Details...</span>' + + '</span>' + + '<span>Date created: ' + + '<span ng:bind="report.creationDate">01/01/01</span>' + + '</span>' + + '</div>' + + '</div>'; + }); + function timeTravel(future) { + executeFuture(future, html, function(value) { future.fulfill(value); }); + expect(future.fulfilled).toBeTruthy(); + } + it('should find elements on the page and provide jquery api', function() { + var future = element('.reports-detail').text(); + expect(future.name).toEqual("Element '.reports-detail'.text()"); + timeTravel(future); + expect(future.value). + toEqual('Description : Details...Date created: 01/01/01'); +// expect(future.value.find('.desc').text()). +// toEqual('Description : Details...'); + }); + it('should find elements with angular syntax', function() { + var future = element('{{report.description}}').text(); + expect(future.name).toEqual("Element '{{report.description}}'.text()"); + timeTravel(future); + expect(future.value).toEqual('Details...'); +// expect(future.value.attr('ng:bind')).toEqual('report.description'); + }); + it('should be able to click elements', function(){ + var future = element('.link-class').click(); + expect(future.name).toEqual("Element '.link-class'.click()"); + executeFuture(future, html, function(value) { future.fulfill(value); }); + expect(future.fulfilled).toBeTruthy(); + // TODO(rajat): look for some side effect from click happening? }); }); }); diff --git a/test/scenario/MatcherSpec.js b/test/scenario/MatcherSpec.js new file mode 100644 index 00000000..2eddd2bc --- /dev/null +++ b/test/scenario/MatcherSpec.js @@ -0,0 +1,38 @@ +describe('Matcher', function () { + function executeFutures() { + for(var i in $scenario.currentSpec.futures) { + var future = $scenario.currentSpec.futures[i]; + future.behavior.call({}, function(value) { future.fulfill(value); }); + } + } + var matcher; + beforeEach(function() { + setUpContext(); + var future = $scenario.addFuture('Calculate first future', function(done) { + done(123); + }); + matcher = new Matcher(this, future); + + }); + it('should correctly match toEqual', function() { + matcher.toEqual(123); + executeFutures(); + }); + it('should throw an error when incorrect match toEqual', function() { + matcher.toEqual(456); + try { + executeFutures(); + fail(); + } catch (e) { + expect(e).toEqual('Expected 456 but was 123'); + } + }); + it('should correctly match arrays', function() { + var future = $scenario.addFuture('Calculate first future', function(done) { + done(['a', 'b']); + }); + matcher = new Matcher(this, future); + matcher.toEqual(['a', 'b']); + executeFutures(); + }); +});
\ No newline at end of file diff --git a/test/scenario/RunnerSpec.js b/test/scenario/RunnerSpec.js index bbdd9e8c..b12c43c6 100644 --- a/test/scenario/RunnerSpec.js +++ b/test/scenario/RunnerSpec.js @@ -1,40 +1,30 @@ -describe('Runner', function(){ - var scenario, runner, log, Describe, It, $scenario, body; - - function logger(text) { - return function(done){ - log += text; - (done||noop)(); - }; - } - - beforeEach(function(){ - log = ''; - scenario = {}; +describe('Runner', function() { + + var Describe, It, BeforeEach, AfterEach, body; + + beforeEach(function() { + setUpContext(); + Describe = _window.describe; + It = _window.it; + BeforeEach = _window.beforeEach; + AfterEach = _window.afterEach; body = _jQuery('<div></div>'); - runner = new angular.scenario.Runner(scenario, _jQuery); - Describe = scenario.describe; - BeforeEach = scenario.beforeEach; - AfterEach = scenario.afterEach; - It = scenario.it; - $scenario = scenario.$scenario; }); - describe('describe', function(){ - it('should consume the describe functions', function(){ + describe('describe', function() { + it('should consume the describe functions', function() { Describe('describe name', logger('body')); - expect(log).toEqual('body'); }); - describe('it', function(){ - it('should consume it', function(){ - Describe('describe name', function(){ + describe('it', function() { + it('should consume it', function() { + Describe('describe name', function() { It('should text', logger('body')); }); expect(log).toEqual('body'); var spec = $scenario.specs['describe name: it should text']; - expect(spec.steps).toEqual([]); + expect(spec.futures).toEqual([]); expect(spec.name).toEqual('describe name: it should text'); }); @@ -42,18 +32,18 @@ describe('Runner', function(){ // WRITE ME!!!! }); - it('should create a failing step if there is a javascript error', function(){ + it('should create a failing future if there is a javascript error', function() { var spec; - Describe('D1', function(){ - It('I1', function(){ + Describe('D1', function() { + It('I1', function() { spec = $scenario.currentSpec; throw {message: 'blah'}; }); }); - var step = spec.steps[0]; - expect(step.name).toEqual('blah'); + var future = spec.futures[0]; + expect(future.name).toEqual('blah'); try { - step.fn(); + future.behavior(); fail(); } catch (e) { expect(e.message).toEqual('blah'); @@ -63,7 +53,7 @@ describe('Runner', function(){ describe('beforeEach', function() { it('should execute beforeEach before every it', function() { - Describe('describe name', function(){ + Describe('describe name', function() { BeforeEach(logger('before;')); It('should text', logger('body;')); It('should text2', logger('body2;')); @@ -73,7 +63,7 @@ describe('Runner', function(){ }); describe('afterEach', function() { it('should execute afterEach after every it', function() { - Describe('describe name', function(){ + Describe('describe name', function() { AfterEach(logger('after;')); It('should text1', logger('body1;')); It('should text2', logger('body2;')); @@ -82,7 +72,7 @@ describe('Runner', function(){ }); it('should always execute afterEach after every it', function() { - Describe('describe name', function(){ + Describe('describe name', function() { AfterEach(logger('after;')); It('should text', function() { logger('body1;')(); @@ -95,62 +85,63 @@ describe('Runner', function(){ it('should report an error if afterEach fails', function() { var next; - Describe('describe name', function(){ + Describe('describe name', function() { AfterEach(function() { - $scenario.addStep('afterEachLog', logger('after;')); - $scenario.addStep('afterEachThrow', function() { + $scenario.addFuture('afterEachLog', logger('after;')); + $scenario.addFuture('afterEachThrow', function() { throw "AfterError"; }); }); It('should text1', function() { - $scenario.addStep('step1', logger('step1;')); + $scenario.addFuture('future1', logger('future1;')); }); It('should text2', function() { - $scenario.addStep('step2', logger('step2;')); + $scenario.addFuture('future2', logger('future2;')); }); }); $scenario.run(body); - expect(log).toEqual('step1;after;step2;after;'); - expect(scenario.$testrun.results).toEqual([ + expect(log).toEqual('future1;after;future2;after;'); + expect(_window.$testrun.results).toEqual([ { name : 'describe name: it should text1', passed : false, error : 'AfterError', - steps : [ 'step1', 'afterEachLog', 'afterEachThrow' ] }, + steps : [ 'future1', 'afterEachLog', 'afterEachThrow' ] }, { name : 'describe name: it should text2', passed : false, error : 'AfterError', - steps : [ 'step2', 'afterEachLog', 'afterEachThrow' ] }]); + steps : [ 'future2', 'afterEachLog', 'afterEachThrow' ] }]); }); }); }); - describe('steps building', function(){ - it('should queue steps', function(){ - function step(){}; - Describe('name', function(){ - It('should', function(){ - $scenario.addStep('stepname', step); + describe('future building', function() { + it('should queue futures', function() { + function behavior(){}; + Describe('name', function() { + It('should', function() { + $scenario.addFuture('futureName', behavior); }); }); - expect($scenario.specs['name: it should'].steps).toEqual([{name:'stepname', fn:step}]); + expect($scenario.specs['name: it should'].futures[0].name). + toEqual('futureName'); }); }); - describe('execution', function(){ - it('should execute the queued steps', function(){ + describe('execution', function() { + it('should execute the queued futures', function() { var next, firstThis, secondThis, doneThis, spec; $scenario.specs['spec'] = { - steps: [ - {name:'step1', fn: function(done) { - next = done; - log += 'first;'; - firstThis = this; - }}, - {name:'step2', fn:function(done){ - next = done; - log += 'second;'; - secondThis = this; - }} + futures: [ + new Future('future1', function(done) { + next = done; + log += 'first;'; + firstThis = this; + }), + new Future('future2', function(done) { + next = done; + log += 'second;'; + secondThis = this; + }) ] }; @@ -174,18 +165,18 @@ describe('Runner', function(){ expect(spec.result.passed).toEqual(true); }); - it('should handle exceptions in a step', function(){ + it('should handle exceptions in a future', function() { $scenario.specs['spec'] = { - steps: [ - {name: 'first step', fn: function(done) { + futures: [ + new Future('first future', function(done) { done(); - }}, - {name:'error', fn:function(done) { + }), + new Future('error', function(done) { throw "MyError"; - }}, - {name: 'should not execute', fn: function(done) { + }), + new Future('should not execute', function(done) { done(); - }} + }) ] }; @@ -195,30 +186,30 @@ describe('Runner', function(){ expect(spec.result.failed).toEqual(true); expect(spec.result.finished).toEqual(true); expect(spec.result.error).toEqual("MyError"); - expect(scenario.$testrun.results).toEqual([{ + expect(_window.$testrun.results).toEqual([{ name: 'spec', passed: false, error: 'MyError', - steps: ['first step', 'error']}]); + steps: ['first future', 'error']}]); }); }); - describe('run', function(){ + describe('run', function() { var next; beforeEach(function() { - Describe('d1', function(){ - It('it1', function(){ $scenario.addStep('s1', logger('s1,')); }); - It('it2', function(){ - $scenario.addStep('s2', logger('s2,')); - $scenario.addStep('s2.2', function(done){ next = done; }); + Describe('d1', function() { + It('it1', function() { $scenario.addFuture('s1', logger('s1,')); }); + It('it2', function() { + $scenario.addFuture('s2', logger('s2,')); + $scenario.addFuture('s2.2', function(done){ next = done; }); }); }); - Describe('d2', function(){ - It('it3', function(){ $scenario.addStep('s3', logger('s3,')); }); - It('it4', function(){ $scenario.addStep('s4', logger('s4,')); }); + Describe('d2', function() { + It('it3', function() { $scenario.addFuture('s3', logger('s3,')); }); + It('it4', function() { $scenario.addFuture('s4', logger('s4,')); }); }); }); - it('should execute all specs', function(){ + it('should execute all specs', function() { $scenario.run(body); expect(log).toEqual('s1,s2,'); @@ -226,20 +217,20 @@ describe('Runner', function(){ expect(log).toEqual('s1,s2,s3,s4,'); }); it('should publish done state and results as tests are run', function() { - expect(scenario.$testrun.done).toBeFalsy(); - expect(scenario.$testrun.results).toEqual([]); + expect(_window.$testrun.done).toBeFalsy(); + expect(_window.$testrun.results).toEqual([]); $scenario.run(body); - expect(scenario.$testrun.done).toBeFalsy(); - expect(scenario.$testrun.results).toEqual([ - {name: 'd1: it it1', passed: true, steps: ['s1']} + expect(_window.$testrun.done).toBeFalsy(); + expect(_window.$testrun.results).toEqual([ + {name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']} ]); next(); - expect(scenario.$testrun.done).toBeTruthy(); - expect(scenario.$testrun.results).toEqual([ - {name: 'd1: it it1', passed: true, steps: ['s1']}, - {name: 'd1: it it2', passed: true, steps: ['s2', 's2.2']}, - {name: 'd2: it it3', passed: true, steps: ['s3']}, - {name: 'd2: it it4', passed: true, steps: ['s4']} + expect(_window.$testrun.done).toBeTruthy(); + expect(_window.$testrun.results).toEqual([ + {name: 'd1: it it1', passed: true, error: undefined, steps: ['s1']}, + {name: 'd1: it it2', passed: true, error: undefined, steps: ['s2', 's2.2']}, + {name: 'd2: it it3', passed: true, error: undefined, steps: ['s3']}, + {name: 'd2: it it4', passed: true, error: undefined, steps: ['s4']} ]); }); }); diff --git a/test/scenario/TestContext.js b/test/scenario/TestContext.js new file mode 100644 index 00000000..0c8e6143 --- /dev/null +++ b/test/scenario/TestContext.js @@ -0,0 +1,15 @@ +var _window, runner, log, $scenario; + +function logger(text) { + return function(done){ + log += text; + (done||noop)(); + }; +} + +function setUpContext() { + _window = {}; + runner = new angular.scenario.Runner(_window, _jQuery); + $scenario = _window.$scenario; + log = ''; +} diff --git a/test/servicesSpec.js b/test/servicesSpec.js index 32e7812a..ffd01267 100644 --- a/test/servicesSpec.js +++ b/test/servicesSpec.js @@ -1,5 +1,5 @@ describe("service", function(){ - var scope, $xhrError, $log; + var scope, $xhrError, $log, mockServices; beforeEach(function(){ $xhrError = jasmine.createSpy('$xhr.error'); @@ -33,32 +33,45 @@ describe("service", function(){ describe("$log", function(){ it('should use console if present', function(){ - function log(){}; - function warn(){}; - function info(){}; - function error(){}; - var scope = createScope(null, angularService, {$window: {console:{log:log, warn:warn, info:info, error:error}}}); - expect(scope.$log.log).toEqual(log); - expect(scope.$log.warn).toEqual(warn); - expect(scope.$log.info).toEqual(info); - expect(scope.$log.error).toEqual(error); + var logger = ""; + function log(){ logger+= 'log;'; }; + function warn(){ logger+= 'warn;'; }; + function info(){ logger+= 'info;'; }; + function error(){ logger+= 'error;'; }; + var scope = createScope(null, angularService, {$window: {console:{log:log, warn:warn, info:info, error:error}}, $document:[{}]}); + scope.$log.log(); + scope.$log.warn(); + scope.$log.info(); + scope.$log.error(); + expect(logger).toEqual('log;warn;info;error;'); }); it('should use console.log if other not present', function(){ - function log(){}; - var scope = createScope(null, angularService, {$window: {console:{log:log}}}); - expect(scope.$log.log).toEqual(log); - expect(scope.$log.warn).toEqual(log); - expect(scope.$log.info).toEqual(log); - expect(scope.$log.error).toEqual(log); + var logger = ""; + function log(){ logger+= 'log;'; }; + var scope = createScope(null, angularService, {$window: {console:{log:log}}, $document:[{}]}); + scope.$log.log(); + scope.$log.warn(); + scope.$log.info(); + scope.$log.error(); + expect(logger).toEqual('log;log;log;log;'); }); it('should use noop if no console', function(){ - var scope = createScope(null, angularService, {$window: {}}); - expect(scope.$log.log).toEqual(noop); - expect(scope.$log.warn).toEqual(noop); - expect(scope.$log.info).toEqual(noop); - expect(scope.$log.error).toEqual(noop); + var scope = createScope(null, angularService, {$window: {}, $document:[{}]}); + scope.$log.log(); + scope.$log.warn(); + scope.$log.info(); + scope.$log.error(); + }); + }); + + describe("$exceptionHandler", function(){ + it('should log errors', function(){ + var error = ''; + $log.error = function(m) { error += m; }; + scope.$exceptionHandler('myError'); + expect(error).toEqual('myError'); }); }); @@ -78,7 +91,7 @@ describe("service", function(){ scope.$location.hashPath = 'page=http://path'; scope.$location.hashSearch = {k:'a=b'}; - expect(scope.$location.toString()).toEqual('http://host:123/p/a/t/h.html?query=value#page=http://path?k=a%3Db'); + expect(scope.$location.toString()).toEqual('http://host:123/p/a/t/h.html?query=value#page%3Dhttp%3A//path?k=a%3Db'); }); it('should parse file://', function(){ @@ -93,7 +106,7 @@ describe("service", function(){ expect(scope.$location.hashPath).toEqual(''); expect(scope.$location.hashSearch).toEqual({}); - expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html#'); + expect(scope.$location.toString()).toEqual('file:///Users/Shared/misko/work/angular.js/scenario/widgets.html'); }); it('should update url on hash change', function(){ @@ -110,6 +123,14 @@ describe("service", function(){ expect(scope.$location.hash).toEqual('?a=b'); }); + it("should parse url which contains - in host", function(){ + scope.$location.parse('http://a-b1.c-d.09/path'); + expect(scope.$location.href).toEqual('http://a-b1.c-d.09/path'); + expect(scope.$location.protocol).toEqual('http'); + expect(scope.$location.host).toEqual('a-b1.c-d.09'); + expect(scope.$location.path).toEqual('/path'); + }); + it('should update hash before any processing', function(){ var scope = compile('<div>'); var log = ''; @@ -123,15 +144,6 @@ describe("service", function(){ scope.$eval(); expect(log).toEqual('/abc;'); }); - - it("should parse url which contains - in host", function(){ - scope.$location.parse('http://a-b1.c-d.09/path'); - expect(scope.$location.href).toEqual('http://a-b1.c-d.09/path'); - expect(scope.$location.protocol).toEqual('http'); - expect(scope.$location.host).toEqual('a-b1.c-d.09'); - expect(scope.$location.path).toEqual('/path'); - }); - }); describe("$invalidWidgets", function(){ diff --git a/test/testabilityPatch.js b/test/testabilityPatch.js index b71943f6..e9a88b67 100644 --- a/test/testabilityPatch.js +++ b/test/testabilityPatch.js @@ -35,22 +35,25 @@ function childNode(element, index) { } extend(angular, { - 'bind': bind, + 'element': jqLite, 'compile': compile, + 'scope': createScope, 'copy': copy, - 'element': jqLite, 'extend': extend, + 'equals': equals, 'foreach': foreach, + 'noop':noop, + 'bind':bind, + 'toJson': toJson, + 'fromJson': fromJson, 'identity':identity, 'isUndefined': isUndefined, 'isDefined': isDefined, - 'isObject': isObject, 'isString': isString, 'isFunction': isFunction, + 'isObject': isObject, 'isNumber': isNumber, - 'isArray': isArray, - 'noop':noop, - 'scope': createScope + 'isArray': isArray }); @@ -63,6 +66,8 @@ function sortedHtml(element) { html += '<' + node.nodeName.toLowerCase(); var attributes = node.attributes || []; var attrs = []; + if (node.className) + attrs.push(' class="' + node.className + '"'); for(var i=0; i<attributes.length; i++) { var attr = attributes[i]; if(attr.name.match(/^ng:/) || @@ -76,6 +81,7 @@ function sortedHtml(element) { attr.name !='complete' && attr.name !='maxLength' && attr.name !='size' && + attr.name !='class' && attr.name !='start' && attr.name !='tabIndex' && attr.name !='style' && diff --git a/test/widgetsSpec.js b/test/widgetsSpec.js index 03f31bfe..ad98e482 100644 --- a/test/widgetsSpec.js +++ b/test/widgetsSpec.js @@ -407,7 +407,7 @@ describe("widget", function(){ it("should match sandwich ids", function(){ var scope = {}; - var match = angular.widget['NG:SWITCH'].route.call(scope, '/a/123/b', '/a/:id'); + var match = angular.widget('NG:SWITCH').route.call(scope, '/a/123/b', '/a/:id'); expect(match).toBeFalsy(); }); |
