angular-sixpack.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  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:_opts.client_id, 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, kpi, callback) {
  113. if (typeof kpi === 'function') {
  114. callback = kpi;
  115. kpi = null;
  116. }
  117. var session = _getOrInitSession();
  118. if (!testName) {
  119. if (_opts.debug) {
  120. $log.info("[sixpack] Recording conversion for all tests", _tests);
  121. };
  122. for (var i = 0, ii = _tests.length; i < ii; i++) {
  123. var test = _tests[i]
  124. , results = [];
  125. session.convert(test, kpi, function (err, res) {
  126. results.push(res);
  127. if (err && _opts.debug) {
  128. $log.warn("[sixpack] Error recording conversion for", test, err);
  129. };
  130. if (results.length == ii) {
  131. if (_opts.debug) {
  132. $log.debug('[sixpack] All results:', results);
  133. };
  134. if (callback) {
  135. $timeout(function () {
  136. callback(results);
  137. });
  138. }
  139. };
  140. });
  141. }
  142. } else {
  143. if (_opts.debug) {
  144. $log.info("[sixpack] Recording conversion for", testName);
  145. };
  146. session.convert(testName, kpi, function (err, res) {
  147. if (err && _opts.debug) {
  148. $log.warn('[sixpack] Error recording conversion:', err);
  149. } else if (_opts.debug) {
  150. $log.debug('[sixpack] Conversion result:', res);
  151. };
  152. if (callback) {
  153. $timeout(function () {
  154. callback(res);
  155. });
  156. }
  157. });
  158. }
  159. }
  160. }
  161. return methods;
  162. }];
  163. })
  164. .directive('sixpackSwitch', ['sixpack', function(sixpack) {
  165. return {
  166. controller : ['$element', function($element) {
  167. var ctrl = this
  168. , _testName
  169. // Map of variation names to transclude fns
  170. , _variations = {};
  171. var _processChoice = function (choice) {
  172. // Triggered if for some reason we get an error from sixpack,
  173. // or optionally if a user is excluded from this test via configuration
  174. if (!choice) {
  175. _setContent(_variations['default']);
  176. } else {
  177. _setContent(_variations[choice]);
  178. }
  179. }
  180. var _setContent = function (fn) {
  181. if (!fn) {
  182. return;
  183. };
  184. fn(function(clone) {
  185. $element.replaceWith(clone);
  186. });
  187. }
  188. // Pseudo-shim for '.keys' method
  189. // Additionally, if obj has a 'default' property, sets that as the first element
  190. // so sixpack will use it as the control
  191. var _keys = function (obj) {
  192. var keys = []
  193. , prop;
  194. for (prop in obj) {
  195. if (!obj.hasOwnProperty(prop)) {
  196. continue;
  197. };
  198. if (prop == 'default') {
  199. keys.unshift(prop);
  200. } else {
  201. keys.push(prop);
  202. }
  203. }
  204. return keys;
  205. }
  206. ctrl.registerSwitch = function (name) {
  207. _testName = name;
  208. sixpack.participate(_testName, _keys(_variations), _processChoice);
  209. }
  210. ctrl.registerVariation = function (variation, fn) {
  211. _variations[variation] = fn;
  212. }
  213. return ctrl;
  214. }],
  215. require: 'sixpackSwitch',
  216. link : function ($scope, $element, $attrs, ctrl) {
  217. ctrl.registerSwitch($attrs.sixpackSwitch);
  218. }
  219. }
  220. }])
  221. // Register a variation for a test
  222. .directive('sixpackWhen', ['$log', function ($log) {
  223. return {
  224. require: '^sixpackSwitch',
  225. transclude: 'element',
  226. link: function($scope, $element, $attrs, ctrl, transcludeFn) {
  227. if ($attrs.sixpackWhen) {
  228. ctrl.registerVariation($attrs.sixpackWhen, transcludeFn);
  229. } else {
  230. $log.debug('[sixpack] When directive initialized without a name, ignoring');
  231. }
  232. }
  233. }
  234. }])
  235. // Register the 'default view, registered as the control variation, and
  236. // always used if sixpack errors out or if user is excluded via configuration
  237. .directive('sixpackDefault', function () {
  238. return {
  239. require: '^sixpackSwitch',
  240. transclude: 'element',
  241. link: function($scope, $element, $attrs, ctrl, transcludeFn) {
  242. ctrl.registerVariation('default', transcludeFn);
  243. }
  244. }
  245. })
  246. .directive('sixpackConvert', ['sixpack', function (sixpack) {
  247. return {
  248. link : function ($scope, $element, $attrs) {
  249. var test = $attrs.sixpackConvert || undefined
  250. , eventType = $attrs.on || 'click';
  251. $element.on(eventType, function () {
  252. sixpack.convert(test);
  253. });
  254. }
  255. }
  256. }]);
  257. })(window.angular, window.sixpack);