├── .editorconfig ├── .gitignore ├── .jshintrc ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── build ├── angular-easyfb.js └── angular-easyfb.min.js ├── package.json ├── src └── angular-easyfb.js └── test ├── helper.js ├── karma-unit.conf.js ├── pubsub.js └── unit ├── ezfb.spec.js ├── pubsub.spec.js └── social-plugin-directive.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | insert_final_newline = true 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | *.sublime-* 4 | .tern-port 5 | .DS_Store 6 | bower_components 7 | coverage 8 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "undef": true 3 | } 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - '0.12' 5 | before_script: 6 | - 'npm install -g grunt-cli' 7 | - 'npm install -g bower' 8 | - 'bower install' 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.6.0 2 | 3 | - Support jQuery 3 through migrating deprecated `.bind` to `.on` ([#84](https://github.com/pc035860/angular-easyfb/pull/84)) 4 | 5 | ## v1.5.2 6 | 7 | * Support [Login Button](https://developers.facebook.com/docs/facebook-login/web/login-button) ([#78](https://github.com/pc035860/angular-easyfb/issues/78)) 8 | * Support [Save Button](https://developers.facebook.com/docs/plugins/save) and [Embedded Comments](https://developers.facebook.com/docs/plugins/embedded-comments) 9 | 10 | ## v1.5.1 11 | 12 | * Bump bower version 13 | 14 | ## v1.5.0 15 | 16 | * Add [Send-to-Messenger Plugin](https://developers.facebook.com/docs/messenger-platform/plugin-reference#send_to_messenger) and [Message-Us Plugin](https://developers.facebook.com/docs/messenger-platform/plugin-reference#message_us) 17 | * Change default FB JS SDK platform version to `v2.6` 18 | 19 | ## v1.4.4 20 | 21 | * Fix intialization error when `xfbml: false` parameter is given ([#66](https://github.com/pc035860/angular-easyfb/issues/66)) 22 | 23 | ## v1.4.3 24 | 25 | * Fix the "childNodes error" caused by [Google Analytics Opt-out Add-on](https://chrome.google.com/webstore/detail/google-analytics-opt-out/fllaojicojecljbmefodhfapmkghcbnh) ([Ben Nadel's article](http://www.bennadel.com/blog/2892-typeerror-cannot-read-property-childnodes-of-undefined-in-angularjs.htm)) ([#65](https://github.com/pc035860/angular-easyfb/issues/65)) 26 | 27 | 28 | ## v1.4.2 29 | 30 | * Update `main` configuration of `package.json` ([#63](https://github.com/pc035860/angular-easyfb/issues/63)) 31 | 32 | 33 | ## v1.4.1 34 | 35 | * Update `main` configuration of `bower.json` ([#56](https://github.com/pc035860/angular-easyfb/issues/56)) 36 | 37 | ## v1.4.0 38 | 39 | * Add [Ad Preview Plugin](https://developers.facebook.com/docs/marketing-api/ad-preview-plugin/v2.4) support 40 | * In order to support _Ad Preview Plugin_, default platform version is now `2.4` 41 | 42 | ## v1.3.2 43 | 44 | * Support `100%` literal of `width` setting of [Comments](https://developers.facebook.com/docs/plugins/comments). ([#53](https://github.com/pc035860/angular-easyfb/issues/53)) 45 | 46 | ## v1.3.1 47 | 48 | * Refine social plugin directives parsing process for plugins with adaptive width (`fb-page` for now) ([#45](https://github.com/pc035860/angular-easyfb/pull/45)) 49 | * Add [Embedded Video Player](https://developers.facebook.com/docs/plugins/embedded-video-player/) support 50 | 51 | ## v1.3.0 52 | 53 | * Change default `FB.init` version from `v1.0` to `v2.0` 54 | * Add [Page plugin](https://developers.facebook.com/docs/plugins/page-plugin/) support ([#45](https://github.com/pc035860/angular-easyfb/pull/45)) 55 | 56 | ## v1.2.1 57 | 58 | * Add API support: `Canvas.getPageInfo` ([#40](https://github.com/pc035860/angular-easyfb/pull/40)) 59 | 60 | ## v1.2.0 61 | 62 | * Add [App Events for Canvas](https://developers.facebook.com/docs/canvas/appevents) support. ([#39](https://github.com/pc035860/angular-easyfb/issues/39)) 63 | 64 | ## v1.1.0 65 | 66 | * Upgrade Facebook JS SDK loading function to support [Facebook Platform versioning](https://developers.facebook.com/docs/apps/changelog/) 67 | * Default version is `v1.0` 68 | 69 | ## v1.0.1 70 | 71 | * Add [fb:comments-count](https://developers.facebook.com/docs/plugins/comments/#faqcount) directive 72 | 73 | ## v1.0.0 74 | 75 | * Rename service `$FB` to `ezfb` 76 | * Local DIs get renamed. 77 | * `$fbInitParams` -> `ezfbInitParams` 78 | * `$fbAsyncInit` -> `ezfbAsyncInit` 79 | * `$fbLocale` -> `ezfbLocale` 80 | 81 | ## v0.3.1 82 | 83 | * Social plugin directivs now support interpolated attributes 84 | 85 | ## v0.3.0 86 | 87 | * `setLoadSDKFunction()` in configuration phase for sdk loading customization 88 | * Support [Facebook Social Plugins](https://developers.facebook.com/docs/plugins) with built-in directives 89 | * `ezfb-xfbml` directive is now deprecated 90 | * Add unit tests for `$FB` and all directives 91 | 92 | ## v0.2.3 93 | 94 | * Implement `$FB.getAuthResponse()` which maps to [`FB.getAuthResponse()`](https://developers.facebook.com/docs/reference/javascript/FB.getAuthResponse/) 95 | 96 | ## v0.2.2 97 | 98 | * `setInitFunction()` in configuration phase for initialization customization 99 | * Make `$FB.Event.unsubscribe` unsubscribes events properly 100 | 101 | ## v0.2.1 102 | 103 | * `setLocale()` in configuration phase 104 | * Configure `FB.init` parameters with `$FB.init` in run block 105 | 106 | ## v0.2.0 107 | 108 | * AngularJS $q promise support 109 | * Fix minified code run-time error 110 | 111 | ## v0.1.0 112 | 113 | First release! 114 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | /*global module*/ 2 | module.exports = function(grunt) { 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | jshint: { 6 | options: { 7 | curly: true, 8 | jshintrc: '.jshintrc', 9 | reporterOutput: '' 10 | }, 11 | beforeuglify: ['src/<%= pkg.name %>.js'], 12 | gruntfile: ['Gruntfile.js'] 13 | }, 14 | uglify: { 15 | src: { 16 | src: 'src/<%= pkg.name %>.js', 17 | dest: 'build/<%= pkg.name %>.js', 18 | options: { 19 | mangle: false, 20 | compress: false, 21 | beautify: true, 22 | banner: 23 | '/*! <%= pkg.name %>\n' + 24 | 'version: <%= pkg.version %>\n' + 25 | 'build date: <%= grunt.template.today("yyyy-mm-dd") %>\n' + 26 | 'author: <%= pkg.author %>\n' + 27 | '<%= pkg.repository.url %> */\n' 28 | } 29 | }, 30 | build: { 31 | src: 'src/<%= pkg.name %>.js', 32 | dest: 'build/<%= pkg.name %>.min.js', 33 | options: { 34 | mangle: true, 35 | compress: {}, 36 | banner: 37 | '/*! <%= pkg.name %>\n' + 38 | 'version: <%= pkg.version %>\n' + 39 | 'build date: <%= grunt.template.today("yyyy-mm-dd") %>\n' + 40 | 'author: <%= pkg.author %>\n' + 41 | '<%= pkg.repository.url %> */\n' 42 | } 43 | } 44 | }, 45 | watch: { 46 | gruntfile: { 47 | files: 'Gruntfile.js', 48 | tasks: ['jshint:gruntfile'] 49 | }, 50 | src: { 51 | files: '<%= pkg.name %>.js', 52 | tasks: ['default'] 53 | } 54 | }, 55 | connect: { 56 | server: { 57 | options: { 58 | port: 8080, 59 | base: '', 60 | keepalive: true 61 | } 62 | }, 63 | coverage: { 64 | options: { 65 | port: 5555, 66 | base: 'coverage/', 67 | keepalive: true 68 | } 69 | } 70 | }, 71 | karma: { 72 | unit: { 73 | configFile: './test/karma-unit.conf.js', 74 | autoWatch: false, 75 | singleRun: true 76 | }, 77 | unit_auto: { 78 | configFile: './test/karma-unit.conf.js', 79 | autoWatch: true, 80 | singleRun: false 81 | }, 82 | unit_coverage: { 83 | configFile: './test/karma-unit.conf.js', 84 | autoWatch: false, 85 | singleRun: true, 86 | reporters: ['progress', 'coverage'], 87 | preprocessors: { 88 | 'angular-easyfb.js': ['coverage'] 89 | }, 90 | coverageReporter: { 91 | type : 'html', 92 | dir : 'coverage/' 93 | } 94 | } 95 | }, 96 | open: { 97 | coverage: { 98 | path: 'http://localhost:5555' 99 | } 100 | } 101 | }); 102 | 103 | grunt.loadNpmTasks('grunt-contrib-jshint'); 104 | grunt.loadNpmTasks('grunt-contrib-uglify'); 105 | grunt.loadNpmTasks('grunt-contrib-watch'); 106 | grunt.loadNpmTasks('grunt-contrib-connect'); 107 | grunt.loadNpmTasks('grunt-karma'); 108 | grunt.loadNpmTasks('grunt-open'); 109 | 110 | // single run tests 111 | grunt.registerTask('test', ['test:unit']); 112 | grunt.registerTask('test:unit', ['karma:unit']); 113 | 114 | // autotest and watch tests 115 | grunt.registerTask('autotest', ['karma:unit_auto']); 116 | grunt.registerTask('autotest:unit', ['karma:unit_auto']); 117 | 118 | //coverage testing 119 | grunt.registerTask('test:coverage', ['karma:unit_coverage']); 120 | grunt.registerTask('coverage', ['karma:unit_coverage','open:coverage','connect:coverage']); 121 | 122 | grunt.registerTask('default', ['jshint:beforeuglify', 'test', 'uglify']); 123 | }; 124 | 125 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Robin Fan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-easyfb [![Build Status](https://travis-ci.org/pc035860/angular-easyfb.svg?branch=master)](https://travis-ci.org/pc035860/angular-easyfb) [![npm](https://img.shields.io/npm/v/angular-easyfb.svg)](https://www.npmjs.com/package/angular-easyfb) [![Bower](https://img.shields.io/bower/v/angular-easyfb.svg)](http://bower.io/search/?q=angular-easyfb) [![Gitter](https://badges.gitter.im/pc035860/angular-easyfb.svg)](https://gitter.im/pc035860/angular-easyfb?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 2 | 3 | AngularJS + Facebook JavaScript SDK. 4 | 5 | **Start from v1.1.0, `angular-easyfb` adds support for [Facebook Platform versioning](https://developers.facebook.com/docs/apps/changelog/).** 6 | 7 | **Please check out [the new FB JS SDK setup doc](https://developers.facebook.com/docs/javascript/quickstart#loading) if you want to switch platform versions (module default is `v2.6`).** 8 | 9 | #### Features 10 | 11 | * Full [Facebook JavaScript SDK](https://developers.facebook.com/docs/reference/javascript/) support 12 | * Seamless FB SDK initialization (asynchronously load script and FB.init) 13 | * All SDK API callbacks are automatically applied with AngularJS context 14 | * Support both callback and $q promise 15 | * Provide built-in directive support for Facebook XFBML plugins 16 | 17 | #### Demos 18 | 19 | * [API demo](http://plnkr.co/edit/qclqht?p=preview) 20 | * [API demo (promise version)](http://plnkr.co/edit/UMUtFc?p=preview) 21 | * [Built-in social plugin directives demo](http://plnkr.co/edit/z8751z?p=preview) 22 | 23 | 24 | ## Getting started 25 | 26 | Include the angular-easyfb module with AngularJS script in your page. 27 | ```html 28 | 29 | 30 | ``` 31 | 32 | Add `ezfb` to your app module's dependency. 33 | ```js 34 | angular.module('myApp', ['ezfb']); 35 | ``` 36 | 37 | ### Install with npm 38 | 39 | ```sh 40 | npm install angular-easyfb 41 | ``` 42 | 43 | ### Install with Bower 44 | 45 | ```sh 46 | bower install angular-easyfb 47 | ``` 48 | 49 | ## Usage 50 | 51 | ### `ezfb` service 52 | 53 | #### Configuration 54 | 55 | ###### `getLocale` / `setLocale` 56 | 57 | Configure the locale of the original FB script file. Default locale is `en_US`. 58 | 59 | ```js 60 | angular.module('myApp') 61 | 62 | .config(function (ezfbProvider) { 63 | ezfbProvider.setLocale('zh_TW'); 64 | }); 65 | ``` 66 | 67 | ###### `getInitParams` / `setInitParams` 68 | 69 | Configure parameters for the original `FB.init` with `ezfbProvider.setInitParams`. (See also [`ezfb.init`](#fbinit)) 70 | 71 | ```js 72 | angular.module('myApp') 73 | 74 | .config(function (ezfbProvider) { 75 | ezfbProvider.setInitParams({ 76 | // This is my FB app id for plunker demo app 77 | appId: '386469651480295', 78 | 79 | // Module default is `v2.6`. 80 | // If you want to use Facebook platform `v2.3`, you'll have to add the following parameter. 81 | // https://developers.facebook.com/docs/javascript/reference/FB.init 82 | version: 'v2.3' 83 | }); 84 | }); 85 | ``` 86 | 87 | ###### `getInitFunction` / `setInitFunction` 88 | 89 | Customize the original `FB.init` function call with services injection support. The initialization parameters set in `setInitParams` are available via local injection `ezfbInitParams`. 90 | 91 | ```js 92 | // Default init function 93 | var _defaultInitFunction = ['$window', 'ezfbInitParams', function ($window, ezfbInitParams) { 94 | // Initialize the FB JS SDK 95 | $window.FB.init(ezfbInitParams); 96 | }]; 97 | ``` 98 | 99 | Customization example: 100 | ```js 101 | angular.module('myApp') 102 | 103 | .config(function (ezfbProvider) { 104 | var myInitFunction = function ($window, $rootScope, ezfbInitParams) { 105 | $window.FB.init({ 106 | appId: '386469651480295' 107 | }); 108 | // or 109 | // $window.FB.init(ezfbInitParams); 110 | 111 | $rootScope.$broadcast('FB.init'); 112 | }; 113 | 114 | ezfbProvider.setInitFunction(myInitFunction); 115 | }); 116 | ``` 117 | 118 | ###### `getLoadSDKFunction / setLoadSDKFunction` 119 | 120 | Customize Facebook JS SDK loading. The function also supports DI, with two more local injections: 121 | 122 | - `ezfbLocale` - locale name 123 | - `ezfbAsyncInit` - must called to finish the module initialization process 124 | 125 | ```js 126 | // Default load SDK function 127 | var _defaultLoadSDKFunction = [ 128 | '$window', '$document', 'ezfbAsyncInit', 'ezfbLocale', 129 | function ($window, $document, ezfbAsyncInit, ezfbLocale) { 130 | // Load the SDK's source Asynchronously 131 | (function(d){ 132 | var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0]; 133 | if (d.getElementById(id)) {return;} 134 | js = d.createElement('script'); js.id = id; js.async = true; 135 | js.src = "//connect.facebook.net/" + ezfbLocale + "/sdk.js"; 136 | // js.src = "//connect.facebook.net/" + ezfbLocale + "/sdk/debug.js"; // debug 137 | ref.parentNode.insertBefore(js, ref); 138 | }($document[0])); 139 | 140 | $window.fbAsyncInit = ezfbAsyncInit; 141 | }]; 142 | ``` 143 | 144 | Customization example: 145 | ```js 146 | angular.module('myApp') 147 | 148 | .config(function (ezfbProvider) { 149 | // Feasible config if the FB JS SDK script is already loaded 150 | ezfbProvider.setLoadSDKFunction(function (ezfbAsyncInit) { 151 | ezfbAsyncInit(); 152 | }); 153 | }); 154 | ``` 155 | 156 | 157 | #### ezfb.init 158 | 159 | In the case that you don't want to(or you can't) configure your `FB.init` parameters in configuration phase, you may use `ezfb.init` in run phase. And any `ezfb` API call will not run until `ezfb.init` is called. 160 | 161 | ```js 162 | angular.module('myApp') 163 | 164 | .run(function (ezfb) { 165 | ezfb.init({ 166 | // This is my FB app id for plunker demo app 167 | appId: '386469651480295' 168 | }); 169 | }); 170 | ``` 171 | 172 | 173 | #### using ezfb 174 | 175 | This is the original `FB` wrapping service, all `FB.*` APIs are available through `ezfb.*`. 176 | 177 | No need to worry about FB script loading and Angular context applying at all. 178 | 179 | 180 | ```js 181 | angular.module('myApp') 182 | 183 | /** 184 | * Inject into controller 185 | */ 186 | .controller('MainCtrl', function (ezfb) { 187 | /** 188 | * Origin: FB.getLoginStatus 189 | */ 190 | ezfb.getLoginStatus(function (res) { 191 | $scope.loginStatus = res; 192 | 193 | (more || angular.noop)(); 194 | }); 195 | 196 | /** 197 | * Origin: FB.api 198 | */ 199 | ezfb.api('/me', function (res) { 200 | $scope.apiMe = res; 201 | }); 202 | }); 203 | 204 | ``` 205 | 206 | Watch the [demo](http://plnkr.co/edit/qclqht?p=preview) to see it in action. 207 | 208 | #### $q promise support 209 | 210 | Support of $q promise create more possibility for `ezfb` service. 211 | 212 | **Only the APIs with callback support returning promise.** 213 | 214 | ##### Combine multiple api calls 215 | ```js 216 | $q.all([ 217 | ezfb.api('/me'), 218 | ezfb.api('/me/likes') 219 | ]) 220 | .then(function (rsvList) { 221 | // result of api('/me') 222 | console.log(rsvList[0]); 223 | 224 | // result of api('/me/likes') 225 | console.log(rsvList[1]); 226 | }); 227 | ``` 228 | 229 | Watch the [promise version api demo](http://plnkr.co/edit/UMUtFc?p=preview) to see it in action. 230 | 231 | 232 | ### Social plugins support 233 | 234 | [Facebook Social Plugins](https://developers.facebook.com/docs/plugins/) are now supported with built-in directives. 235 | 236 | The code copied from the above link will automatically work in `angular-easyfb`-covered AngularJS apps. 237 | 238 | Additionally, you can add an `onrender` parameter to the social plugin directive. Expressions in the `onrender` parameter will be evaluated every time the social plugin gets rendered. 239 | 240 | ```html 241 |
247 | ``` 248 | 249 | [Demo (directives demonstration)](http://plnkr.co/edit/z8751z?p=preview) 250 | 251 | [Demo2 (interpolated attributes)](http://plnkr.co/edit/gFM1LV?p=preview) 252 | 253 | 254 | ## Changelog 255 | 256 | See the changelog [here](https://github.com/pc035860/angular-easyfb/blob/master/CHANGELOG.md). 257 | 258 | 259 | ## Develop 260 | 261 | `angular-easyfb` uses [Grunt](http://gruntjs.com/) to run all the development tasks. 262 | 263 | If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. 264 | 265 | `angular-easyfb` also uses [Bower](http://bower.io/) to manage packages for tests. 266 | 267 | ### Setup 268 | 269 | After cloning the git repo to your place, simply run following commands to install required packages. 270 | ```sh 271 | npm install 272 | bower install 273 | ``` 274 | 275 | ### Build 276 | 277 | Generate a minified js file after running all the tests. 278 | 279 | ```sh 280 | grunt 281 | ``` 282 | 283 | ### Running tests 284 | 285 | Unit tests: 286 | ```sh 287 | grunt test:unit 288 | ``` 289 | 290 | Test coverage: 291 | ```sh 292 | grunt coverage 293 | ``` 294 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-easyfb", 3 | "version": "1.6.0", 4 | "description": "Super easy AngularJS + Facebook JavaScript SDK.", 5 | "main": "./build/angular-easyfb.js", 6 | "ignore": [ 7 | "**/.*", 8 | "node_modules", 9 | "bower_components", 10 | "Gruntfile.js", 11 | "package.json", 12 | "test", 13 | "tests" 14 | ], 15 | "dependencies": {}, 16 | "devDependencies": { 17 | "angular": "^1.4.0", 18 | "angular-mocks": "^1.4.0" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /build/angular-easyfb.js: -------------------------------------------------------------------------------- 1 | /*! angular-easyfb 2 | version: 1.6.0 3 | build date: 2019-08-20 4 | author: Robin Fan 5 | https://github.com/pc035860/angular-easyfb.git */ 6 | (function(module) { 7 | module.provider("ezfb", function() { 8 | var APP_EVENTS_EVENT_NAMES = { 9 | COMPLETED_REGISTRATION: "fb_mobile_complete_registration", 10 | VIEWED_CONTENT: "fb_mobile_content_view", 11 | SEARCHED: "fb_mobile_search", 12 | RATED: "fb_mobile_rate", 13 | COMPLETED_TUTORIAL: "fb_mobile_tutorial_completion", 14 | ADDED_TO_CART: "fb_mobile_add_to_cart", 15 | ADDED_TO_WISHLIST: "fb_mobile_add_to_wishlist", 16 | INITIATED_CHECKOUT: "fb_mobile_initiated_checkout", 17 | ADDED_PAYMENT_INFO: "fb_mobile_add_payment_info", 18 | ACHIEVED_LEVEL: "fb_mobile_level_achieved", 19 | UNLOCKED_ACHIEVEMENT: "fb_mobile_achievement_unlocked", 20 | SPENT_CREDITS: "fb_mobile_spent_credits" 21 | }, APP_EVENTS_PARAMETER_NAMES = { 22 | CURRENCY: "fb_currency", 23 | REGISTRATION_METHOD: "fb_registration_method", 24 | CONTENT_TYPE: "fb_content_type", 25 | CONTENT_ID: "fb_content_id", 26 | SEARCH_STRING: "fb_search_string", 27 | SUCCESS: "fb_success", 28 | MAX_RATING_VALUE: "fb_max_rating_value", 29 | PAYMENT_INFO_AVAILABLE: "fb_payment_info_available", 30 | NUM_ITEMS: "fb_num_items", 31 | LEVEL: "fb_level", 32 | DESCRIPTION: "fb_description" 33 | }; 34 | var NO_CALLBACK = -1; 35 | var _publishedApis = { 36 | api: [ 1, 2, 3 ], 37 | ui: 1, 38 | getAuthResponse: NO_CALLBACK, 39 | getLoginStatus: 0, 40 | login: 0, 41 | logout: 0, 42 | "Event.subscribe": 1, 43 | "Event.unsubscribe": 1, 44 | "XFBML.parse": 1, 45 | "Canvas.Prefetcher.addStaticResource": NO_CALLBACK, 46 | "Canvas.Prefetcher.setCollectionMode": NO_CALLBACK, 47 | "Canvas.getPageInfo": 0, 48 | "Canvas.hideFlashElement": NO_CALLBACK, 49 | "Canvas.scrollTo": NO_CALLBACK, 50 | "Canvas.setAutoGrow": NO_CALLBACK, 51 | "Canvas.setDoneLoading": 0, 52 | "Canvas.setSize": NO_CALLBACK, 53 | "Canvas.setUrlHandler": 0, 54 | "Canvas.showFlashElement": NO_CALLBACK, 55 | "Canvas.startTimer": NO_CALLBACK, 56 | "Canvas.stopTimer": 0, 57 | "AppEvents.logEvent": NO_CALLBACK, 58 | "AppEvents.logPurchase": NO_CALLBACK, 59 | "AppEvents.activateApp": NO_CALLBACK 60 | }; 61 | var _locale = "en_US"; 62 | var _initParams = { 63 | status: true, 64 | cookie: true, 65 | xfbml: true, 66 | version: "v2.6" 67 | }; 68 | var _defaultLoadSDKFunction = [ "$window", "$document", "$timeout", "ezfbAsyncInit", "ezfbLocale", function($window, $document, $timeout, ezfbAsyncInit, ezfbLocale) { 69 | (function(d) { 70 | var insertScript = function() { 71 | var js, id = "facebook-jssdk", ref = d.getElementsByTagName("script")[0]; 72 | if (d.getElementById(id)) { 73 | return; 74 | } 75 | js = d.createElement("script"); 76 | js.id = id; 77 | js.async = true; 78 | js.src = "//connect.facebook.net/" + ezfbLocale + "/sdk.js"; 79 | ref.parentNode.insertBefore(js, ref); 80 | }; 81 | $timeout(insertScript, 0, false); 82 | })($document[0]); 83 | $window.fbAsyncInit = ezfbAsyncInit; 84 | } ], _loadSDKFunction = _defaultLoadSDKFunction; 85 | var _defaultInitFunction = [ "$window", "ezfbInitParams", function($window, ezfbInitParams) { 86 | $window.FB.init(ezfbInitParams); 87 | } ], _initFunction = _defaultInitFunction; 88 | function _config(target, config) { 89 | if (angular.isObject(config)) { 90 | angular.extend(target, config); 91 | } else { 92 | return angular.copy(target); 93 | } 94 | } 95 | function _proxy(func, context, args) { 96 | return function() { 97 | return func.apply(context, args); 98 | }; 99 | } 100 | return { 101 | setInitParams: function(params) { 102 | _config(_initParams, params); 103 | }, 104 | getInitParams: function() { 105 | return _config(_initParams); 106 | }, 107 | setLocale: function(locale) { 108 | _locale = locale; 109 | }, 110 | getLocale: function() { 111 | return _locale; 112 | }, 113 | setLoadSDKFunction: function(func) { 114 | if (angular.isArray(func) || angular.isFunction(func)) { 115 | _loadSDKFunction = func; 116 | } else { 117 | throw new Error("Init function type error."); 118 | } 119 | }, 120 | getLoadSDKFunction: function() { 121 | return _loadSDKFunction; 122 | }, 123 | setInitFunction: function(func) { 124 | if (angular.isArray(func) || angular.isFunction(func)) { 125 | _initFunction = func; 126 | } else { 127 | throw new Error("Init function type error."); 128 | } 129 | }, 130 | getInitFunction: function() { 131 | return _initFunction; 132 | }, 133 | $get: [ "$window", "$q", "$document", "$parse", "$rootScope", "$injector", "$timeout", function($window, $q, $document, $parse, $rootScope, $injector, $timeout) { 134 | var _initReady, _initRenderReady, _ezfb, _savedListeners, _paramsReady, ezfbAsyncInit; 135 | _savedListeners = {}; 136 | _paramsReady = $q.defer(); 137 | if (_initParams.appId || _initFunction !== _defaultInitFunction) { 138 | _paramsReady.resolve(); 139 | } 140 | _initReady = $q.defer(); 141 | _initRenderReady = $q.defer(); 142 | if (!$document[0].getElementById("fb-root")) { 143 | $document.find("body").append('
'); 144 | } 145 | ezfbAsyncInit = function() { 146 | _paramsReady.promise.then(function() { 147 | if (_initParams.xfbml) { 148 | var onRender = function() { 149 | _ezfb.$$xfbmlRendered = true; 150 | $timeout(function() { 151 | _initRenderReady.resolve(true); 152 | }); 153 | _ezfb.Event.unsubscribe("xfbml.render", onRender); 154 | }; 155 | _ezfb.Event.subscribe("xfbml.render", onRender); 156 | } else { 157 | $timeout(function() { 158 | _initRenderReady.resolve(false); 159 | }); 160 | } 161 | $injector.invoke(_initFunction, null, { 162 | ezfbInitParams: _initParams 163 | }); 164 | _ezfb.$$ready = true; 165 | _initReady.resolve(); 166 | }); 167 | }; 168 | $injector.invoke(_loadSDKFunction, null, { 169 | ezfbAsyncInit: ezfbAsyncInit, 170 | ezfbLocale: _locale 171 | }); 172 | _ezfb = { 173 | $$ready: false, 174 | $$xfbmlRendered: false, 175 | $ready: function(fn) { 176 | if (angular.isFunction(fn)) { 177 | _initReady.promise.then(fn); 178 | } 179 | return _initReady.promise; 180 | }, 181 | $rendered: function(fn) { 182 | if (angular.isFunction(fn)) { 183 | _initRenderReady.promise.then(fn); 184 | } 185 | return _initRenderReady.promise; 186 | }, 187 | init: function(params) { 188 | _config(_initParams, params); 189 | _paramsReady.resolve(); 190 | }, 191 | AppEvents: { 192 | EventNames: APP_EVENTS_EVENT_NAMES, 193 | ParameterNames: APP_EVENTS_PARAMETER_NAMES 194 | } 195 | }; 196 | angular.forEach(_publishedApis, function(cbArgIndex, apiPath) { 197 | var getter = $parse(apiPath), setter = getter.assign; 198 | setter(_ezfb, function() { 199 | var apiCall = _proxy(function(args) { 200 | var dfd, replaceCallbackAt; 201 | dfd = $q.defer(); 202 | replaceCallbackAt = function(index) { 203 | var func, newFunc; 204 | func = angular.isFunction(args[index]) ? args[index] : angular.noop; 205 | newFunc = function() { 206 | var funcArgs = Array.prototype.slice.call(arguments); 207 | if ($rootScope.$$phase) { 208 | func.apply(null, funcArgs); 209 | dfd.resolve.apply(dfd, funcArgs); 210 | } else { 211 | $rootScope.$apply(function() { 212 | func.apply(null, funcArgs); 213 | dfd.resolve.apply(dfd, funcArgs); 214 | }); 215 | } 216 | }; 217 | while (args.length <= index) { 218 | args.push(null); 219 | } 220 | var eventName; 221 | if (apiPath === "Event.subscribe") { 222 | eventName = args[0]; 223 | if (angular.isUndefined(_savedListeners[eventName])) { 224 | _savedListeners[eventName] = []; 225 | } 226 | _savedListeners[eventName].push({ 227 | original: func, 228 | wrapped: newFunc 229 | }); 230 | } else if (apiPath === "Event.unsubscribe") { 231 | eventName = args[0]; 232 | if (angular.isArray(_savedListeners[eventName])) { 233 | var i, subscribed, l = _savedListeners[eventName].length; 234 | for (i = 0; i < l; i++) { 235 | subscribed = _savedListeners[eventName][i]; 236 | if (subscribed.original === func) { 237 | newFunc = subscribed.wrapped; 238 | _savedListeners[eventName].splice(i, 1); 239 | break; 240 | } 241 | } 242 | } 243 | } 244 | args[index] = newFunc; 245 | }; 246 | if (cbArgIndex !== NO_CALLBACK) { 247 | if (angular.isNumber(cbArgIndex)) { 248 | replaceCallbackAt(cbArgIndex); 249 | } else if (angular.isArray(cbArgIndex)) { 250 | var i, c; 251 | for (i = 0; i < cbArgIndex.length; i++) { 252 | c = cbArgIndex[i]; 253 | if (args.length == c || args.length == c + 1 && angular.isFunction(args[c])) { 254 | replaceCallbackAt(c); 255 | break; 256 | } 257 | } 258 | } 259 | } 260 | var origFBFunc = getter($window.FB); 261 | if (!origFBFunc) { 262 | throw new Error("Facebook API `FB." + apiPath + "` doesn't exist."); 263 | } 264 | origFBFunc.apply($window.FB, args); 265 | return dfd.promise; 266 | }, null, [ Array.prototype.slice.call(arguments) ]); 267 | if (apiPath === "getAuthResponse") { 268 | if (angular.isUndefined($window.FB)) { 269 | throw new Error("`FB` is not ready."); 270 | } 271 | return $window.FB.getAuthResponse(); 272 | } else if (cbArgIndex === NO_CALLBACK) { 273 | _initReady.promise.then(apiCall); 274 | } else { 275 | return _initReady.promise.then(apiCall); 276 | } 277 | }); 278 | }); 279 | return _ezfb; 280 | } ] 281 | }; 282 | }).directive("ezfbXfbml", [ "ezfb", "$parse", "$compile", "$timeout", function(ezfb, $parse, $compile, $timeout) { 283 | return { 284 | restrict: "EAC", 285 | controller: function() {}, 286 | compile: function(tElm, tAttrs) { 287 | var _savedHtml = tElm.html(); 288 | return function postLink(scope, iElm, iAttrs) { 289 | var _rendering = true, onrenderExp = iAttrs.onrender, onrenderHandler = function() { 290 | if (_rendering) { 291 | if (onrenderExp) { 292 | scope.$eval(onrenderExp); 293 | } 294 | _rendering = false; 295 | } 296 | }; 297 | ezfb.XFBML.parse(iElm[0], onrenderHandler); 298 | var setter = $parse(iAttrs.ezfbXfbml).assign; 299 | scope.$watch(iAttrs.ezfbXfbml, function(val) { 300 | if (val) { 301 | _rendering = true; 302 | iElm.html(_savedHtml); 303 | $compile(iElm.contents())(scope); 304 | $timeout(function() { 305 | ezfb.XFBML.parse(iElm[0], onrenderHandler); 306 | }); 307 | (setter || angular.noop)(scope, false); 308 | } 309 | }, true); 310 | }; 311 | } 312 | }; 313 | } ]); 314 | var _socialPluginDirectiveConfig = { 315 | fbLike: [ "action", "colorscheme", "href", "kidDirectedSite", "layout", "ref", "share", "showFaces", "width" ], 316 | fbShareButton: [ "href", "layout", "width" ], 317 | fbSend: [ "colorscheme", "href", "kidDirectedSite", "ref" ], 318 | fbPost: [ "href", "width" ], 319 | fbFollow: [ "colorscheme", "href", "kidDirectedSite", "layout", "showFaces", "width" ], 320 | fbComments: [ "colorscheme", "href", "mobile", "numPosts", "orderBy", "width" ], 321 | fbCommentsCount: [ "href" ], 322 | fbActivity: [ "action", "appId", "colorscheme", "filter", "header", "height", "linktarget", "maxAge", "recommendations", "ref", "site", "width" ], 323 | fbRecommendations: [ "action", "appId", "colorscheme", "header", "height", "linktarget", "maxAge", "ref", "site", "width" ], 324 | fbRecommendationsBar: [ "action", "href", "maxAge", "numRecommendations", "readTime", "ref", "side", "site", "trigger" ], 325 | fbLikeBox: [ "colorscheme", "forceWall", "header", "height", "href", "showBorder", "showFaces", "stream", "width" ], 326 | fbFacepile: [ "action", "appId", "colorscheme", "href", "maxRows", "size", "width" ], 327 | fbPage: [ "href", "width", "height", "hideCover", "showFacepile", "showPosts" ], 328 | fbVideo: [ "href", "width", "allowfullscreen" ], 329 | fbAdPreview: [ "adAccountId", "adgroupId", "creative", "creativeId", "adFormat", "pageType", "targeting", "post" ], 330 | fbSendToMessenger: [ "messengerAppId", "pageId", "ref", "color", "size" ], 331 | fbMessengermessageus: [ "messengerAppId", "pageId", "color", "size" ], 332 | fbLoginButton: [ "autoLogoutLink", "maxRows", "onLogin", "scope", "size", "showFaces", "defaultAudience" ], 333 | fbCommentEmbed: [ "href", "width", "includeParent" ], 334 | fbSave: [ "uri" ] 335 | }; 336 | angular.forEach(_socialPluginDirectiveConfig, creatSocialPluginDirective); 337 | function creatSocialPluginDirective(availableAttrs, dirName) { 338 | var CLASS_WRAP = "ezfb-social-plugin-wrap", STYLE_WRAP_SPAN = "display: inline-block; width: 0; height: 0; overflow: hidden;"; 339 | var PLUGINS_WITH_ADAPTIVE_WIDTH = [ "fbPage", "fbComments" ]; 340 | var _wrap = function($elm) { 341 | var tmpl = ''; 342 | return $elm.wrap(tmpl).parent(); 343 | }, _wrapAdaptive = function($elm) { 344 | var tmpl = '
'; 345 | return $elm.wrap(tmpl).parent(); 346 | }, _isWrapped = function($elm) { 347 | return $elm.parent().hasClass(CLASS_WRAP); 348 | }, _unwrap = function($elm) { 349 | var $parent = $elm.parent(); 350 | $parent.after($elm).remove(); 351 | return $elm; 352 | }; 353 | module.directive(dirName, [ "ezfb", "$q", "$document", function(ezfb, $q, $document) { 354 | var _withAdaptiveWidth = PLUGINS_WITH_ADAPTIVE_WIDTH.indexOf(dirName) >= 0; 355 | var _dirClassName = dirName.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase(); 356 | return { 357 | restrict: "EC", 358 | require: "?^ezfbXfbml", 359 | compile: function(tElm, tAttrs) { 360 | tElm.removeClass(_dirClassName); 361 | return function postLink(scope, iElm, iAttrs, xfbmlCtrl) { 362 | if (xfbmlCtrl) { 363 | return; 364 | } 365 | var rendering = false, renderId = 0; 366 | ezfb.$rendered().then(function() { 367 | iElm.addClass(_dirClassName); 368 | scope.$watch(function() { 369 | var watchList = []; 370 | angular.forEach(availableAttrs, function(attrName) { 371 | watchList.push(iAttrs[attrName]); 372 | }); 373 | return watchList; 374 | }, function(v) { 375 | var wrapFn; 376 | renderId++; 377 | if (!rendering) { 378 | rendering = true; 379 | wrapFn = _withAdaptiveWidth ? _wrapAdaptive : _wrap; 380 | ezfb.XFBML.parse(wrapFn(iElm)[0], genOnRenderHandler(renderId)); 381 | } else { 382 | ezfb.XFBML.parse(iElm.parent()[0], genOnRenderHandler(renderId)); 383 | } 384 | }, true); 385 | }); 386 | iElm.on("$destroy", function() { 387 | if (_isWrapped(iElm)) { 388 | _unwrap(iElm); 389 | } 390 | }); 391 | function genOnRenderHandler(id) { 392 | return function() { 393 | var onrenderExp; 394 | if (rendering && id === renderId) { 395 | onrenderExp = iAttrs.onrender; 396 | if (onrenderExp) { 397 | scope.$eval(onrenderExp); 398 | } 399 | rendering = false; 400 | _unwrap(iElm); 401 | } 402 | }; 403 | } 404 | }; 405 | } 406 | }; 407 | } ]); 408 | } 409 | })(angular.module("ezfb", [])); -------------------------------------------------------------------------------- /build/angular-easyfb.min.js: -------------------------------------------------------------------------------- 1 | /*! angular-easyfb 2 | version: 1.6.0 3 | build date: 2019-08-20 4 | author: Robin Fan 5 | https://github.com/pc035860/angular-easyfb.git */ 6 | !function(a){function b(b,c){var d="ezfb-social-plugin-wrap",e="display: inline-block; width: 0; height: 0; overflow: hidden;",f=["fbPage","fbComments"],g=function(a){var b='';return a.wrap(b).parent()},h=function(a){var b='
';return a.wrap(b).parent()},i=function(a){return a.parent().hasClass(d)},j=function(a){return a.parent().after(a).remove(),a};a.directive(c,["ezfb","$q","$document",function(a,d,e){var k=f.indexOf(c)>=0,l=c.replace(/([a-z])([A-Z])/g,"$1-$2").toLowerCase();return{restrict:"EC",require:"?^ezfbXfbml",compile:function(c,d){return c.removeClass(l),function(c,d,e,f){function m(a){return function(){var b;n&&a===o&&(b=e.onrender,b&&c.$eval(b),n=!1,j(d))}}if(!f){var n=!1,o=0;a.$rendered().then(function(){d.addClass(l),c.$watch(function(){var a=[];return angular.forEach(b,function(b){a.push(e[b])}),a},function(b){var c;o++,n?a.XFBML.parse(d.parent()[0],m(o)):(n=!0,c=k?h:g,a.XFBML.parse(c(d)[0],m(o)))},!0)}),d.on("$destroy",function(){i(d)&&j(d)})}}}}}])}a.provider("ezfb",function(){function a(a,b){if(!angular.isObject(b))return angular.copy(a);angular.extend(a,b)}function b(a,b,c){return function(){return a.apply(b,c)}}var c={COMPLETED_REGISTRATION:"fb_mobile_complete_registration",VIEWED_CONTENT:"fb_mobile_content_view",SEARCHED:"fb_mobile_search",RATED:"fb_mobile_rate",COMPLETED_TUTORIAL:"fb_mobile_tutorial_completion",ADDED_TO_CART:"fb_mobile_add_to_cart",ADDED_TO_WISHLIST:"fb_mobile_add_to_wishlist",INITIATED_CHECKOUT:"fb_mobile_initiated_checkout",ADDED_PAYMENT_INFO:"fb_mobile_add_payment_info",ACHIEVED_LEVEL:"fb_mobile_level_achieved",UNLOCKED_ACHIEVEMENT:"fb_mobile_achievement_unlocked",SPENT_CREDITS:"fb_mobile_spent_credits"},d={CURRENCY:"fb_currency",REGISTRATION_METHOD:"fb_registration_method",CONTENT_TYPE:"fb_content_type",CONTENT_ID:"fb_content_id",SEARCH_STRING:"fb_search_string",SUCCESS:"fb_success",MAX_RATING_VALUE:"fb_max_rating_value",PAYMENT_INFO_AVAILABLE:"fb_payment_info_available",NUM_ITEMS:"fb_num_items",LEVEL:"fb_level",DESCRIPTION:"fb_description"},e=-1,f={api:[1,2,3],ui:1,getAuthResponse:e,getLoginStatus:0,login:0,logout:0,"Event.subscribe":1,"Event.unsubscribe":1,"XFBML.parse":1,"Canvas.Prefetcher.addStaticResource":e,"Canvas.Prefetcher.setCollectionMode":e,"Canvas.getPageInfo":0,"Canvas.hideFlashElement":e,"Canvas.scrollTo":e,"Canvas.setAutoGrow":e,"Canvas.setDoneLoading":0,"Canvas.setSize":e,"Canvas.setUrlHandler":0,"Canvas.showFlashElement":e,"Canvas.startTimer":e,"Canvas.stopTimer":0,"AppEvents.logEvent":e,"AppEvents.logPurchase":e,"AppEvents.activateApp":e},g="en_US",h={status:!0,cookie:!0,xfbml:!0,version:"v2.6"},i=["$window","$document","$timeout","ezfbAsyncInit","ezfbLocale",function(a,b,c,d,e){!function(a){c(function(){var b,c="facebook-jssdk",d=a.getElementsByTagName("script")[0];a.getElementById(c)||(b=a.createElement("script"),b.id=c,b.async=!0,b.src="//connect.facebook.net/"+e+"/sdk.js",d.parentNode.insertBefore(b,d))},0,!1)}(b[0]),a.fbAsyncInit=d}],j=i,k=["$window","ezfbInitParams",function(a,b){a.FB.init(b)}],l=k;return{setInitParams:function(b){a(h,b)},getInitParams:function(){return a(h)},setLocale:function(a){g=a},getLocale:function(){return g},setLoadSDKFunction:function(a){if(!angular.isArray(a)&&!angular.isFunction(a))throw new Error("Init function type error.");j=a},getLoadSDKFunction:function(){return j},setInitFunction:function(a){if(!angular.isArray(a)&&!angular.isFunction(a))throw new Error("Init function type error.");l=a},getInitFunction:function(){return l},$get:["$window","$q","$document","$parse","$rootScope","$injector","$timeout",function(i,m,n,o,p,q,r){var s,t,u,v,w,x;return v={},w=m.defer(),(h.appId||l!==k)&&w.resolve(),s=m.defer(),t=m.defer(),n[0].getElementById("fb-root")||n.find("body").append('
'),x=function(){w.promise.then(function(){if(h.xfbml){var a=function(){u.$$xfbmlRendered=!0,r(function(){t.resolve(!0)}),u.Event.unsubscribe("xfbml.render",a)};u.Event.subscribe("xfbml.render",a)}else r(function(){t.resolve(!1)});q.invoke(l,null,{ezfbInitParams:h}),u.$$ready=!0,s.resolve()})},q.invoke(j,null,{ezfbAsyncInit:x,ezfbLocale:g}),u={$$ready:!1,$$xfbmlRendered:!1,$ready:function(a){return angular.isFunction(a)&&s.promise.then(a),s.promise},$rendered:function(a){return angular.isFunction(a)&&t.promise.then(a),t.promise},init:function(b){a(h,b),w.resolve()},AppEvents:{EventNames:c,ParameterNames:d}},angular.forEach(f,function(a,c){var d=o(c);(0,d.assign)(u,function(){var f=b(function(b){var f,g;if(f=m.defer(),g=function(a){var d,e;for(d=angular.isFunction(b[a])?b[a]:angular.noop,e=function(){var a=Array.prototype.slice.call(arguments);p.$$phase?(d.apply(null,a),f.resolve.apply(f,a)):p.$apply(function(){d.apply(null,a),f.resolve.apply(f,a)})};b.length<=a;)b.push(null);var g;if("Event.subscribe"===c)g=b[0],angular.isUndefined(v[g])&&(v[g]=[]),v[g].push({original:d,wrapped:e});else if("Event.unsubscribe"===c&&(g=b[0],angular.isArray(v[g]))){var h,i,j=v[g].length;for(h=0;h
'); 234 | } 235 | 236 | // Run load SDK function 237 | ezfbAsyncInit = function () { 238 | _paramsReady.promise.then(function() { 239 | // console.log('params ready'); 240 | 241 | if (_initParams.xfbml) { 242 | // with first-time xfbml parsing parameter 243 | 244 | var onRender = function () { 245 | // console.log('on render'); 246 | _ezfb.$$xfbmlRendered = true; 247 | $timeout(function () { 248 | _initRenderReady.resolve(true); 249 | }); 250 | _ezfb.Event.unsubscribe('xfbml.render', onRender); 251 | }; 252 | _ezfb.Event.subscribe('xfbml.render', onRender); 253 | } 254 | else { 255 | // without first-time xfbml parsing parameter 256 | 257 | $timeout(function () { 258 | _initRenderReady.resolve(false); 259 | }); 260 | } 261 | 262 | // Run init function 263 | $injector.invoke(_initFunction, null, {'ezfbInitParams': _initParams}); 264 | 265 | _ezfb.$$ready = true; 266 | 267 | _initReady.resolve(); 268 | }); 269 | }; 270 | $injector.invoke(_loadSDKFunction, null, { 271 | 'ezfbAsyncInit': ezfbAsyncInit, 272 | 'ezfbLocale': _locale 273 | }); 274 | 275 | _ezfb = { 276 | $$ready: false, 277 | $$xfbmlRendered: false, 278 | 279 | // fbsdk init ready 280 | $ready: function (fn) { 281 | if (angular.isFunction(fn)) { 282 | _initReady.promise.then(fn); 283 | } 284 | return _initReady.promise; 285 | }, 286 | 287 | // intended for first xfbml parse ready if `xfbml: true` is presented 288 | // still gets resolved if `xfbml: false`, but resolved value would be `false` 289 | $rendered: function (fn) { 290 | if (angular.isFunction(fn)) { 291 | _initRenderReady.promise.then(fn); 292 | } 293 | return _initRenderReady.promise; 294 | }, 295 | 296 | init: function (params) { 297 | _config(_initParams, params); 298 | _paramsReady.resolve(); 299 | }, 300 | 301 | AppEvents: { 302 | EventNames: APP_EVENTS_EVENT_NAMES, 303 | ParameterNames: APP_EVENTS_PARAMETER_NAMES 304 | } 305 | }; 306 | 307 | /** 308 | * _ezfb initialization 309 | * 310 | * Publish FB APIs with auto-check ready state 311 | */ 312 | angular.forEach(_publishedApis, function (cbArgIndex, apiPath) { 313 | var getter = $parse(apiPath), 314 | setter = getter.assign; 315 | setter(_ezfb, function () { 316 | var apiCall = _proxy(function (args) { 317 | var dfd, replaceCallbackAt; 318 | 319 | dfd = $q.defer(); 320 | 321 | /** 322 | * Add or replce original callback function with deferred resolve 323 | * 324 | * @param {number} index expected api callback index 325 | */ 326 | replaceCallbackAt = function (index) { 327 | var func, newFunc; 328 | 329 | func = angular.isFunction(args[index]) ? args[index] : angular.noop; 330 | newFunc = function () { 331 | var funcArgs = Array.prototype.slice.call(arguments); 332 | 333 | if ($rootScope.$$phase) { 334 | // already in angularjs context 335 | func.apply(null, funcArgs); 336 | dfd.resolve.apply(dfd, funcArgs); 337 | } 338 | else { 339 | // not in angularjs context 340 | $rootScope.$apply(function () { 341 | func.apply(null, funcArgs); 342 | dfd.resolve.apply(dfd, funcArgs); 343 | }); 344 | } 345 | }; 346 | 347 | while (args.length <= index) { 348 | args.push(null); 349 | } 350 | 351 | /** 352 | * `FB.Event.unsubscribe` requires the original listener function. 353 | * Save the mapping of original->wrapped on `FB.Event.subscribe` for unsubscribing. 354 | */ 355 | var eventName; 356 | if (apiPath === 'Event.subscribe') { 357 | eventName = args[0]; 358 | if (angular.isUndefined(_savedListeners[eventName])) { 359 | _savedListeners[eventName] = []; 360 | } 361 | _savedListeners[eventName].push({ 362 | original: func, 363 | wrapped: newFunc 364 | }); 365 | } 366 | else if (apiPath === 'Event.unsubscribe') { 367 | eventName = args[0]; 368 | if (angular.isArray(_savedListeners[eventName])) { 369 | var i, subscribed, l = _savedListeners[eventName].length; 370 | for (i = 0; i < l; i++) { 371 | subscribed = _savedListeners[eventName][i]; 372 | if (subscribed.original === func) { 373 | newFunc = subscribed.wrapped; 374 | _savedListeners[eventName].splice(i, 1); 375 | break; 376 | } 377 | } 378 | } 379 | } 380 | 381 | // Replace the original one (or null) with newFunc 382 | args[index] = newFunc; 383 | }; 384 | 385 | if (cbArgIndex !== NO_CALLBACK) { 386 | if (angular.isNumber(cbArgIndex)) { 387 | /** 388 | * Constant callback argument index 389 | */ 390 | replaceCallbackAt(cbArgIndex); 391 | } 392 | else if (angular.isArray(cbArgIndex)) { 393 | /** 394 | * Multiple possible callback argument index 395 | */ 396 | var i, c; 397 | for (i = 0; i < cbArgIndex.length; i++) { 398 | c = cbArgIndex[i]; 399 | 400 | if (args.length == c || 401 | args.length == (c + 1) && angular.isFunction(args[c])) { 402 | 403 | replaceCallbackAt(c); 404 | 405 | break; 406 | } 407 | } 408 | } 409 | } 410 | 411 | /** 412 | * Apply back to original FB SDK 413 | */ 414 | var origFBFunc = getter($window.FB); 415 | if (!origFBFunc) { 416 | throw new Error("Facebook API `FB." + apiPath + "` doesn't exist."); 417 | } 418 | origFBFunc.apply($window.FB, args); 419 | 420 | return dfd.promise; 421 | }, null, [Array.prototype.slice.call(arguments)]); 422 | 423 | /** 424 | * Wrap the api function with our ready promise 425 | * 426 | * The only exception is `getAuthResponse`, which doesn't rely on a callback function to get the response 427 | */ 428 | if (apiPath === 'getAuthResponse') { 429 | if (angular.isUndefined($window.FB)) { 430 | throw new Error('`FB` is not ready.'); 431 | } 432 | return $window.FB.getAuthResponse(); 433 | } 434 | else if (cbArgIndex === NO_CALLBACK) { 435 | // Do not return promise for no-callback apis 436 | _initReady.promise.then(apiCall); 437 | } 438 | else { 439 | return _initReady.promise.then(apiCall); 440 | } 441 | }); 442 | }); 443 | 444 | return _ezfb; 445 | }] 446 | }; 447 | }) 448 | 449 | /** 450 | * @ngdoc directive 451 | * @name ng.directive:ezfbXfbml 452 | * @restrict EAC 453 | * 454 | * @description 455 | * Parse XFBML inside the directive 456 | * 457 | * @param {boolean} ezfb-xfbml Reload trigger for inside XFBML, 458 | * should keep only XFBML content inside the directive. 459 | * @param {expr} onrender Evaluated every time content xfbml gets rendered. 460 | */ 461 | .directive('ezfbXfbml', [ 462 | 'ezfb', '$parse', '$compile', '$timeout', 463 | function (ezfb, $parse, $compile, $timeout) { 464 | return { 465 | restrict: 'EAC', 466 | controller: function () { 467 | // do nothing 468 | }, 469 | compile: function (tElm, tAttrs) { 470 | var _savedHtml = tElm.html(); 471 | 472 | return function postLink(scope, iElm, iAttrs) { 473 | var _rendering = true, 474 | onrenderExp = iAttrs.onrender, 475 | onrenderHandler = function () { 476 | if (_rendering) { 477 | if (onrenderExp) { 478 | scope.$eval(onrenderExp); 479 | } 480 | 481 | _rendering = false; 482 | } 483 | }; 484 | 485 | ezfb.XFBML.parse(iElm[0], onrenderHandler); 486 | 487 | /** 488 | * The trigger 489 | */ 490 | var setter = $parse(iAttrs.ezfbXfbml).assign; 491 | scope.$watch(iAttrs.ezfbXfbml, function (val) { 492 | if (val) { 493 | _rendering = true; 494 | iElm.html(_savedHtml); 495 | 496 | $compile(iElm.contents())(scope); 497 | $timeout(function () { 498 | ezfb.XFBML.parse(iElm[0], onrenderHandler); 499 | }); 500 | 501 | // Reset the trigger if it's settable 502 | (setter || angular.noop)(scope, false); 503 | } 504 | }, true); 505 | 506 | }; 507 | } 508 | }; 509 | }]); 510 | 511 | 512 | // ref: https://developers.facebook.com/docs/plugins 513 | var _socialPluginDirectiveConfig = { 514 | 'fbLike': [ 515 | 'action', 'colorscheme', 'href', 'kidDirectedSite', 516 | 'layout', 'ref', 'share', 'showFaces', 'width' 517 | ], 518 | 'fbShareButton': [ 519 | 'href', 'layout', 'width' 520 | ], 521 | 'fbSend': [ 522 | 'colorscheme', 'href', 'kidDirectedSite', 'ref' 523 | ], 524 | 'fbPost': [ 525 | 'href', 'width' 526 | ], 527 | 'fbFollow': [ 528 | 'colorscheme', 'href', 'kidDirectedSite', 'layout', 529 | 'showFaces', 'width' 530 | ], 531 | 'fbComments': [ 532 | 'colorscheme', 'href', 'mobile', 'numPosts', 533 | 'orderBy', 'width' 534 | ], 535 | 'fbCommentsCount': [ 536 | 'href' 537 | ], 538 | 'fbActivity': [ 539 | 'action', 'appId', 'colorscheme', 'filter', 'header', 540 | 'height', 'linktarget', 'maxAge', 'recommendations', 541 | 'ref', 'site', 'width' 542 | ], 543 | 'fbRecommendations': [ 544 | 'action', 'appId', 'colorscheme', 'header', 'height', 545 | 'linktarget', 'maxAge', 'ref', 'site', 'width' 546 | ], 547 | 'fbRecommendationsBar': [ 548 | 'action', 'href', 'maxAge', 'numRecommendations', 549 | 'readTime', 'ref', 'side', 'site', 'trigger' 550 | ], 551 | 'fbLikeBox': [ 552 | 'colorscheme', 'forceWall', 'header', 'height', 553 | 'href', 'showBorder', 'showFaces', 'stream', 'width' 554 | ], 555 | 'fbFacepile': [ 556 | 'action', 'appId', 'colorscheme', 'href', 'maxRows', 557 | 'size', 'width' 558 | ], 559 | 'fbPage': [ 560 | 'href', 'width', 'height', 'hideCover', 'showFacepile', 'showPosts' 561 | ], 562 | 'fbVideo': [ 563 | 'href', 'width', 'allowfullscreen' 564 | ], 565 | 'fbAdPreview': [ 566 | 'adAccountId', 'adgroupId', 'creative', 'creativeId', 'adFormat', 'pageType', 'targeting', 'post' 567 | ], 568 | 'fbSendToMessenger': [ 569 | 'messengerAppId', 'pageId', 'ref', 'color', 'size' 570 | ], 571 | 'fbMessengermessageus': [ 572 | 'messengerAppId', 'pageId', 'color', 'size' 573 | ], 574 | 'fbLoginButton': [ 575 | 'autoLogoutLink', 'maxRows', 'onLogin', 'scope', 'size', 'showFaces', 'defaultAudience' 576 | ], 577 | 'fbCommentEmbed': [ 578 | 'href', 'width', 'includeParent' 579 | ], 580 | 'fbSave': [ 581 | 'uri' 582 | ] 583 | }; 584 | 585 | angular.forEach(_socialPluginDirectiveConfig, creatSocialPluginDirective); 586 | 587 | function creatSocialPluginDirective(availableAttrs, dirName) { 588 | var CLASS_WRAP = 'ezfb-social-plugin-wrap', 589 | STYLE_WRAP_SPAN = 'display: inline-block; width: 0; height: 0; overflow: hidden;'; 590 | 591 | // Adpative width plugins 592 | // e.g. https://developers.facebook.com/docs/plugins/page-plugin#adaptive-width 593 | var PLUGINS_WITH_ADAPTIVE_WIDTH = ['fbPage', 'fbComments']; 594 | 595 | /** 596 | * Wrap-related functions 597 | */ 598 | var _wrap = function ($elm) { 599 | var tmpl = ''; 600 | return $elm.wrap(tmpl).parent(); 601 | }, 602 | _wrapAdaptive = function ($elm) { 603 | // Plugin with adaptive width prefers the "blocky" wrapping element 604 | var tmpl = '
'; 605 | return $elm.wrap(tmpl).parent(); 606 | }, 607 | _isWrapped = function ($elm) { 608 | return $elm.parent().hasClass(CLASS_WRAP); 609 | }, 610 | _unwrap = function ($elm) { 611 | var $parent = $elm.parent(); 612 | $parent.after($elm).remove(); 613 | return $elm; 614 | }; 615 | 616 | module.directive(dirName, [ 617 | 'ezfb', '$q', '$document', 618 | function (ezfb, $q, $document) { 619 | var _withAdaptiveWidth = PLUGINS_WITH_ADAPTIVE_WIDTH.indexOf(dirName) >= 0; 620 | 621 | var _dirClassName = dirName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase(); 622 | 623 | return { 624 | restrict: 'EC', 625 | require: '?^ezfbXfbml', 626 | compile: function (tElm, tAttrs) { 627 | tElm.removeClass(_dirClassName); 628 | 629 | return function postLink(scope, iElm, iAttrs, xfbmlCtrl) { 630 | /** 631 | * For backward compatibility, skip self rendering if contained by easyfb-xfbml directive 632 | */ 633 | if (xfbmlCtrl) { 634 | return; 635 | } 636 | 637 | var rendering = false, 638 | renderId = 0; 639 | 640 | ezfb.$rendered() 641 | .then(function () { 642 | iElm.addClass(_dirClassName); 643 | 644 | scope.$watch(function () { 645 | var watchList = []; 646 | angular.forEach(availableAttrs, function (attrName) { 647 | watchList.push(iAttrs[attrName]); 648 | }); 649 | return watchList; 650 | }, function (v) { 651 | var wrapFn; 652 | 653 | renderId++; 654 | if (!rendering) { 655 | rendering = true; 656 | 657 | wrapFn = _withAdaptiveWidth ? _wrapAdaptive : _wrap; 658 | // Wrap the social plugin code for FB.XFBML.parse 659 | ezfb.XFBML.parse(wrapFn(iElm)[0], genOnRenderHandler(renderId)); 660 | } 661 | else { 662 | // Already rendering, do not wrap 663 | ezfb.XFBML.parse(iElm.parent()[0], genOnRenderHandler(renderId)); 664 | } 665 | }, true); 666 | }); 667 | 668 | 669 | // Unwrap on $destroy 670 | iElm.on('$destroy', function () { 671 | if (_isWrapped(iElm)) { 672 | _unwrap(iElm); 673 | } 674 | }); 675 | 676 | function genOnRenderHandler(id) { 677 | return function () { 678 | var onrenderExp; 679 | 680 | if (rendering && id === renderId) { 681 | onrenderExp = iAttrs.onrender; 682 | if (onrenderExp) { 683 | scope.$eval(onrenderExp); 684 | } 685 | 686 | rendering = false; 687 | _unwrap(iElm); 688 | } 689 | }; 690 | } 691 | }; 692 | } 693 | }; 694 | }]); 695 | } 696 | 697 | })(angular.module('ezfb', [])); 698 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | /** 2 | * FB SDK Loading function for testing 3 | * We don't load real FB JS SDK in. We mock it with `mockSDKApi` 4 | * 5 | * @param {function} fbAsyncInit module initialization function 6 | */ 7 | function mockSDKLoader(ezfbAsyncInit) { 8 | ezfbAsyncInit(); 9 | } 10 | 11 | /** 12 | * Trigger 'xfbml.render' event manually with `pubsub` 13 | */ 14 | function mockXFBMLrendered () { 15 | pubsub.pub('xfbml.render'); 16 | } 17 | 18 | /** 19 | * Decorate $window for mocking FB JS SDK, with given api path and method. 20 | * May accept string/array as api path. Also support object input. 21 | * 22 | * @param {mixed} apiPath path to the api call 23 | * @param {function} value must be a function if presented 24 | */ 25 | function mockSDKApi(apiPath, value) { 26 | var __toString = Object.prototype.toString; 27 | 28 | module(function ($provide) { 29 | var pathAssign = function (obj, pathStr, value) { 30 | var paths = pathStr.split(/\./); 31 | 32 | if (paths.length === 0) { 33 | return; 34 | } 35 | 36 | var path = paths.shift(); 37 | 38 | if (paths.length === 0) { 39 | obj[path] = value; 40 | return; 41 | } 42 | 43 | if (!obj[path]) { 44 | obj[path] = {}; 45 | } 46 | 47 | pathAssign(obj[path], paths.join('.'), value); 48 | }; 49 | 50 | if (typeof apiPath === 'string') { 51 | apiPath = [apiPath]; 52 | } 53 | 54 | var mockFB = { 55 | // Still required to provide an `init` function 56 | init: angular.noop, 57 | Event: { 58 | subscribe: function (name, handler) { 59 | pubsub.sub(name, handler); 60 | }, 61 | unsubscribe: function (name, handler) { 62 | pubsub.unsub(name, handler); 63 | } 64 | } 65 | }; 66 | if (__toString.call(apiPath) === '[object Object]' && !value) { 67 | // map mode 68 | angular.forEach(apiPath, function (v, p) { 69 | pathAssign(mockFB, p, v); 70 | }); 71 | } 72 | else { 73 | // array mode 74 | angular.forEach(apiPath, function (p) { 75 | pathAssign(mockFB, p, value); 76 | }); 77 | } 78 | 79 | $provide.decorator('$window', function ($delegate) { 80 | $delegate.FB = mockFB; 81 | return $delegate; 82 | }); 83 | }); 84 | } 85 | 86 | 87 | /** 88 | * Ref: https://github.com/angular/angular.js/blob/master/test/helpers/testabilityPatch.js 89 | */ 90 | function dealoc(obj) { 91 | var jqCache = angular.element.cache; 92 | if (obj) { 93 | if (angular.isElement(obj)) { 94 | cleanup(angular.element(obj)); 95 | } else { 96 | for(var key in jqCache) { 97 | var value = jqCache[key]; 98 | if (value.data && value.data.$scope == obj) { 99 | delete jqCache[key]; 100 | } 101 | } 102 | } 103 | } 104 | 105 | function cleanup(element) { 106 | element.off().removeData(); 107 | // Note: We aren't using element.contents() here. Under jQuery, element.contents() can fail 108 | // for IFRAME elements. jQuery explicitly uses (element.contentDocument || 109 | // element.contentWindow.document) and both properties are null for IFRAMES that aren't attached 110 | // to a document. 111 | var children = element[0].childNodes || []; 112 | for ( var i = 0; i < children.length; i++) { 113 | cleanup(angular.element(children[i])); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /test/karma-unit.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | files : [ 4 | 'bower_components/angular/angular.js', 5 | 'bower_components/angular-mocks/angular-mocks.js', 6 | 7 | 'src/angular-easyfb.js', 8 | 9 | 'test/pubsub.js', 10 | 'test/helper.js', 11 | 'test/unit/*.js' 12 | ], 13 | basePath: '../', 14 | frameworks: ['jasmine'], 15 | reporters: ['progress'], 16 | browsers: [/*'Chrome', 'Firefox', */'PhantomJS'], 17 | autoWatch: false, 18 | singleRun: true, 19 | colors: true 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /test/pubsub.js: -------------------------------------------------------------------------------- 1 | /*global angular*/ 2 | var pubsub = (function () { 3 | var jqLite = angular.element; 4 | var _core = jqLite(''); 5 | 6 | return { 7 | pub: function (name) { 8 | // console.log('pubsub:pub', name); 9 | _core.triggerHandler(name); 10 | }, 11 | sub: function (name, handler) { 12 | // console.log('pubsub:sub', name, handler); 13 | _core.on(name, handler); 14 | }, 15 | unsub: function (name, handler) { 16 | if (!handler || typeof handler !== 'function') { 17 | return; 18 | } 19 | _core.off(name, handler); 20 | }, 21 | clear: function () { 22 | _core.off(); 23 | } 24 | }; 25 | }()); 26 | -------------------------------------------------------------------------------- /test/unit/ezfb.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('ezfb', function () { 4 | 5 | var MODULE_NAME = 'ezfb', 6 | APP_ID = 'some fb app id', 7 | DELAY = 999999999999, 8 | DEFAULT_INIT_PARAMS = { 9 | status : true, 10 | cookie : true, 11 | xfbml : true, 12 | version : 'v2.6' 13 | }; 14 | 15 | var jqLite = angular.element; 16 | 17 | beforeEach(function(){ 18 | this.addMatchers({ 19 | toEqualData: function(expected) { 20 | return angular.equals(this.actual, expected); 21 | } 22 | }); 23 | }); 24 | 25 | beforeEach(module(MODULE_NAME)); 26 | 27 | describe('configuration phase (ezfbProvider)', function () { 28 | 29 | var loadSDKSpy, initSpy; 30 | 31 | var $rootScope; 32 | 33 | beforeEach(function () { 34 | loadSDKSpy = jasmine.createSpy('load sdk'); 35 | initSpy = jasmine.createSpy('init'); 36 | 37 | 38 | mockSDKApi('init', initSpy); 39 | }); 40 | 41 | function injectEzfb () { 42 | inject(function (ezfb, _$rootScope_) { 43 | $rootScope = _$rootScope_; 44 | }); 45 | } 46 | 47 | describe('.setLoadSDKFunction', function () { 48 | 49 | it('should be called on ezfb injection', function () { 50 | module(function (ezfbProvider) { 51 | ezfbProvider.setLoadSDKFunction(['ezfbAsyncInit', 'ezfbLocale', loadSDKSpy]); 52 | }); 53 | injectEzfb(); 54 | 55 | expect(loadSDKSpy.callCount).toEqual(1); 56 | }); 57 | 58 | it('should be able to be called with DI locals: ezfbAsyncInit, ezfbLocale', function () { 59 | module(function (ezfbProvider) { 60 | ezfbProvider.setLoadSDKFunction(['ezfbAsyncInit', 'ezfbLocale', loadSDKSpy]); 61 | }); 62 | injectEzfb(); 63 | 64 | // ezfbAsyncInit 65 | expect(typeof loadSDKSpy.mostRecentCall.args[0] === 'function').toBeTruthy(); 66 | // ezfbLocale 67 | expect(typeof loadSDKSpy.mostRecentCall.args[1] === 'string').toBeTruthy(); 68 | }); 69 | }); 70 | 71 | describe('.setInitParams', function () { 72 | 73 | beforeEach(module(function (ezfbProvider) { 74 | ezfbProvider.setLoadSDKFunction(function (ezfbAsyncInit) { 75 | mockSDKLoader(ezfbAsyncInit); 76 | loadSDKSpy(ezfbAsyncInit); 77 | }); 78 | })); 79 | 80 | it('should cause SDK to be loaded', function () { 81 | module(function (ezfbProvider) { 82 | ezfbProvider.setInitParams({ 83 | appId: APP_ID 84 | }); 85 | }); 86 | injectEzfb(); 87 | 88 | expect(loadSDKSpy.callCount).toEqual(1); 89 | expect(typeof loadSDKSpy.mostRecentCall.args[0] === 'function').toBeTruthy(); 90 | }); 91 | 92 | it('should have FB.init called with correct parameters', function () { 93 | module(function (ezfbProvider) { 94 | ezfbProvider.setInitParams({ 95 | appId: APP_ID 96 | }); 97 | }); 98 | injectEzfb(); 99 | $rootScope.$apply(); 100 | 101 | var expectedParams = angular.extend({}, DEFAULT_INIT_PARAMS, {appId: APP_ID}); 102 | 103 | expect(initSpy.callCount).toEqual(1); 104 | expect(initSpy.mostRecentCall.args[0]).toEqualData(expectedParams); 105 | }); 106 | 107 | it('should have FB.init called even SDK is loaded asynchronously', function () { 108 | module(function (ezfbProvider) { 109 | ezfbProvider.setLoadSDKFunction([ 110 | 'ezfbAsyncInit', '$timeout', 111 | function (ezfbAsyncInit, $timeout) { 112 | // Delay a bit 113 | $timeout(function () { 114 | ezfbAsyncInit(); 115 | }, DELAY); 116 | }]); 117 | 118 | ezfbProvider.setInitParams({ 119 | appId: APP_ID 120 | }); 121 | }); 122 | inject(function (ezfb, $rootScope, $timeout) { 123 | $rootScope.$apply(); 124 | $timeout.flush(); 125 | 126 | expect(initSpy.callCount).toEqual(1); 127 | }); 128 | }); 129 | }); 130 | 131 | describe('.setLocale', function () { 132 | beforeEach(module(function (ezfbProvider) { 133 | ezfbProvider.setLoadSDKFunction(function (ezfbLocale) { 134 | loadSDKSpy(ezfbLocale); 135 | }); 136 | ezfbProvider.setInitParams({ 137 | appId: APP_ID 138 | }); 139 | })); 140 | 141 | it('should cause SDK to be loaded with given locale', function () { 142 | var LOCALE = 'zhTW'; 143 | 144 | module(function (ezfbProvider) { 145 | ezfbProvider.setLocale(LOCALE); 146 | }); 147 | injectEzfb(); 148 | 149 | expect(loadSDKSpy.mostRecentCall.args[0]).toEqual(LOCALE); 150 | }); 151 | }); 152 | 153 | describe('.setInitFunction', function () { 154 | var customInitSpy; 155 | 156 | beforeEach(module(function (ezfbProvider) { 157 | ezfbProvider.setLoadSDKFunction(mockSDKLoader); 158 | ezfbProvider.setInitParams({ 159 | appId: APP_ID 160 | }); 161 | })); 162 | 163 | beforeEach(function () { 164 | customInitSpy = jasmine.createSpy('custom init function'); 165 | }); 166 | 167 | it('should call custom init function on init', function () { 168 | module(function (ezfbProvider) { 169 | ezfbProvider.setInitFunction(customInitSpy); 170 | }); 171 | injectEzfb(); 172 | $rootScope.$apply(); 173 | 174 | expect(customInitSpy.callCount).toEqual(1); 175 | }); 176 | 177 | it('should be able to be called with DI local: ezfbInitParams', function () { 178 | module(function (ezfbProvider) { 179 | ezfbProvider.setInitFunction(['ezfbInitParams', customInitSpy]); 180 | }); 181 | injectEzfb(); 182 | $rootScope.$apply(); 183 | 184 | var expectedParams = angular.extend({}, DEFAULT_INIT_PARAMS, {appId: APP_ID}); 185 | expect(customInitSpy.mostRecentCall.args[0]).toEqualData(expectedParams); 186 | }); 187 | }); 188 | 189 | }); 190 | 191 | describe('the instance', function () { 192 | var API_RESPONSE = { 193 | angular: 1, 194 | easyfb: 2 195 | }; 196 | 197 | var fbMockCallSpy, fbMockCallbackSpy, fbMockPromiseSpy; 198 | 199 | beforeEach(function () { 200 | fbMockCallSpy = jasmine.createSpy('fb api call'); 201 | fbMockCallbackSpy = jasmine.createSpy('fb api callback'); 202 | fbMockPromiseSpy = jasmine.createSpy('fb api promise'); 203 | 204 | module(function (ezfbProvider) { 205 | ezfbProvider.setLoadSDKFunction(mockSDKLoader); 206 | }); 207 | }); 208 | 209 | describe('"$" methods', function () { 210 | 211 | var ezfb, $rootScope; 212 | 213 | beforeEach(function () { 214 | mockSDKApi(); 215 | 216 | inject(function (_ezfb_, _$rootScope_) { 217 | ezfb = _ezfb_; 218 | $rootScope = _$rootScope_; 219 | }); 220 | }); 221 | 222 | describe('.$ready', function () { 223 | it('callback should run when FB instance is ready', function () { 224 | ezfb.$ready(fbMockCallbackSpy); 225 | 226 | ezfb.init({ 227 | appId: APP_ID 228 | }); 229 | 230 | expect(fbMockCallbackSpy.callCount).toEqual(0); 231 | 232 | $rootScope.$apply(); 233 | 234 | expect(fbMockCallbackSpy.callCount).toEqual(1); 235 | }); 236 | 237 | it('promise should be resolved when FB instance is ready', function () { 238 | ezfb.$ready().then(fbMockPromiseSpy); 239 | 240 | ezfb.init({ 241 | appId: APP_ID 242 | }); 243 | 244 | expect(fbMockPromiseSpy.callCount).toEqual(0); 245 | 246 | $rootScope.$apply(); 247 | 248 | expect(fbMockPromiseSpy.callCount).toEqual(1); 249 | }); 250 | }); 251 | 252 | describe('.$rendered', function () { 253 | var $timeout; 254 | 255 | beforeEach(inject(function (_$timeout_) { 256 | $timeout = _$timeout_; 257 | })); 258 | 259 | it('callback should run when auto XFBML parsing is done', function () { 260 | ezfb.$rendered(fbMockCallbackSpy); 261 | 262 | ezfb.init({ 263 | appId: APP_ID 264 | }); 265 | 266 | $timeout(function () { 267 | mockXFBMLrendered(); 268 | }, 100); 269 | 270 | expect(fbMockCallbackSpy.callCount).toEqual(0); 271 | 272 | // $rootScope.$apply(); 273 | $timeout.flush(100); 274 | 275 | expect(fbMockCallbackSpy).toHaveBeenCalledWith(true); 276 | }); 277 | 278 | it('promise should be resolved when auto XFBML parsing is done', function () { 279 | ezfb.$rendered().then(fbMockPromiseSpy); 280 | 281 | ezfb.init({ 282 | appId: APP_ID 283 | }); 284 | 285 | $timeout(function () { 286 | mockXFBMLrendered(); 287 | }, 100); 288 | 289 | expect(fbMockPromiseSpy.callCount).toEqual(0); 290 | 291 | // $rootScope.$apply(); 292 | $timeout.flush(100); 293 | 294 | expect(fbMockPromiseSpy).toHaveBeenCalledWith(true); 295 | }); 296 | 297 | it('callback should be called with `false` on parameter `xfbml: false`', function () { 298 | ezfb.$rendered(fbMockCallbackSpy); 299 | 300 | ezfb.init({ 301 | appId: APP_ID, 302 | xfbml: false 303 | }); 304 | 305 | expect(fbMockCallbackSpy.callCount).toEqual(0); 306 | 307 | $timeout.flush(); 308 | 309 | expect(fbMockCallbackSpy).toHaveBeenCalledWith(false); 310 | }); 311 | 312 | it('promise should be resolved with `false` on parameter `xfbml: false`', function () { 313 | ezfb.$rendered().then(fbMockPromiseSpy); 314 | 315 | ezfb.init({ 316 | appId: APP_ID, 317 | xfbml: false 318 | }); 319 | 320 | expect(fbMockPromiseSpy.callCount).toEqual(0); 321 | 322 | $timeout.flush(); 323 | 324 | expect(fbMockPromiseSpy).toHaveBeenCalledWith(false); 325 | }); 326 | }); 327 | 328 | }); 329 | 330 | describe('.init', function () { 331 | /** 332 | * Ref: https://developers.facebook.com/docs/javascript/reference/FB.init 333 | */ 334 | 335 | var ezfb, $rootScope; 336 | 337 | beforeEach(function () { 338 | mockSDKApi('init', function () { 339 | var args = [].slice.call(arguments); 340 | 341 | fbMockCallSpy.apply(jasmine, args); 342 | }); 343 | 344 | inject(function (_ezfb_, _$rootScope_) { 345 | ezfb = _ezfb_; 346 | $rootScope = _$rootScope_; 347 | }); 348 | }); 349 | 350 | it('should call FB.init with correct parameters', function () { 351 | ezfb.init({ 352 | appId: APP_ID 353 | }); 354 | 355 | $rootScope.$apply(); 356 | 357 | expect(fbMockCallSpy.callCount).toEqual(1); 358 | expect(fbMockCallSpy.mostRecentCall.args[0]).toEqual( 359 | angular.extend(DEFAULT_INIT_PARAMS, { 360 | appId: APP_ID 361 | }) 362 | ); 363 | }); 364 | }); 365 | 366 | describe('.api', function () { 367 | /** 368 | * Ref: https://developers.facebook.com/docs/javascript/reference/FB.api 369 | */ 370 | 371 | var ezfb, $rootScope; 372 | 373 | beforeEach(function () { 374 | mockSDKApi('api', function () { 375 | var args = [].slice.call(arguments); 376 | 377 | fbMockCallSpy.apply(jasmine, args); 378 | 379 | if (typeof args[1] === 'function') { 380 | args[1](API_RESPONSE); 381 | } 382 | else if (typeof args[2] === 'function') { 383 | args[2](API_RESPONSE); 384 | } 385 | else if (typeof args[3] === 'function') { 386 | args[3](API_RESPONSE); 387 | } 388 | }); 389 | 390 | inject(function (_ezfb_, _$rootScope_) { 391 | ezfb = _ezfb_; 392 | $rootScope = _$rootScope_; 393 | }); 394 | }); 395 | 396 | it('should call FB.api', function () { 397 | ezfb.init({ 398 | appId: APP_ID 399 | }); 400 | 401 | ezfb.api('/me'); 402 | ezfb.api('/me', null); 403 | ezfb.api('/me', angular.noop); 404 | $rootScope.$apply(); 405 | 406 | expect(fbMockCallSpy.callCount).toEqual(3); 407 | }); 408 | 409 | it('should call FB.api after FB.init is called', function () { 410 | inject(function ($timeout) { 411 | ezfb.api('/me'); 412 | 413 | $timeout(function () { 414 | expect(fbMockCallSpy.callCount).toEqual(0); 415 | 416 | ezfb.init({ 417 | appId: APP_ID 418 | }); 419 | }, DELAY); 420 | 421 | $timeout.flush(); 422 | 423 | expect(fbMockCallSpy.callCount).toEqual(1); 424 | }); 425 | }); 426 | 427 | it('should trigger callbacks under different arguments situation with correct response', function () { 428 | ezfb.init({ 429 | appId: APP_ID 430 | }); 431 | 432 | ezfb.api('/me', fbMockCallbackSpy); 433 | $rootScope.$apply(); 434 | 435 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 436 | 437 | 438 | ezfb.api('/me', {fields: 'last_name'}, fbMockCallbackSpy); 439 | $rootScope.$apply(); 440 | 441 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 442 | 443 | 444 | ezfb.api('/me/feed', 'post', { message: 'post something' }, fbMockCallbackSpy); 445 | $rootScope.$apply(); 446 | 447 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 448 | 449 | 450 | expect(fbMockCallbackSpy.callCount).toEqual(3); 451 | }); 452 | 453 | it('should trigger promise under different arguments situation with correct response', function () { 454 | ezfb.init({ 455 | appId: APP_ID 456 | }); 457 | 458 | ezfb.api('/me').then(fbMockPromiseSpy); 459 | $rootScope.$apply(); 460 | 461 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 462 | 463 | 464 | ezfb.api('/me', {fields: 'last_name'}).then(fbMockPromiseSpy); 465 | $rootScope.$apply(); 466 | 467 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 468 | 469 | 470 | ezfb.api('/me/feed', 'post', { message: 'post something' }).then(fbMockPromiseSpy); 471 | $rootScope.$apply(); 472 | 473 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 474 | 475 | 476 | expect(fbMockPromiseSpy.callCount).toEqual(3); 477 | }); 478 | 479 | it('should trigger both callback and promise with correct response', function () { 480 | ezfb.init({ 481 | appId: APP_ID 482 | }); 483 | 484 | ezfb.api('/me', fbMockCallbackSpy).then(fbMockPromiseSpy); 485 | $rootScope.$apply(); 486 | 487 | expect(fbMockCallbackSpy.callCount).toEqual(1); 488 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 489 | expect(fbMockPromiseSpy.callCount).toEqual(1); 490 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 491 | }); 492 | }); 493 | 494 | describe('.ui', function () { 495 | /** 496 | * Ref: https://developers.facebook.com/docs/javascript/reference/FB.ui 497 | */ 498 | 499 | var ezfb, $rootScope; 500 | 501 | var UI_PARAMS = {war: 1, machine: 2, rox: 3}; 502 | 503 | beforeEach(function () { 504 | mockSDKApi('ui', function () { 505 | var args = [].slice.call(arguments); 506 | 507 | fbMockCallSpy.apply(jasmine, args); 508 | 509 | if (typeof args[1] === 'function') { 510 | args[1](API_RESPONSE); 511 | } 512 | }); 513 | 514 | inject(function (_ezfb_, _$rootScope_) { 515 | ezfb = _ezfb_; 516 | $rootScope = _$rootScope_; 517 | }); 518 | 519 | ezfb.init({ 520 | appId: APP_ID 521 | }); 522 | }); 523 | 524 | it('should call FB.ui', function () { 525 | ezfb.ui(UI_PARAMS); 526 | ezfb.ui(UI_PARAMS, null); 527 | ezfb.ui(UI_PARAMS, angular.noop); 528 | $rootScope.$apply(); 529 | 530 | expect(fbMockCallSpy.callCount).toEqual(3); 531 | }); 532 | 533 | it('should trigger callback with correct response', function () { 534 | ezfb.ui(UI_PARAMS, fbMockCallbackSpy); 535 | $rootScope.$apply(); 536 | 537 | expect(fbMockCallbackSpy.callCount).toEqual(1); 538 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 539 | }); 540 | 541 | it('should trigger promise with correct response ', function () { 542 | ezfb.ui(UI_PARAMS).then(fbMockPromiseSpy); 543 | $rootScope.$apply(); 544 | 545 | expect(fbMockPromiseSpy.callCount).toEqual(1); 546 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 547 | }); 548 | 549 | it('should trigger both callback and promise with correct response', function () { 550 | ezfb.ui(UI_PARAMS, fbMockCallbackSpy).then(fbMockPromiseSpy); 551 | $rootScope.$apply(); 552 | 553 | expect(fbMockCallbackSpy.callCount).toEqual(1); 554 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 555 | expect(fbMockPromiseSpy.callCount).toEqual(1); 556 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 557 | }); 558 | }); 559 | 560 | describe('.getAuthResponse', function () { 561 | /** 562 | * Ref: https://developers.facebook.com/docs/reference/javascript/FB.getAuthResponse 563 | */ 564 | 565 | var ezfb, $rootScope; 566 | 567 | beforeEach(function () { 568 | mockSDKApi('getAuthResponse', function () { 569 | var args = [].slice.call(arguments); 570 | 571 | fbMockCallSpy.apply(jasmine, args); 572 | 573 | return API_RESPONSE; 574 | }); 575 | 576 | inject(function (_ezfb_, _$rootScope_) { 577 | ezfb = _ezfb_; 578 | $rootScope = _$rootScope_; 579 | }); 580 | 581 | ezfb.init({ 582 | appId: APP_ID 583 | }); 584 | }); 585 | 586 | it('should call FB.getAuthResponse', function () { 587 | ezfb.getAuthResponse(); 588 | 589 | expect(fbMockCallSpy.callCount).toEqual(1); 590 | }); 591 | 592 | it('should retrieve synchronous response', function () { 593 | expect(ezfb.getAuthResponse()).toEqual(API_RESPONSE); 594 | }); 595 | }); 596 | 597 | 598 | /** 599 | * Ref: 600 | * https://developers.facebook.com/docs/reference/javascript/FB.getLoginStatus 601 | * https://developers.facebook.com/docs/reference/javascript/FB.login 602 | * https://developers.facebook.com/docs/reference/javascript/FB.logout 603 | */ 604 | angular.forEach([ 605 | 'getLoginStatus', 'login', 'logout' 606 | ], function (apiName) { 607 | 608 | describe('.' + apiName, function () { 609 | 610 | var ezfb, $rootScope; 611 | 612 | beforeEach(function () { 613 | mockSDKApi(apiName, function () { 614 | var args = [].slice.call(arguments); 615 | 616 | fbMockCallSpy.apply(jasmine, args); 617 | 618 | if (typeof args[0] === 'function') { 619 | args[0]({ 620 | res: apiName 621 | }); 622 | } 623 | }); 624 | 625 | inject(function (_ezfb_, _$rootScope_) { 626 | ezfb = _ezfb_; 627 | $rootScope = _$rootScope_; 628 | }); 629 | 630 | ezfb.init({ 631 | appId: APP_ID 632 | }); 633 | }); 634 | 635 | it('should call FB.' + apiName, function () { 636 | ezfb[apiName](); 637 | $rootScope.$apply(); 638 | 639 | expect(fbMockCallSpy.callCount).toEqual(1); 640 | }); 641 | 642 | it('should trigger callback with correct response', function () { 643 | ezfb[apiName](fbMockCallbackSpy); 644 | $rootScope.$apply(); 645 | 646 | expect(fbMockCallbackSpy.callCount).toEqual(1); 647 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqualData({ 648 | res: apiName 649 | }); 650 | }); 651 | 652 | it('should trigger promise with correct response', function () { 653 | ezfb[apiName]().then(fbMockPromiseSpy); 654 | $rootScope.$apply(); 655 | 656 | expect(fbMockPromiseSpy.callCount).toEqual(1); 657 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqualData({ 658 | res: apiName 659 | }); 660 | }); 661 | 662 | it('should trigger both callback and promise with correct response', function () { 663 | ezfb[apiName](fbMockCallbackSpy).then(fbMockPromiseSpy); 664 | $rootScope.$apply(); 665 | 666 | expect(fbMockCallbackSpy.callCount).toEqual(1); 667 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqualData({ 668 | res: apiName 669 | }); 670 | expect(fbMockPromiseSpy.callCount).toEqual(1); 671 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqualData({ 672 | res: apiName 673 | }); 674 | }); 675 | }); 676 | 677 | }); 678 | 679 | 680 | describe('.XFBML.parse', function () { 681 | var ezfb, $rootScope, elm; 682 | 683 | beforeEach(function () { 684 | mockSDKApi('XFBML.parse', function () { 685 | var args = [].slice.call(arguments); 686 | 687 | fbMockCallSpy.apply(jasmine, args); 688 | 689 | if (typeof args[1] === 'function') { 690 | args[1](API_RESPONSE); 691 | } 692 | }); 693 | 694 | inject(function (_ezfb_, _$rootScope_) { 695 | ezfb = _ezfb_; 696 | $rootScope = _$rootScope_; 697 | }); 698 | 699 | ezfb.init({ 700 | appId: APP_ID 701 | }); 702 | 703 | elm = jqLite('
')[0]; 704 | }); 705 | 706 | it('should call FB.XFBML.parse', function () { 707 | ezfb.XFBML.parse(); 708 | $rootScope.$apply(); 709 | 710 | expect(fbMockCallSpy.callCount).toEqual(1); 711 | 712 | ezfb.XFBML.parse(elm); 713 | $rootScope.$apply(); 714 | 715 | expect(fbMockCallSpy.callCount).toEqual(2); 716 | }); 717 | 718 | it('should trigger callback with correct response', function () { 719 | ezfb.XFBML.parse(elm, fbMockCallbackSpy); 720 | $rootScope.$apply(); 721 | 722 | expect(fbMockCallbackSpy.callCount).toEqual(1); 723 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 724 | }); 725 | 726 | it('should trigger promise with correct response', function () { 727 | ezfb.XFBML.parse(elm).then(fbMockPromiseSpy); 728 | $rootScope.$apply(); 729 | 730 | expect(fbMockPromiseSpy.callCount).toEqual(1); 731 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 732 | }); 733 | 734 | it('should trigger both callback and promise with correct response', function () { 735 | ezfb.XFBML.parse(elm, fbMockCallbackSpy).then(fbMockPromiseSpy); 736 | $rootScope.$apply(); 737 | 738 | expect(fbMockCallbackSpy.callCount).toEqual(1); 739 | expect(fbMockCallbackSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 740 | expect(fbMockPromiseSpy.callCount).toEqual(1); 741 | expect(fbMockPromiseSpy.mostRecentCall.args[0]).toEqual(API_RESPONSE); 742 | }); 743 | }); 744 | 745 | describe('.Event', function () { 746 | /** 747 | * Ref: 748 | * https://developers.facebook.com/docs/reference/javascript/FB.Event.subscribe 749 | * https://developers.facebook.com/docs/reference/javascript/FB.Event.unsubscribe 750 | */ 751 | 752 | var EVENT_NAME = 'edge.create'; 753 | 754 | var ezfb, $rootScope; 755 | 756 | var subSpy, subHandlerSpy, unsubSpy, subPromiseSpy; 757 | 758 | beforeEach(function () { 759 | subSpy = jasmine.createSpy('.Event.subscribe call'); 760 | subHandlerSpy = jasmine.createSpy('.Event.subscribe handler call'); 761 | subPromiseSpy = jasmine.createSpy('.Event.subscribe promise call'); 762 | unsubSpy = jasmine.createSpy('.Event.unsubscribe call'); 763 | 764 | mockSDKApi({ 765 | 'Event.subscribe': function (name, handler) { 766 | pubsub.sub(name, handler); 767 | subSpy(name, handler); 768 | }, 769 | 'Event.unsubscribe': function (name, handler) { 770 | pubsub.unsub(name, handler); 771 | unsubSpy(name, handler); 772 | } 773 | }); 774 | 775 | inject(function (_ezfb_, _$rootScope_) { 776 | ezfb = _ezfb_; 777 | $rootScope = _$rootScope_; 778 | }); 779 | 780 | ezfb.init({ 781 | appId: APP_ID 782 | }); 783 | }); 784 | 785 | afterEach(function () { 786 | pubsub.clear(); 787 | }); 788 | 789 | describe('.subscribe', function () { 790 | it('should call FB.Event.subscribe', function () { 791 | inject(function ($timeout) { 792 | ezfb.$rendered() 793 | .then(function () { 794 | ezfb.Event.subscribe(EVENT_NAME); 795 | }); 796 | 797 | $rootScope.$apply(); 798 | 799 | mockXFBMLrendered(); 800 | $timeout.flush(); 801 | 802 | expect(subSpy.mostRecentCall.args[0]).toEqual(EVENT_NAME); 803 | }); 804 | }); 805 | 806 | it('should trigger handler on event takes place', function () { 807 | ezfb.Event.subscribe(EVENT_NAME, subHandlerSpy); 808 | $rootScope.$apply(); 809 | 810 | pubsub.pub(EVENT_NAME); 811 | expect(subHandlerSpy.callCount).toEqual(1); 812 | }); 813 | 814 | it('should trigger promise on event takes place', function () { 815 | ezfb.Event.subscribe(EVENT_NAME).then(subPromiseSpy); 816 | $rootScope.$apply(); 817 | 818 | pubsub.pub(EVENT_NAME); 819 | expect(subPromiseSpy.callCount).toEqual(1); 820 | }); 821 | 822 | it('should trigger both handler and promise on event takes place', function () { 823 | ezfb.Event.subscribe(EVENT_NAME, subHandlerSpy).then(subPromiseSpy); 824 | $rootScope.$apply(); 825 | 826 | pubsub.pub(EVENT_NAME); 827 | expect(subHandlerSpy.callCount).toEqual(1); 828 | expect(subPromiseSpy.callCount).toEqual(1); 829 | }); 830 | 831 | it('should only trigger corresponding handler on event takes place', function () { 832 | var aHandler = jasmine.createSpy('a'), 833 | bHandler = jasmine.createSpy('b'); 834 | 835 | ezfb.Event.subscribe('a', aHandler); 836 | ezfb.Event.subscribe('b', bHandler); 837 | $rootScope.$apply(); 838 | 839 | pubsub.pub('a'); 840 | expect(aHandler.callCount).toEqual(1); 841 | expect(bHandler.callCount).toEqual(0); 842 | 843 | pubsub.pub('b'); 844 | expect(aHandler.callCount).toEqual(1); 845 | expect(bHandler.callCount).toEqual(1); 846 | }); 847 | }); 848 | 849 | describe('.unsubscribe', function () { 850 | it('should call FB.Event.unsubscribe', function () { 851 | ezfb.Event.unsubscribe(EVENT_NAME); 852 | $rootScope.$apply(); 853 | 854 | expect(unsubSpy.callCount).toEqual(1); 855 | }); 856 | 857 | it('should trigger both handler and promise on event takes place if called without specifying handler or a different handler', function () { 858 | ezfb.Event.subscribe(EVENT_NAME, subHandlerSpy).then(subPromiseSpy); 859 | ezfb.Event.unsubscribe(EVENT_NAME); 860 | ezfb.Event.unsubscribe(EVENT_NAME, angular.noop); 861 | $rootScope.$apply(); 862 | 863 | pubsub.pub(EVENT_NAME); 864 | expect(subHandlerSpy.callCount).toEqual(1); 865 | expect(subPromiseSpy.callCount).toEqual(1); 866 | }); 867 | 868 | it('should not trigger either handler or promise after being called correctly', function () { 869 | ezfb.Event.subscribe(EVENT_NAME, subHandlerSpy).then(subPromiseSpy); 870 | ezfb.Event.unsubscribe(EVENT_NAME, subHandlerSpy); 871 | $rootScope.$apply(); 872 | 873 | pubsub.pub(EVENT_NAME); 874 | expect(subHandlerSpy.callCount).toEqual(0); 875 | expect(subPromiseSpy.callCount).toEqual(0); 876 | }); 877 | }); 878 | 879 | }); 880 | 881 | describe('.AppEvents', function () { 882 | /** 883 | * Ref: 884 | * https://developers.facebook.com/docs/canvas/appevents 885 | */ 886 | 887 | // FB.AppEvents.EventNames 888 | var EVENTS = [ 889 | 'ACHIEVED_LEVEL', 'ADDED_PAYMENT_INFO', 890 | 'ADDED_TO_CART', 'ADDED_TO_WISHLIST', 891 | 'COMPLETED_REGISTRATION', 'COMPLETED_TUTORIAL', 892 | 'INITIATED_CHECKOUT', 'RATED', 'SEARCHED', 893 | 'SPENT_CREDITS', 'UNLOCKED_ACHIEVEMENT', 894 | 'VIEWED_CONTENT' 895 | ], 896 | // FB.AppEvents.ParameterNames 897 | PARAMS = [ 898 | 'CONTENT_ID', 'CONTENT_TYPE', 'CURRENCY', 899 | 'DESCRIPTION', 'LEVEL', 'MAX_RATING_VALUE', 900 | 'NUM_ITEMS', 'PAYMENT_INFO_AVAILABLE', 901 | 'REGISTRATION_METHOD', 'SEARCH_STRING', 902 | 'SUCCESS' 903 | ]; 904 | 905 | 906 | var ezfb, $rootScope; 907 | 908 | describe('.EventNames', function () { 909 | beforeEach(function () { 910 | inject(function (_ezfb_, _$rootScope_) { 911 | ezfb = _ezfb_; 912 | $rootScope = _$rootScope_; 913 | }); 914 | 915 | ezfb.init({ 916 | appId: APP_ID 917 | }); 918 | }); 919 | 920 | it('should exist', function () { 921 | expect(ezfb.AppEvents.EventNames).not.toBeUndefined(); 922 | }); 923 | 924 | angular.forEach(EVENTS, function (name) { 925 | it('should have name `'+ name +'`', function () { 926 | expect(ezfb.AppEvents.EventNames[name]).not.toBeUndefined(); 927 | }); 928 | }); 929 | }); 930 | 931 | describe('.ParameterNames', function () { 932 | beforeEach(function () { 933 | inject(function (_ezfb_, _$rootScope_) { 934 | ezfb = _ezfb_; 935 | $rootScope = _$rootScope_; 936 | }); 937 | 938 | ezfb.init({ 939 | appId: APP_ID 940 | }); 941 | }); 942 | 943 | it('should exist', function () { 944 | expect(ezfb.AppEvents.ParameterNames).not.toBeUndefined(); 945 | }); 946 | 947 | angular.forEach(PARAMS, function (name) { 948 | it('should have name `'+ name +'`', function () { 949 | expect(ezfb.AppEvents.ParameterNames[name]).not.toBeUndefined(); 950 | }); 951 | }); 952 | }); 953 | 954 | angular.forEach([ 955 | 'activateApp', 'logEvent', 'logPurchase' 956 | ], function (apiName) { 957 | describe('.'+ apiName, function () { 958 | beforeEach(function () { 959 | // They don't have callback 960 | mockSDKApi('AppEvents.'+ apiName, function () { 961 | var args = [].slice.call(arguments); 962 | 963 | fbMockCallSpy.apply(jasmine, args); 964 | }); 965 | 966 | inject(function (_ezfb_, _$rootScope_) { 967 | ezfb = _ezfb_; 968 | $rootScope = _$rootScope_; 969 | }); 970 | 971 | ezfb.init({ 972 | appId: APP_ID 973 | }); 974 | }); 975 | 976 | it('should exists', function () { 977 | expect(ezfb.AppEvents[apiName]).not.toBeUndefined(); 978 | }); 979 | 980 | it('should call `FB.AppEvents.'+ apiName +'` with correct parameters', function () { 981 | var ARGS; 982 | 983 | switch (apiName) { 984 | case 'activateApp': 985 | ARGS = []; 986 | break; 987 | case 'logEvent': 988 | ARGS = ['eventName', 0.1, {'parameters': 1}]; 989 | break; 990 | case 'logPurchase': 991 | ARGS = [0.99, 'TWD', {'parameters': 1}]; 992 | break; 993 | } 994 | 995 | ezfb.AppEvents[apiName].apply(ezfb, ARGS); 996 | $rootScope.$apply(); 997 | 998 | expect(fbMockCallSpy.callCount).toEqual(1); 999 | expect(fbMockCallSpy.mostRecentCall.args).toEqualData(ARGS); 1000 | }); 1001 | }); 1002 | }); 1003 | }); 1004 | 1005 | // TODO: 1006 | // Canvas.* APIs? 1007 | }); 1008 | 1009 | }); 1010 | -------------------------------------------------------------------------------- /test/unit/pubsub.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | describe('pubsub service with jqLite', function () { 4 | var EVENT_NAME = 'sweetjs'; 5 | 6 | var handlerSpy; 7 | 8 | beforeEach(function () { 9 | handlerSpy = jasmine.createSpy('pubsub sub handler'); 10 | }); 11 | 12 | afterEach(function () { 13 | pubsub.clear(); 14 | }); 15 | 16 | it('should trigger handler', function () { 17 | pubsub.sub(EVENT_NAME, handlerSpy); 18 | pubsub.pub(EVENT_NAME); 19 | 20 | expect(handlerSpy.callCount).toEqual(1); 21 | }); 22 | 23 | it('should not trigger handler if sub after pub', function () { 24 | pubsub.pub(EVENT_NAME); 25 | pubsub.sub(EVENT_NAME, handlerSpy); 26 | 27 | expect(handlerSpy.callCount).toEqual(0); 28 | }); 29 | 30 | it('should not trigger handler after unsub', function () { 31 | pubsub.sub(EVENT_NAME, handlerSpy); 32 | pubsub.unsub(EVENT_NAME, handlerSpy); 33 | pubsub.pub(EVENT_NAME); 34 | 35 | expect(handlerSpy.callCount).toEqual(0); 36 | }); 37 | 38 | it('should trigger handler if unsub with a different handler', function () { 39 | pubsub.sub(EVENT_NAME, handlerSpy); 40 | pubsub.unsub(EVENT_NAME, angular.noop); 41 | pubsub.pub(EVENT_NAME); 42 | 43 | expect(handlerSpy.callCount).toEqual(1); 44 | }); 45 | 46 | it('should trigger handler if unsub without specifying handler', function () { 47 | pubsub.sub(EVENT_NAME, handlerSpy); 48 | pubsub.unsub(EVENT_NAME); 49 | pubsub.pub(EVENT_NAME); 50 | 51 | expect(handlerSpy.callCount).toEqual(1); 52 | }); 53 | 54 | it('should not trigger handler after clear', function () { 55 | pubsub.sub(EVENT_NAME, handlerSpy); 56 | pubsub.clear(); 57 | pubsub.pub(EVENT_NAME); 58 | 59 | expect(handlerSpy.callCount).toEqual(0); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/unit/social-plugin-directive.spec.js: -------------------------------------------------------------------------------- 1 | /*jshint undef:false, multistr:true*/ 2 | describe('social plugin directive', function () { 3 | 'use strict'; 4 | 5 | var MODULE_NAME = 'ezfb', 6 | APP_ID = 'some fb app id', 7 | DIRECTIVES_CONFIG = { 8 | 'fb:like': ['href', 'kid_directed_site'], 9 | 'fb:share-button': ['href', 'layout'], 10 | 'fb:send': ['href', 'kid_directed_site'], 11 | 'fb:post': ['href', 'width'], 12 | 'fb:follow': ['href', 'kid_directed_site'], 13 | 'fb:comments': ['href', 'width'], 14 | 'fb:comments-count': ['href'], 15 | 'fb:activity': ['site', 'app_id'], 16 | 'fb:recommendations': ['site', 'app_id'], 17 | 'fb:recommendations-bar': ['href', 'site'], 18 | 'fb:like-box': ['href', 'show_faces'], 19 | 'fb:facepile': ['href', 'app_id'], 20 | 'fb:page': ['href', 'show_facepile'], 21 | 'fb:video': ['href', 'width'], 22 | 'fb:ad-preview': ['creative', 'ad_format'], 23 | 'fb:send-to-messenger': ['messenger_app_id', 'page_id'], 24 | 'fb:messengermessageus': ['messenger_app_id', 'page_id'], 25 | 'fb:login-button': ['scope', 'size'], 26 | 'fb:comment-embed': ['href', 'width'], 27 | 'fb:save': ['uri'] 28 | }, 29 | INTERESTED_ATTRS = [ 30 | 'href', 'kid_directed_site', 'layout', 'width', 31 | 'site', 'show_faces', 'app_id', 'show_facepile', 32 | 'creative', 'ad_format', 'messenger_app_id', 'page_id', 33 | 'scope', 'size', 'uri' 34 | ], 35 | HELLO_RENDER_DELAY = 20, 36 | PARSE_DELAY_BY = 100; 37 | 38 | var jqLite = angular.element; 39 | 40 | var xfbmlParseSpy, onrenderSpy, xfbmlParseSpyExtra; 41 | 42 | var $scope, element, $rootScope; 43 | 44 | var $compile, $timeout, $log; 45 | 46 | beforeEach(function () { 47 | xfbmlParseSpy = jasmine.createSpy('FB.XFBML.parse call'); 48 | onrenderSpy = jasmine.createSpy('directive onrender'); 49 | xfbmlParseSpyExtra = jasmine.createSpy('FB.XFBML.parse call extra info') 50 | }); 51 | 52 | beforeEach(function () { 53 | mockSDKApi(); 54 | }); 55 | 56 | beforeEach(module(MODULE_NAME, function (ezfbProvider) { 57 | ezfbProvider.setLoadSDKFunction(function (ezfbAsyncInit) { 58 | ezfbAsyncInit(); 59 | }); 60 | ezfbProvider.setInitParams({ 61 | appId: APP_ID 62 | }); 63 | })); 64 | 65 | beforeEach(module(function ($provide) { 66 | $provide.decorator('ezfb', function ($delegate, $log) { 67 | var callCount = 1; 68 | 69 | /** 70 | * Mock ezfb.XFBML.parse 71 | */ 72 | $delegate.XFBML.parse = function (elm, callback) { 73 | /** 74 | * Makes different $timeout delay for each 75 | * @return {[type]} [description] 76 | */ 77 | $timeout(function () { 78 | (callback || angular.noop)(); 79 | }, PARSE_DELAY_BY * callCount); 80 | 81 | callCount++; 82 | 83 | var childTagName = jqLite(elm).children()[0].tagName; 84 | xfbmlParseSpy(elm, callback); 85 | xfbmlParseSpyExtra(childTagName); 86 | 87 | var $elm = jqLite(elm), obj = {}; 88 | angular.forEach(INTERESTED_ATTRS, function (attrName) { 89 | var val = $elm.children().attr(attrName); 90 | if (val) { 91 | obj[attrName] = val; 92 | } 93 | }); 94 | // log attr values for tests 95 | $log.debug(obj); 96 | }; 97 | return $delegate; 98 | }); 99 | })); 100 | 101 | beforeEach(inject(function (_$rootScope_, _$compile_, _$timeout_, _$log_) { 102 | $rootScope = _$rootScope_; 103 | 104 | $scope = $rootScope.$new(); 105 | $scope.rendered = onrenderSpy; 106 | $scope.renderSwitch = false; 107 | 108 | $compile = _$compile_; 109 | $timeout = _$timeout_; 110 | $log = _$log_; 111 | $log.reset(); 112 | 113 | element = $compile('
')($scope); 114 | })); 115 | 116 | afterEach(function () { 117 | dealoc(element); 118 | }); 119 | 120 | function makeItRendered(callCount) { 121 | if (callCount) { 122 | $timeout.flush(PARSE_DELAY_BY * callCount); 123 | } 124 | else { 125 | $timeout.flush(); 126 | } 127 | } 128 | 129 | describe('ezfbXfbml', function () { 130 | function compileDir(template) { 131 | element.append($compile(template)($scope)); 132 | $scope.$apply(); 133 | } 134 | 135 | beforeEach(function () { 136 | compileDir('\ 137 |
\ 138 | \ 139 |
\ 140 | '); 141 | }); 142 | 143 | it('should call ezfb.XFBML.parse once', function () { 144 | expect(xfbmlParseSpy.callCount).toEqual(1); 145 | }); 146 | 147 | it('should call ezfb.XFBML.parse with compiled element', function () { 148 | expect(xfbmlParseSpy.mostRecentCall.args[0]).toEqual(element.children()[0]); 149 | }); 150 | 151 | it('should evaluate onrender expression after rendered', function () { 152 | expect(onrenderSpy.callCount).toEqual(0); 153 | makeItRendered(); 154 | expect(onrenderSpy.callCount).toEqual(1); 155 | }); 156 | 157 | it('should call ezfb.XFBML.parse on triggering rerender', function () { 158 | makeItRendered(); 159 | expect(xfbmlParseSpy.callCount).toEqual(1); 160 | 161 | $scope.renderSwitch = true; 162 | $scope.$apply(); 163 | 164 | $timeout.flush(); 165 | 166 | expect(xfbmlParseSpy.callCount).toEqual(2); 167 | }); 168 | 169 | it('should evaluate onrender expression after triggering rerender', function () { 170 | makeItRendered(); 171 | expect(onrenderSpy.callCount).toEqual(1); 172 | 173 | $scope.renderSwitch = true; 174 | $scope.$apply(); 175 | 176 | $timeout.flush(); // Rerender call to parse method is in a $timeout 177 | 178 | makeItRendered(); 179 | expect(onrenderSpy.callCount).toEqual(2); 180 | }); 181 | }); 182 | 183 | describe('seemless integration', function () { 184 | function compileDir(template) { 185 | var compiled = $compile(template)($scope); 186 | element.append(compiled); 187 | } 188 | 189 | function destroyDirectiveElement () { 190 | element.children().children().remove(); 191 | } 192 | 193 | function getTemplate(dirTag, attrs) { 194 | var attrsStr = '', l; 195 | 196 | if (attrs && angular.isObject(attrs)) { 197 | l = ['']; 198 | angular.forEach(attrs, function (value, name) { 199 | l.push(name + '="' + value + '"'); 200 | }); 201 | attrsStr += l.join(' '); 202 | } 203 | 204 | return '<'+ dirTag +' onrender="rendered()"'+ attrsStr +'>'; 205 | } 206 | 207 | function toCamelCase(dirTag) { 208 | var l = []; 209 | angular.forEach(dirTag.split(/-|:/), function (token, i) { 210 | if (i === 0) { 211 | l.push(token); 212 | } 213 | else { 214 | l.push(token.charAt(0).toUpperCase() + token.slice(1)); 215 | } 216 | }); 217 | return l.join(''); 218 | } 219 | 220 | function lastLoggedAttrs () { 221 | if ($log.debug.logs.length === 0) { 222 | return null; 223 | } 224 | // $log.debug is called in the mocked ezfb.XFBML.parse 225 | return $log.debug.logs[$log.debug.logs.length - 1][0]; 226 | } 227 | 228 | function makeValue(attrName) { 229 | return '_' + attrName + '_'; 230 | } 231 | 232 | var WRAPPER_CLASS = 'ezfb-social-plugin-wrap'; 233 | 234 | angular.forEach(DIRECTIVES_CONFIG, function (attrNames, dirTag) { 235 | 236 | describe(toCamelCase(dirTag), function () { 237 | function helloRendered() { 238 | $timeout.flush(HELLO_RENDER_DELAY); 239 | } 240 | 241 | beforeEach(function () { 242 | // Simulate init xfbml parsing 243 | $timeout(function () { 244 | mockXFBMLrendered(); 245 | }, HELLO_RENDER_DELAY); 246 | }); 247 | 248 | it('should call ezfb.XFBML.parse once', function () { 249 | compileDir(getTemplate(dirTag)); 250 | 251 | expect(xfbmlParseSpy.callCount).toEqual(0); 252 | 253 | helloRendered(); 254 | 255 | expect(xfbmlParseSpy.callCount).toEqual(1); 256 | }); 257 | 258 | it('should call ezfb.XFBML.parse with wrapper element', function () { 259 | compileDir(getTemplate(dirTag)); 260 | $scope.$apply(); 261 | 262 | helloRendered(); 263 | 264 | expect(xfbmlParseSpy.mostRecentCall.args[0]).toEqual(element.children()[0]); 265 | }); 266 | 267 | it('should evaluate onrender expression after rendered', function () { 268 | compileDir(getTemplate(dirTag)); 269 | $scope.$apply(); 270 | 271 | helloRendered(); 272 | 273 | expect(onrenderSpy.callCount).toEqual(0); 274 | 275 | makeItRendered(); 276 | 277 | expect(onrenderSpy.callCount).toEqual(1); 278 | }); 279 | 280 | it('should unwrap after rendered', function () { 281 | compileDir(getTemplate(dirTag)); 282 | $scope.$apply(); 283 | 284 | helloRendered(); 285 | 286 | var classList = Array.prototype.slice.call(element.children()[0].classList); 287 | expect(classList.indexOf(WRAPPER_CLASS) >= 0).toBeTruthy(); 288 | 289 | makeItRendered(); 290 | 291 | expect(element.children()[0].tagName).toEqual(dirTag.toUpperCase()); 292 | }); 293 | 294 | it("should unwrap on $destroy even hasn't been rendered", function () { 295 | compileDir(getTemplate(dirTag)); 296 | $scope.$apply(); 297 | 298 | helloRendered(); 299 | 300 | var classList = Array.prototype.slice.call(element.children()[0].classList); 301 | expect(classList.indexOf(WRAPPER_CLASS) >= 0).toBeTruthy(); 302 | 303 | destroyDirectiveElement(); 304 | 305 | expect(element.children().length).toEqual(0); 306 | }); 307 | 308 | it('should call ezfb.XFBML.parse with interpolated attribute', function () { 309 | var attrs = {}, lastAttrs; 310 | attrs[attrNames[0]] = '{{v0}}'; 311 | $scope.v0 = attrNames[0]; 312 | 313 | compileDir(getTemplate(dirTag, attrs)); 314 | 315 | expect(lastLoggedAttrs()).toBeFalsy(); 316 | 317 | $scope.$apply(); 318 | 319 | helloRendered(); 320 | 321 | lastAttrs = lastLoggedAttrs(); 322 | expect(lastAttrs[attrNames[0]]).toEqual($scope.v0); 323 | }); 324 | 325 | it('should call ezfb.XFBML.parse with delay-interpolated attribute', function () { 326 | var INTERPOLATE_0 = 150; 327 | 328 | var attrs = {}, lastAttrs; 329 | 330 | attrs[attrNames[0]] = '{{v0}}'; 331 | 332 | $timeout(function () { 333 | $scope.v0 = makeValue(attrNames[0]); 334 | }, INTERPOLATE_0); 335 | 336 | compileDir(getTemplate(dirTag, attrs)); 337 | $scope.$apply(); 338 | 339 | helloRendered(); 340 | 341 | // No attr interpolated 342 | lastAttrs = lastLoggedAttrs(); 343 | expect(lastAttrs[attrNames[0]]).toBeFalsy(); 344 | 345 | // attrNames[0] interpolated 346 | $timeout.flush(INTERPOLATE_0); 347 | 348 | lastAttrs = lastLoggedAttrs(); 349 | expect(lastAttrs[attrNames[0]]).toEqual($scope.v0); 350 | }); 351 | 352 | it('should call ezfb.XFBML.parse with correct attributes when they are interpolated staggerly', function () { 353 | if (attrNames.length <= 1) { 354 | return; 355 | } 356 | 357 | var INTERPOLATE_0 = 50, 358 | INTERPOLATE_1 = 250; 359 | 360 | var attrs = {}, lastAttrs, callChildTagName; 361 | 362 | attrs[attrNames[0]] = '{{v0}}'; 363 | attrs[attrNames[1]] = '{{v1}}'; 364 | 365 | $timeout(function () { 366 | $scope.v0 = makeValue(attrNames[0]); 367 | }, INTERPOLATE_0); 368 | $timeout(function () { 369 | $scope.v1 = makeValue(attrNames[1]); 370 | }, INTERPOLATE_1); 371 | 372 | compileDir(getTemplate(dirTag, attrs)); 373 | $scope.$apply(); 374 | 375 | helloRendered(); 376 | 377 | // No attr interpolated 378 | lastAttrs = lastLoggedAttrs(); 379 | expect(lastAttrs[attrNames[0]]).toBeFalsy(); 380 | expect(lastAttrs[attrNames[1]]).toBeFalsy(); 381 | expect(xfbmlParseSpy.callCount).toEqual(1); 382 | // Make sure there's no nesting of wrapper element 383 | callChildTagName = xfbmlParseSpyExtra.mostRecentCall.args[0]; 384 | expect(callChildTagName).toEqual(dirTag.toUpperCase()); 385 | 386 | // attrNames[0] interpolated 387 | $timeout.flush(INTERPOLATE_0); // at 50 388 | 389 | // First call rendered 390 | makeItRendered(1); // at 100 391 | 392 | lastAttrs = lastLoggedAttrs(); 393 | expect(lastAttrs[attrNames[0]]).toEqual($scope.v0); 394 | expect(lastAttrs[attrNames[1]]).toBeFalsy(); 395 | expect(xfbmlParseSpy.callCount).toEqual(2); 396 | // Make sure there's no nesting of wrapper element 397 | callChildTagName = xfbmlParseSpyExtra.mostRecentCall.args[0]; 398 | expect(callChildTagName).toEqual(dirTag.toUpperCase()); 399 | 400 | // Second call rendered 401 | makeItRendered(2); // at 200 402 | 403 | // attrNames[1] interpolated 404 | $timeout.flush(INTERPOLATE_1); // at 250 405 | 406 | lastAttrs = lastLoggedAttrs(); 407 | expect(lastAttrs[attrNames[0]]).toEqual($scope.v0); 408 | expect(lastAttrs[attrNames[1]]).toEqual($scope.v1); 409 | expect(xfbmlParseSpy.callCount).toEqual(3); 410 | // Make sure there's no nesting of wrapper element 411 | callChildTagName = xfbmlParseSpyExtra.mostRecentCall.args[0]; 412 | expect(callChildTagName).toEqual(dirTag.toUpperCase()); 413 | }); 414 | }); 415 | 416 | }); 417 | }); 418 | 419 | }); 420 | --------------------------------------------------------------------------------