angular-sixpack.js 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. (function(angular) {
  2. // Just in case...
  3. if (!Array.prototype.indexOf) {
  4. Array.prototype.indexOf = function (searchElement, fromIndex) {
  5. if ( this === undefined || this === null ) {
  6. throw new TypeError( '"this" is null or not defined' );
  7. }
  8. var length = this.length >>> 0; // Hack to convert object.length to a UInt32
  9. fromIndex = +fromIndex || 0;
  10. if (Math.abs(fromIndex) === Infinity) {
  11. fromIndex = 0;
  12. }
  13. if (fromIndex < 0) {
  14. fromIndex += length;
  15. if (fromIndex < 0) {
  16. fromIndex = 0;
  17. }
  18. }
  19. for (;fromIndex < length; fromIndex++) {
  20. if (this[fromIndex] === searchElement) {
  21. return fromIndex;
  22. }
  23. }
  24. return -1;
  25. };
  26. }
  27. angular.module('mvdSixpack', ['ngCookies'])
  28. .provider('sixpack', function() {
  29. var $body
  30. , _tests = []
  31. , _choices = {}
  32. , _opts = {
  33. baseUrl: '',
  34. debug: false,
  35. }
  36. , sp = window.sixpack;
  37. this.setOptions = function (options) {
  38. angular.extend(_opts, options || {});
  39. }
  40. this.$get = ['$cookies','$timeout', '$log', function($cookies, $timeout, $log) {
  41. var _cookiePrefix = 'sixpack-'
  42. , _session
  43. , _clientId;
  44. var _getOrInitSession = function () {
  45. if (!_session) {
  46. if (_clientId = $cookies[_cookiePrefix + 'clientId']) {
  47. _session = new sp.Session({client_id:_clientId, base_url:_opts.baseUrl});
  48. } else {
  49. _session = new sp.Session({client_id:undefined, base_url:_opts.baseUrl});
  50. $cookies[_cookiePrefix + 'clientId'] = _clientId = _session.client_id;
  51. }
  52. if (_opts.debug) {
  53. $log.debug('[sixpack] Initialized session with clientId', _clientId, 'and base url', _opts.baseUrl);
  54. };
  55. };
  56. return _session;
  57. }
  58. var methods = {
  59. participate : function (testName, variations, callback) {
  60. if (_tests.indexOf(testName) < 0) {
  61. _tests.push(testName);
  62. } else if (angular.isDefined(_choices[testName])) {
  63. var res = _choices[testName];
  64. if (res === false) {
  65. // Still loading
  66. $timeout(function () {
  67. methods.participate(testName, variations, callback);
  68. }, 50);
  69. return;
  70. };
  71. if (_opts.debug) {
  72. $log.info('[sixpack] Using already chosen variation for test', testName, res);
  73. };
  74. $timeout(function () {
  75. callback(res.alternative.name, res);
  76. });
  77. return;
  78. }
  79. _choices[testName] = false;
  80. var session = _getOrInitSession();
  81. if (_opts.debug) {
  82. $log.info('[sixpack] Getting choice for', testName, 'out of', variations);
  83. };
  84. session.participate(testName, variations, function (err, res) {
  85. if (err) {
  86. if (_opts.debug) {
  87. $log.warn('[sixpack] Received error', err);
  88. };
  89. $timeout(function () {
  90. callback(false);
  91. });
  92. delete _choices[testName];
  93. return;
  94. };
  95. _choices[testName] = res;
  96. var choice = res.alternative.name;
  97. if (_opts.debug) {
  98. $log.info('[sixpack] Alternative chosen:', choice);
  99. $log.debug('[sixpack] Full response', res);
  100. };
  101. if (!$body) {
  102. $body = angular.element(document).find('body');
  103. };
  104. $body.addClass('sixpack-'+testName+' sixpack-'+testName+'-'+choice);
  105. $timeout(function() {
  106. callback(choice, res);
  107. });
  108. });
  109. },
  110. // Register a 'conversion'. If no testName, will call for all active tests
  111. // Takes an optional callback that receives the raw response from sixpack (or undefined on error)
  112. convert : function (testName, callback) {
  113. var session = _getOrInitSession();
  114. if (!testName) {
  115. if (_opts.debug) {
  116. $log.info("[sixpack] Recording conversion for all tests", _tests);
  117. };
  118. for (var i = 0, ii = _tests.length; i < ii; i++) {
  119. var test = _tests[i]
  120. , results = [];
  121. session.convert(test, function (err, res) {
  122. results.push(res);
  123. if (err && _opts.debug) {
  124. $log.warn("[sixpack] Error recording conversion for", test, err);
  125. };
  126. if (results.length == ii) {
  127. if (_opts.debug) {
  128. $log.debug('[sixpack] All results:', results);
  129. };
  130. if (callback) {
  131. $timeout(function () {
  132. callback(results);
  133. });
  134. }
  135. };
  136. });
  137. }
  138. } else {
  139. if (_opts.debug) {
  140. $log.info("[sixpack] Recording conversion for", testName);
  141. };
  142. session.convert(testName, function (err, res) {
  143. if (err && _opts.debug) {
  144. $log.warn('[sixpack] Error recording conversion:', err);
  145. } else if (_opts.debug) {
  146. $log.debug('[sixpack] Conversion result:', res);
  147. };
  148. if (callback) {
  149. $timeout(function () {
  150. callback(res);
  151. });
  152. }
  153. });
  154. }
  155. }
  156. }
  157. return methods;
  158. }];
  159. })
  160. .directive('sixpackSwitch', ['sixpack', function(sixpack) {
  161. return {
  162. controller : ['$element', function($element) {
  163. var ctrl = this
  164. , _testName
  165. // Map of variation names to transclude fns
  166. , _variations = {};
  167. var _processChoice = function (choice) {
  168. // Triggered if for some reason we get an error from sixpack,
  169. // or optionally if a user is excluded from this test via configuration
  170. if (!choice) {
  171. _setContent(_variations['default']);
  172. } else {
  173. _setContent(_variations[choice]);
  174. }
  175. }
  176. var _setContent = function (fn) {
  177. if (!fn) {
  178. return;
  179. };
  180. fn(function(clone) {
  181. $element.replaceWith(clone);
  182. });
  183. }
  184. // Pseudo-shim for '.keys' method
  185. // Additionally, if obj has a 'default' property, sets that as the first element
  186. // so sixpack will use it as the control
  187. var _keys = function (obj) {
  188. var keys = []
  189. , prop;
  190. for (prop in obj) {
  191. if (!obj.hasOwnProperty(prop)) {
  192. continue;
  193. };
  194. if (prop == 'default') {
  195. keys.unshift(prop);
  196. } else {
  197. keys.push(prop);
  198. }
  199. }
  200. return keys;
  201. }
  202. ctrl.registerSwitch = function (name) {
  203. _testName = name;
  204. sixpack.participate(_testName, _keys(_variations), _processChoice);
  205. }
  206. ctrl.registerVariation = function (variation, fn) {
  207. _variations[variation] = fn;
  208. }
  209. return ctrl;
  210. }],
  211. require: 'sixpackSwitch',
  212. link : function ($scope, $element, $attrs, ctrl) {
  213. ctrl.registerSwitch($attrs.sixpackSwitch);
  214. }
  215. }
  216. }])
  217. // Register a variation for a test
  218. .directive('sixpackWhen', ['$log', function ($log) {
  219. return {
  220. require: '^sixpackSwitch',
  221. transclude: 'element',
  222. link: function($scope, $element, $attrs, ctrl, transcludeFn) {
  223. if ($attrs.sixpackWhen) {
  224. ctrl.registerVariation($attrs.sixpackWhen, transcludeFn);
  225. } else {
  226. $log.debug('[sixpack] When directive initialized without a name, ignoring');
  227. }
  228. }
  229. }
  230. }])
  231. // Register the 'default view, registered as the control variation, and
  232. // always used if sixpack errors out or if user is excluded via configuration
  233. .directive('sixpackDefault', function () {
  234. return {
  235. require: '^sixpackSwitch',
  236. transclude: 'element',
  237. link: function($scope, $element, $attrs, ctrl, transcludeFn) {
  238. ctrl.registerVariation('default', transcludeFn);
  239. }
  240. }
  241. })
  242. .directive('sixpackConvert', ['sixpack', function (sixpack) {
  243. return {
  244. link : function ($scope, $element, $attrs) {
  245. var test = $attrs.sixpackConvert || undefined
  246. , eventType = $attrs.on || 'click';
  247. $element.on(eventType, function () {
  248. sixpack.convert(test);
  249. });
  250. }
  251. }
  252. }]);
  253. })(window.angular, window.sixpack);