angular-sixpack.js 7.7 KB

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