offendersDirectives.js 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921
  1. (function() {
  2. "use strict";
  3. var offendersDirectives = angular.module('offendersDirectives', []);
  4. function getdomTreeHTML(tree) {
  5. return '<div class="domTree">' + getdomTreeInnerHTML(tree) + '</div>';
  6. }
  7. function getdomTreeInnerHTML(tree) {
  8. return recursiveHtmlBuilder(tree);
  9. }
  10. function recursiveHtmlBuilder(tree) {
  11. var html = '';
  12. var keys = Object.keys(tree);
  13. keys.forEach(function(key) {
  14. if (isNaN(tree[key])) {
  15. html += '<div><span>' + key + '</span>' + recursiveHtmlBuilder(tree[key]) + '</div>';
  16. } else if (tree[key] > 1) {
  17. html += '<div><span>' + key + ' <span>(x' + tree[key] + ')</span></span></div>';
  18. } else {
  19. html += '<div><span>' + key + '</span></div>';
  20. }
  21. });
  22. return html;
  23. }
  24. offendersDirectives.directive('domTree', function() {
  25. return {
  26. restrict: 'E',
  27. scope: {
  28. tree: '='
  29. },
  30. template: '<div class="domTree"></div>',
  31. replace: true,
  32. link: function(scope, element) {
  33. element.append(getdomTreeInnerHTML(scope.tree));
  34. }
  35. };
  36. });
  37. function getDomElementButtonHTML(obj, onASingleLine) {
  38. if (obj.tree && !onASingleLine) {
  39. return '<div class="offenderButton opens">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
  40. } else {
  41. return '<div class="offenderButton">' + getDomElementButtonInnerHTML(obj, onASingleLine) + '</div>';
  42. }
  43. }
  44. function getDomElementButtonInnerHTML(obj, onASingleLine) {
  45. if (obj.type === 'html' ||
  46. obj.type === 'body' ||
  47. obj.type === 'head' ||
  48. obj.type === 'window' ||
  49. obj.type === 'document' ||
  50. obj.type === 'fragment') {
  51. return obj.type;
  52. }
  53. if (obj.type === 'notAnElement') {
  54. return 'Incorrect element';
  55. }
  56. var html = '';
  57. if (obj.type === 'domElement') {
  58. html = 'DOM element <b>' + obj.element + '</b>';
  59. } else if (obj.type === 'fragmentElement') {
  60. html = 'Fragment element <b>' + obj.element + '</b>';
  61. } else if (obj.type === 'createdElement') {
  62. html = 'Created element <b>' + obj.element + '</b>';
  63. }
  64. if (obj.tree && !onASingleLine) {
  65. html += getdomTreeHTML(obj.tree);
  66. }
  67. return html;
  68. }
  69. offendersDirectives.directive('domElementButton', function() {
  70. return {
  71. restrict: 'E',
  72. scope: {
  73. obj: '='
  74. },
  75. template: '<div class="offenderButton" ng-class="{opens: obj.tree}"></div>',
  76. replace: true,
  77. link: function(scope, element) {
  78. element.append(getDomElementButtonInnerHTML(scope.obj));
  79. }
  80. };
  81. });
  82. function getJQueryContextButtonHTML(context, onASingleLine) {
  83. if (context.length === 0) {
  84. return '<span class="offenderButton">Empty jQuery object</span>';
  85. }
  86. if (context.length === 1) {
  87. return getDomElementButtonHTML(context.elements[0], onASingleLine);
  88. }
  89. var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine);
  90. if (context.length === 3) {
  91. html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine);
  92. } else if (context.length > 3) {
  93. html += ' and ' + (context.length - 2) + ' more...';
  94. }
  95. return html + ')';
  96. }
  97. function isJQuery(node) {
  98. return node.data.type.indexOf('jQuery ') === 0;
  99. }
  100. function getNonJQueryHTML(node, onASingleLine) {
  101. var type = node.data.type;
  102. if (node.windowPerformance) {
  103. switch (type) {
  104. case 'documentScroll':
  105. return '(triggering the scroll event on <b>document</b>)';
  106. case 'windowScroll':
  107. return '(triggering the scroll event on <b>window</b>)';
  108. case 'window.onscroll':
  109. return '(calling the <b>window.onscroll</b> function)';
  110. default:
  111. return '';
  112. }
  113. }
  114. if (!node.data.callDetails) {
  115. return '';
  116. }
  117. var args = node.data.callDetails.arguments;
  118. var ctxt = node.data.callDetails.context;
  119. switch (type) {
  120. case 'getElementById':
  121. case 'createElement':
  122. return '<b>' + args[0] + '</b>';
  123. case 'getElementsByClassName':
  124. case 'getElementsByTagName':
  125. case 'querySelector':
  126. case 'querySelectorAll':
  127. return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
  128. case 'appendChild':
  129. return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
  130. case 'insertBefore':
  131. return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine);
  132. case 'addEventListener':
  133. return 'bind <b>' + args[0] + '</b> to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
  134. case 'getComputedStyle':
  135. return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || '');
  136. case 'error':
  137. return args[0];
  138. case 'jQuery - onDOMReady':
  139. return '(function)';
  140. case 'documentScroll':
  141. return 'The scroll event just triggered on document';
  142. case 'windowScroll':
  143. return 'The scroll event just triggered on window';
  144. case 'window.onscroll':
  145. return 'The window.onscroll function just got called';
  146. default:
  147. return '';
  148. }
  149. }
  150. function getJQueryHTML(node, onASingleLine) {
  151. var type = node.data.type;
  152. var unescapedArgs = node.data.callDetails.arguments;
  153. var args = [];
  154. var ctxt = node.data.callDetails.context;
  155. // escape HTML in args
  156. for (var i = 0 ; i < 4 ; i ++) {
  157. if (unescapedArgs[i] !== undefined) {
  158. args[i] = escapeHTML(unescapedArgs[i]);
  159. }
  160. }
  161. if (type === 'jQuery loaded' || type === 'jQuery version change') {
  162. return args[0];
  163. }
  164. switch (type) {
  165. case 'jQuery - onDOMReady':
  166. case 'jQuery - windowOnLoad':
  167. return '(function)';
  168. case 'jQuery - Sizzle call':
  169. return '<b>' + args[0] + '</b> on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine);
  170. case 'jQuery - find':
  171. if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') {
  172. return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  173. } else {
  174. return '<b>' + args[0] + '</b>';
  175. }
  176. break;
  177. case 'jQuery - html':
  178. if (args[0] !== undefined) {
  179. return 'set content "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  180. } else {
  181. return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  182. }
  183. break;
  184. case 'jQuery - append':
  185. return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  186. case 'jQuery - appendTo':
  187. return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
  188. case 'jQuery - prepend':
  189. return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  190. case 'jQuery - prependTo':
  191. return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[0] + '</b>';
  192. case 'jQuery - before':
  193. return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  194. case 'jQuery - insertBefore':
  195. return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before <b>' + args[0] + '</b>';
  196. case 'jQuery - after':
  197. return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  198. case 'jQuery - insertAfter':
  199. return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after <b>' + args[0] + '</b>';
  200. case 'jQuery - remove':
  201. case 'jQuery - detach':
  202. if (args[0]) {
  203. return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
  204. } else {
  205. return getJQueryContextButtonHTML(ctxt, onASingleLine);
  206. }
  207. break;
  208. case 'jQuery - empty':
  209. case 'jQuery - clone':
  210. case 'jQuery - unwrap':
  211. case 'jQuery - show':
  212. case 'jQuery - hide':
  213. case 'jQuery - animate':
  214. case 'jQuery - fadeIn':
  215. case 'jQuery - fadeOut':
  216. case 'jQuery - fadeTo':
  217. case 'jQuery - fadeToggle':
  218. case 'jQuery - slideDown':
  219. case 'jQuery - slideUp':
  220. case 'jQuery - slideToggle':
  221. return getJQueryContextButtonHTML(ctxt, onASingleLine);
  222. case 'jQuery - replaceWith':
  223. return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with <b>' + args[0] + '</b>';
  224. case 'jQuery - replaceAll':
  225. return 'replace <b>' + args[0] + '</b> with ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  226. case 'jQuery - text':
  227. if (args[0] !== undefined) {
  228. return 'set text "<b>' + args[0] + '</b>" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  229. } else {
  230. return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  231. }
  232. break;
  233. case 'jQuery - wrap':
  234. case 'jQuery - wrapAll':
  235. return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
  236. case 'jQuery - wrapInner':
  237. return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within <b>' + args[0] + '</b>';
  238. case 'jQuery - css':
  239. case 'jQuery - attr':
  240. case 'jQuery - prop':
  241. if (isStringOfObject(args[0])) {
  242. return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  243. } else if (args[1]) {
  244. return 'set <b>' + args[0] + '</b> : <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  245. } else {
  246. return 'get <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  247. }
  248. break;
  249. case 'jQuery - offset':
  250. case 'jQuery - height':
  251. case 'jQuery - innerHeight':
  252. case 'jQuery - width':
  253. case 'jQuery - innerWidth':
  254. case 'jQuery - scrollLeft':
  255. case 'jQuery - scrollTop':
  256. case 'jQuery - position':
  257. if (args[0]) {
  258. return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  259. } else {
  260. return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  261. }
  262. break;
  263. case 'jQuery - outerHeight':
  264. case 'jQuery - outerWidth':
  265. if (args[0] && args[0] !== 'true') {
  266. return 'set <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  267. } else if (args[0] === 'true') {
  268. return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)';
  269. } else {
  270. return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  271. }
  272. break;
  273. case 'jQuery - toggle':
  274. if (args[0] === 'true') {
  275. return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible';
  276. } else if (args[0] === 'false') {
  277. return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden';
  278. } else {
  279. return getJQueryContextButtonHTML(ctxt, onASingleLine);
  280. }
  281. break;
  282. case 'jQuery - on':
  283. case 'jQuery - one':
  284. if (isStringOfObject(args[0])) {
  285. return '<b>' + args[0].replace(/&quot;\(function\)&quot;/g, '(function)') + '</b>';
  286. } else if (args[1] && isPureString(args[1])) {
  287. return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
  288. } else {
  289. return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  290. }
  291. break;
  292. case 'jQuery - off':
  293. if (args[0]) {
  294. if (args[1]) {
  295. return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[1] + '</b>';
  296. } else {
  297. return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  298. }
  299. } else {
  300. return 'unbind all events';
  301. }
  302. break;
  303. case 'jQuery - live':
  304. case 'jQuery - bind':
  305. return 'bind <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  306. case 'jQuery - die':
  307. case 'jQuery - unbind':
  308. if (args[0]) {
  309. return 'unbind <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  310. } else {
  311. return 'unbind all events';
  312. }
  313. break;
  314. case 'jQuery - delegate':
  315. return 'bind <b>' + args[1] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
  316. case 'jQuery - undelegate':
  317. if (args[0]) {
  318. if (args[1]) {
  319. return 'unbind <b>' + args[1] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by <b>' + args[0] + '</b>';
  320. } else {
  321. return 'unbind namespace <b>' + args[0] + '</b>';
  322. }
  323. } else {
  324. return 'unbind all events';
  325. }
  326. break;
  327. case 'jQuery - blur':
  328. case 'jQuery - change':
  329. case 'jQuery - click':
  330. case 'jQuery - dblclick':
  331. case 'jQuery - focus':
  332. case 'jQuery - keydown':
  333. case 'jQuery - keypress':
  334. case 'jQuery - keyup':
  335. case 'jQuery - mousedown':
  336. case 'jQuery - mouseenter':
  337. case 'jQuery - mouseleave':
  338. case 'jQuery - mousemove':
  339. case 'jQuery - mouseout':
  340. case 'jQuery - mouseover':
  341. case 'jQuery - mouseup':
  342. case 'jQuery - resize':
  343. case 'jQuery - scroll':
  344. case 'jQuery - select':
  345. case 'jQuery - submit':
  346. if (args[0]) {
  347. return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  348. } else {
  349. return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  350. }
  351. break;
  352. case 'jQuery - error':
  353. case 'jQuery - focusin':
  354. case 'jQuery - focusout':
  355. case 'jQuery - hover':
  356. case 'jQuery - load':
  357. case 'jQuery - unload':
  358. return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  359. case 'jQuery - removeAttr':
  360. case 'jQuery - removeProp':
  361. return 'remove <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  362. case 'jQuery - val':
  363. if (args[0]) {
  364. return 'set value <b>' + args[0] + '</b> to ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  365. } else {
  366. return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  367. }
  368. break;
  369. case 'jQuery - hasClass':
  370. case 'jQuery - addClass':
  371. case 'jQuery - removeClass':
  372. return '<b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  373. case 'jQuery - toggleClass':
  374. if (args[0]) {
  375. if (args[1]) {
  376. return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to <b>' + args[1] + '</b>';
  377. } else {
  378. return 'toggle <b>' + args[0] + '</b> on ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  379. }
  380. } else {
  381. return 'magic no-argument toggleClass';
  382. }
  383. break;
  384. case 'jQuery - children':
  385. if (args[0]) {
  386. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by <b>' + args[0] + '</b>';
  387. } else {
  388. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  389. }
  390. break;
  391. case 'jQuery - closest':
  392. if (args[1]) {
  393. return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context <b>' + args[1] + '</b>';
  394. } else {
  395. return 'closest <b>' + args[0] + '</b> from ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  396. }
  397. break;
  398. case 'jQuery - next':
  399. case 'jQuery - nextAll':
  400. if (args[0]) {
  401. return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
  402. } else {
  403. return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  404. }
  405. break;
  406. case 'jQuery - nextUntil':
  407. if (args[0]) {
  408. if (args[1]) {
  409. return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
  410. } else {
  411. return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
  412. }
  413. } else {
  414. return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  415. }
  416. break;
  417. case 'jQuery - offsetParent':
  418. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  419. case 'jQuery - prev':
  420. case 'jQuery - prevAll':
  421. if (args[0]) {
  422. return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
  423. } else {
  424. return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  425. }
  426. break;
  427. case 'jQuery - prevUntil':
  428. if (args[0]) {
  429. if (args[1]) {
  430. return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
  431. } else {
  432. return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
  433. }
  434. } else {
  435. return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  436. }
  437. break;
  438. case 'jQuery - parent':
  439. case 'jQuery - parents':
  440. if (args[0]) {
  441. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
  442. } else {
  443. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  444. }
  445. break;
  446. case 'jQuery - parentsUntil':
  447. if (args[0]) {
  448. if (args[1]) {
  449. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b> and matching <b>' + args[1] + '</b>';
  450. } else {
  451. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until <b>' + args[0] + '</b>';
  452. }
  453. } else {
  454. return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  455. }
  456. break;
  457. case 'jQuery - siblings':
  458. if (args[0]) {
  459. return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching <b>' + args[0] + '</b>';
  460. } else {
  461. return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine);
  462. }
  463. break;
  464. default:
  465. return '';
  466. }
  467. }
  468. function escapeHTML(html) {
  469. var entityMap = {
  470. "&": "&amp;",
  471. "<": "&lt;",
  472. ">": "&gt;",
  473. '"': '&quot;',
  474. "'": '&#39;',
  475. "/": '&#x2F;'
  476. };
  477. return String(html).replace(/[&<>"'\/]/g, function (s) {
  478. return entityMap[s];
  479. });
  480. }
  481. function joinArgs(args) {
  482. var html = '<b>' + args[0] + '</b>';
  483. if (args[1]) {
  484. html += ', <b>' + args[1] + '</b>';
  485. if (args[2]) {
  486. html += ', <b>' + args[2] + '</b>';
  487. if (args[3]) {
  488. html += ', and more...';
  489. }
  490. }
  491. }
  492. return html;
  493. }
  494. function isStringOfObject(str) {
  495. return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}';
  496. }
  497. function isPureString(str) {
  498. return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown';
  499. }
  500. function getTimelineParamsHTML(node, onASingleLine) {
  501. if (isJQuery(node)) {
  502. return getJQueryHTML(node, onASingleLine);
  503. } else {
  504. return getNonJQueryHTML(node, onASingleLine);
  505. }
  506. }
  507. function getBacktraceHTML(backtrace) {
  508. var html = '';
  509. var parsedBacktrace = parseBacktrace(backtrace);
  510. if (!parsedBacktrace || parsedBacktrace.length === 0) {
  511. html += '<div><div>can\'t find any backtrace :/</div></div>';
  512. } else {
  513. for (var i = 0 ; i < parsedBacktrace.length ; i++) {
  514. html += '<div>';
  515. html += '<div>' + (parsedBacktrace[i].fnName || '(anonymous function)') + '</div>';
  516. html += '<div class="trace">' + getUrlLink(parsedBacktrace[i].filePath, 40) + '</div>';
  517. if (parsedBacktrace[i].column) {
  518. html += '<div>' + parsedBacktrace[i].line + ':' + parsedBacktrace[i].column + '</div>';
  519. } else {
  520. html += '<div>line ' + parsedBacktrace[i].line + '</div>';
  521. }
  522. html += '</div>';
  523. }
  524. }
  525. return html;
  526. }
  527. function parseBacktrace(str) {
  528. if (!str) {
  529. return null;
  530. }
  531. var out = [];
  532. var splited = str.split(' / ');
  533. try {
  534. splited.forEach(function(trace) {
  535. var fnName = null, fileAndLine;
  536. var withFnResult = /^([^\s\(]+) \((.+:\d+)\)$/.exec(trace);
  537. if (withFnResult === null) {
  538. // Try the PhantomJS 2 format
  539. withFnResult = /^([^\s\(]+) \((.+:\d+:\d+)\)$/.exec(trace);
  540. }
  541. if (withFnResult === null) {
  542. // Yet another PhantomJS 2 format?
  543. withFnResult = /^([^\s\(]+|global code)@(.+:\d+:\d+)$/.exec(trace);
  544. }
  545. if (withFnResult === null) {
  546. // Try the PhantomJS 2 ERROR format
  547. withFnResult = /^([^\s\(]+) (http.+:\d+)$/.exec(trace);
  548. }
  549. if (withFnResult === null) {
  550. fileAndLine = trace;
  551. } else {
  552. fnName = withFnResult[1];
  553. fileAndLine = withFnResult[2];
  554. }
  555. // And now the second part
  556. var fileAndLineSplit = /^(.*):(\d+):(\d+)$/.exec(fileAndLine);
  557. if (fileAndLineSplit === null) {
  558. fileAndLineSplit = /^(.*):(\d+)$/.exec(fileAndLine);
  559. }
  560. var filePath = fileAndLineSplit[1];
  561. var line = fileAndLineSplit[2];
  562. var column = fileAndLineSplit[3];
  563. // Filter phantomas code
  564. if (filePath.indexOf('phantomjs://') === -1) {
  565. out.push({
  566. fnName: fnName,
  567. filePath: filePath,
  568. line: line,
  569. column: column
  570. });
  571. }
  572. });
  573. } catch(e) {
  574. return null;
  575. }
  576. return out;
  577. }
  578. function getTimelineDetailsHTML(node) {
  579. var html = '';
  580. if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) {
  581. if (node.warning || node.error) {
  582. html += '<div class="icon-warning"></div>';
  583. } else {
  584. html += '<div class="icon-question"></div>';
  585. }
  586. html += '<div class="detailsOverlay">';
  587. html += '<div class="closeBtn">✖</div>';
  588. if (node.data.callDetails.context && node.data.callDetails.context.length === 0) {
  589. html += '<h4>Called on 0 jQuery element</h4><p class="advice">Useless function call, as the jQuery object is empty.</p>';
  590. } else if (node.eventNotDelegated) {
  591. html += '<p class="advice">This binding should use Event Delegation instead of binding each element one by one.</p>';
  592. }
  593. if (node.data.resultsNumber === 0) {
  594. html += '<p class="advice">The query returned 0 results. Could it be unused or dead code?</p>';
  595. } else if (node.data.resultsNumber > 0) {
  596. html += '<p>The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.</p>';
  597. }
  598. if (node.data.backtrace) {
  599. html += '<h4>Backtrace</h4>';
  600. html += '<div class="table">';
  601. html += getBacktraceHTML(node.data.backtrace);
  602. html += '</div>';
  603. }
  604. html += '</div>';
  605. }
  606. return html;
  607. }
  608. offendersDirectives.directive('profilerLine', ['$filter', function($filter) {
  609. var numberWithCommas = $filter('number');
  610. function getProfilerLineHTML(index, node) {
  611. return '<div class="index">' + (index + 1) + '</div>' +
  612. '<div class="type">' + node.data.type + (node.children ? '<div class="children">' + recursiveChildrenHTML(node) + '</div>' : '') + '</div>' +
  613. '<div class="value">' + getTimelineParamsHTML(node, false) + '</div>' +
  614. '<div class="details">' + getTimelineDetailsHTML(node) + '</div>' +
  615. '<div class="startTime ' + node.data.loadingStep + '">' + numberWithCommas(node.data.timestamp, 0) + ' ms</div>';
  616. }
  617. function recursiveChildrenHTML(node) {
  618. var html = '';
  619. if (node.children) {
  620. node.children.forEach(function(child) {
  621. html += '<div class="child"><span>' + child.data.type + '<div class="childArgs">' + getTimelineParamsHTML(child, true) + '</div></span>' + recursiveChildrenHTML(child) + '</div>';
  622. });
  623. }
  624. return html;
  625. }
  626. function onDetailsClick(row) {
  627. // Close if it's alreay open
  628. if (row.classList.contains('showDetails')) {
  629. closeDetails(row);
  630. return;
  631. }
  632. // Close any other open details overlay
  633. var openOnes = document.getElementsByClassName('showDetails');
  634. if (openOnes.length > 0) {
  635. openOnes[0].classList.remove('showDetails');
  636. }
  637. // Make it appear
  638. row.classList.add('showDetails');
  639. // Bind the close button
  640. row.querySelector('.closeBtn').addEventListener('click', function() {
  641. closeDetails(row);
  642. });
  643. }
  644. function closeDetails(row) {
  645. row.classList.remove('showDetails');
  646. // Unbind the close button
  647. row.querySelector('.closeBtn').removeEventListener('click', closeDetails);
  648. }
  649. return {
  650. restrict: 'E',
  651. scope: {
  652. index: '=',
  653. node: '='
  654. },
  655. template: '<div></div>',
  656. replace: true,
  657. link: function(scope, element) {
  658. if (scope.node.error) {
  659. element.addClass('jsError');
  660. } else if (scope.node.windowPerformance) {
  661. element.addClass('windowPerformance');
  662. }
  663. element.append(getProfilerLineHTML(scope.index, scope.node));
  664. element[0].id = 'line_' + scope.index;
  665. if (scope.node.warning) {
  666. element[0].classList.add('warning');
  667. if (scope.node.queryWithoutResults) {
  668. element[0].classList.add('queryWithoutResults');
  669. }
  670. if (scope.node.jQueryCallOnEmptyObject) {
  671. element[0].classList.add('jQueryCallOnEmptyObject');
  672. }
  673. if (scope.node.eventNotDelegated) {
  674. element[0].classList.add('eventNotDelegated');
  675. }
  676. }
  677. // Bind click on the details icon
  678. var detailsIcon = element[0].querySelector('.details div');
  679. if (detailsIcon) {
  680. detailsIcon.addEventListener('click', function() {
  681. onDetailsClick(this.parentNode.parentNode);
  682. });
  683. }
  684. }
  685. };
  686. }]);
  687. function shortenUrl(url, maxLength) {
  688. if (!maxLength) {
  689. maxLength = 110;
  690. }
  691. // Why dividing by 2.1? Because it adds a 5% margin.
  692. var leftLength = Math.floor((maxLength - 5) / 2.1);
  693. var rightLength = Math.ceil((maxLength - 5) / 2.1);
  694. return (url.length > maxLength) ? url.substr(0, leftLength) + ' ... ' + url.substr(-rightLength) : url;
  695. }
  696. offendersDirectives.filter('shortenUrl', function() {
  697. return shortenUrl;
  698. });
  699. function getUrlLink(url, maxLength) {
  700. return '<a href="' + url + '" target="_blank" title="' + url + '">' + shortenUrl(url, maxLength) + '</a>';
  701. }
  702. offendersDirectives.directive('urlLink', function() {
  703. return {
  704. restrict: 'E',
  705. scope: {
  706. url: '=',
  707. maxLength: '='
  708. },
  709. template: '<a href="{{url}}" target="_blank" title="{{url}}">{{url | shortenUrl:maxLength}}</a>',
  710. replace: true
  711. };
  712. });
  713. offendersDirectives.filter('encodeURIComponent', function() {
  714. return window.encodeURIComponent;
  715. });
  716. offendersDirectives.directive('fileAndLine', function() {
  717. return {
  718. restrict: 'E',
  719. scope: {
  720. file: '=',
  721. line: '=',
  722. column: '='
  723. },
  724. template: '<span><span ng-if="file"><url-link url="file" max-length="60"></url-link></span><span ng-if="!file">&lt;inline CSS&gt;</span><span ng-if="line !== null && column !== null"> @ {{line}}:{{column}}</span></span>',
  725. replace: true
  726. };
  727. });
  728. offendersDirectives.directive('fileAndLineButton', function() {
  729. return {
  730. restrict: 'E',
  731. scope: {
  732. file: '=',
  733. line: '=',
  734. column: '='
  735. },
  736. template: '<div class="offenderButton opens">css file<div class="cssFileAndLine"><file-and-line file="file" line="line" column="column" button="true"></file-and-line></div></div>',
  737. replace: true
  738. };
  739. });
  740. offendersDirectives.filter('bytes', function() {
  741. return function(bytes) {
  742. if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) {
  743. return '-';
  744. }
  745. var kilo = bytes / 1024;
  746. if (kilo < 1) {
  747. return bytes + ' bytes';
  748. }
  749. if (kilo < 100) {
  750. return kilo.toFixed(1) + ' KB';
  751. }
  752. if (kilo < 1024) {
  753. return kilo.toFixed(0) + ' KB';
  754. }
  755. var mega = kilo / 1024;
  756. if (mega < 10) {
  757. return mega.toFixed(2) + ' MB';
  758. }
  759. return mega.toFixed(1) + ' MB';
  760. };
  761. });
  762. })();