debugbar.js 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216
  1. if (typeof(PhpDebugBar) == 'undefined') {
  2. // namespace
  3. var PhpDebugBar = {};
  4. PhpDebugBar.$ = jQuery;
  5. }
  6. (function($) {
  7. if (typeof(localStorage) == 'undefined') {
  8. // provide mock localStorage object for dumb browsers
  9. localStorage = {
  10. setItem: function(key, value) {},
  11. getItem: function(key) { return null; }
  12. };
  13. }
  14. if (typeof(PhpDebugBar.utils) == 'undefined') {
  15. PhpDebugBar.utils = {};
  16. }
  17. /**
  18. * Returns the value from an object property.
  19. * Using dots in the key, it is possible to retrieve nested property values
  20. *
  21. * @param {Object} dict
  22. * @param {String} key
  23. * @param {Object} default_value
  24. * @return {Object}
  25. */
  26. var getDictValue = PhpDebugBar.utils.getDictValue = function(dict, key, default_value) {
  27. var d = dict, parts = key.split('.');
  28. for (var i = 0; i < parts.length; i++) {
  29. if (!d[parts[i]]) {
  30. return default_value;
  31. }
  32. d = d[parts[i]];
  33. }
  34. return d;
  35. }
  36. /**
  37. * Counts the number of properties in an object
  38. *
  39. * @param {Object} obj
  40. * @return {Integer}
  41. */
  42. var getObjectSize = PhpDebugBar.utils.getObjectSize = function(obj) {
  43. if (Object.keys) {
  44. return Object.keys(obj).length;
  45. }
  46. var count = 0;
  47. for (var k in obj) {
  48. if (obj.hasOwnProperty(k)) {
  49. count++;
  50. }
  51. }
  52. return count;
  53. }
  54. /**
  55. * Returns a prefixed css class name
  56. *
  57. * @param {String} cls
  58. * @return {String}
  59. */
  60. PhpDebugBar.utils.csscls = function(cls, prefix) {
  61. if (cls.indexOf(' ') > -1) {
  62. var clss = cls.split(' '), out = [];
  63. for (var i = 0, c = clss.length; i < c; i++) {
  64. out.push(PhpDebugBar.utils.csscls(clss[i], prefix));
  65. }
  66. return out.join(' ');
  67. }
  68. if (cls.indexOf('.') === 0) {
  69. return '.' + prefix + cls.substr(1);
  70. }
  71. return prefix + cls;
  72. };
  73. /**
  74. * Creates a partial function of csscls where the second
  75. * argument is already defined
  76. *
  77. * @param {string} prefix
  78. * @return {Function}
  79. */
  80. PhpDebugBar.utils.makecsscls = function(prefix) {
  81. var f = function(cls) {
  82. return PhpDebugBar.utils.csscls(cls, prefix);
  83. };
  84. return f;
  85. }
  86. var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-');
  87. // ------------------------------------------------------------------
  88. /**
  89. * Base class for all elements with a visual component
  90. *
  91. * @param {Object} options
  92. * @constructor
  93. */
  94. var Widget = PhpDebugBar.Widget = function(options) {
  95. this._attributes = $.extend({}, this.defaults);
  96. this._boundAttributes = {};
  97. this.$el = $('<' + this.tagName + ' />');
  98. if (this.className) {
  99. this.$el.addClass(this.className);
  100. }
  101. this.initialize.apply(this, [options || {}]);
  102. this.render.apply(this);
  103. };
  104. $.extend(Widget.prototype, {
  105. tagName: 'div',
  106. className: null,
  107. defaults: {},
  108. /**
  109. * Called after the constructor
  110. *
  111. * @param {Object} options
  112. */
  113. initialize: function(options) {
  114. this.set(options);
  115. },
  116. /**
  117. * Called after the constructor to render the element
  118. */
  119. render: function() {},
  120. /**
  121. * Sets the value of an attribute
  122. *
  123. * @param {String} attr Can also be an object to set multiple attributes at once
  124. * @param {Object} value
  125. */
  126. set: function(attr, value) {
  127. if (typeof(attr) != 'string') {
  128. for (var k in attr) {
  129. this.set(k, attr[k]);
  130. }
  131. return;
  132. }
  133. this._attributes[attr] = value;
  134. if (typeof(this._boundAttributes[attr]) !== 'undefined') {
  135. for (var i = 0, c = this._boundAttributes[attr].length; i < c; i++) {
  136. this._boundAttributes[attr][i].apply(this, [value]);
  137. }
  138. }
  139. },
  140. /**
  141. * Checks if an attribute exists and is not null
  142. *
  143. * @param {String} attr
  144. * @return {[type]} [description]
  145. */
  146. has: function(attr) {
  147. return typeof(this._attributes[attr]) !== 'undefined' && this._attributes[attr] !== null;
  148. },
  149. /**
  150. * Returns the value of an attribute
  151. *
  152. * @param {String} attr
  153. * @return {Object}
  154. */
  155. get: function(attr) {
  156. return this._attributes[attr];
  157. },
  158. /**
  159. * Registers a callback function that will be called whenever the value of the attribute changes
  160. *
  161. * If cb is a jQuery element, text() will be used to fill the element
  162. *
  163. * @param {String} attr
  164. * @param {Function} cb
  165. */
  166. bindAttr: function(attr, cb) {
  167. if ($.isArray(attr)) {
  168. for (var i = 0, c = attr.length; i < c; i++) {
  169. this.bindAttr(attr[i], cb);
  170. }
  171. return;
  172. }
  173. if (typeof(this._boundAttributes[attr]) == 'undefined') {
  174. this._boundAttributes[attr] = [];
  175. }
  176. if (typeof(cb) == 'object') {
  177. var el = cb;
  178. cb = function(value) { el.text(value || ''); };
  179. }
  180. this._boundAttributes[attr].push(cb);
  181. if (this.has(attr)) {
  182. cb.apply(this, [this._attributes[attr]]);
  183. }
  184. }
  185. });
  186. /**
  187. * Creates a subclass
  188. *
  189. * Code from Backbone.js
  190. *
  191. * @param {Array} props Prototype properties
  192. * @return {Function}
  193. */
  194. Widget.extend = function(props) {
  195. var parent = this;
  196. var child = function() { return parent.apply(this, arguments); };
  197. $.extend(child, parent);
  198. var Surrogate = function() { this.constructor = child; };
  199. Surrogate.prototype = parent.prototype;
  200. child.prototype = new Surrogate;
  201. $.extend(child.prototype, props);
  202. child.__super__ = parent.prototype;
  203. return child;
  204. };
  205. // ------------------------------------------------------------------
  206. /**
  207. * Tab
  208. *
  209. * A tab is composed of a tab label which is always visible and
  210. * a tab panel which is visible only when the tab is active.
  211. *
  212. * The panel must contain a widget. A widget is an object which has
  213. * an element property containing something appendable to a jQuery object.
  214. *
  215. * Options:
  216. * - title
  217. * - badge
  218. * - widget
  219. * - data: forward data to widget data
  220. */
  221. var Tab = Widget.extend({
  222. className: csscls('panel'),
  223. render: function() {
  224. this.$tab = $('<a />').addClass(csscls('tab'));
  225. this.$icon = $('<i />').appendTo(this.$tab);
  226. this.bindAttr('icon', function(icon) {
  227. if (icon) {
  228. this.$icon.attr('class', 'phpdebugbar-fa phpdebugbar-fa-' + icon);
  229. } else {
  230. this.$icon.attr('class', '');
  231. }
  232. });
  233. this.bindAttr('title', $('<span />').addClass(csscls('text')).appendTo(this.$tab));
  234. this.$badge = $('<span />').addClass(csscls('badge')).appendTo(this.$tab);
  235. this.bindAttr('badge', function(value) {
  236. if (value !== null) {
  237. this.$badge.text(value);
  238. this.$badge.addClass(csscls('visible'));
  239. } else {
  240. this.$badge.removeClass(csscls('visible'));
  241. }
  242. });
  243. this.bindAttr('widget', function(widget) {
  244. this.$el.empty().append(widget.$el);
  245. });
  246. this.bindAttr('data', function(data) {
  247. if (this.has('widget')) {
  248. this.get('widget').set('data', data);
  249. }
  250. })
  251. }
  252. });
  253. // ------------------------------------------------------------------
  254. /**
  255. * Indicator
  256. *
  257. * An indicator is a text and an icon to display single value information
  258. * right inside the always visible part of the debug bar
  259. *
  260. * Options:
  261. * - icon
  262. * - title
  263. * - tooltip
  264. * - data: alias of title
  265. */
  266. var Indicator = Widget.extend({
  267. tagName: 'span',
  268. className: csscls('indicator'),
  269. render: function() {
  270. this.$icon = $('<i />').appendTo(this.$el);
  271. this.bindAttr('icon', function(icon) {
  272. if (icon) {
  273. this.$icon.attr('class', 'phpdebugbar-fa phpdebugbar-fa-' + icon);
  274. } else {
  275. this.$icon.attr('class', '');
  276. }
  277. });
  278. this.bindAttr(['title', 'data'], $('<span />').addClass(csscls('text')).appendTo(this.$el));
  279. this.$tooltip = $('<span />').addClass(csscls('tooltip disabled')).appendTo(this.$el);
  280. this.bindAttr('tooltip', function(tooltip) {
  281. if (tooltip) {
  282. this.$tooltip.text(tooltip).removeClass(csscls('disabled'));
  283. } else {
  284. this.$tooltip.addClass(csscls('disabled'));
  285. }
  286. });
  287. }
  288. });
  289. // ------------------------------------------------------------------
  290. /**
  291. * Dataset title formater
  292. *
  293. * Formats the title of a dataset for the select box
  294. */
  295. var DatasetTitleFormater = PhpDebugBar.DatasetTitleFormater = function(debugbar) {
  296. this.debugbar = debugbar;
  297. };
  298. $.extend(DatasetTitleFormater.prototype, {
  299. /**
  300. * Formats the title of a dataset
  301. *
  302. * @this {DatasetTitleFormater}
  303. * @param {String} id
  304. * @param {Object} data
  305. * @param {String} suffix
  306. * @return {String}
  307. */
  308. format: function(id, data, suffix) {
  309. if (suffix) {
  310. suffix = ' ' + suffix;
  311. } else {
  312. suffix = '';
  313. }
  314. var nb = getObjectSize(this.debugbar.datasets) + 1;
  315. if (typeof(data['__meta']) === 'undefined') {
  316. return "#" + nb + suffix;
  317. }
  318. var uri = data['__meta']['uri'], filename;
  319. if (uri.length && uri.charAt(uri.length - 1) === '/') {
  320. // URI ends in a trailing /: get the portion before then to avoid returning an empty string
  321. filename = uri.substr(0, uri.length - 1); // strip trailing '/'
  322. filename = filename.substr(filename.lastIndexOf('/') + 1); // get last path segment
  323. filename += '/'; // add the trailing '/' back
  324. } else {
  325. filename = uri.substr(uri.lastIndexOf('/') + 1);
  326. }
  327. // truncate the filename in the label, if it's too long
  328. var maxLength = 150;
  329. if (filename.length > maxLength) {
  330. filename = filename.substr(0, maxLength) + '...';
  331. }
  332. var label = "#" + nb + " " + filename + suffix + ' (' + data['__meta']['datetime'].split(' ')[1] + ')';
  333. return label;
  334. }
  335. });
  336. // ------------------------------------------------------------------
  337. /**
  338. * DebugBar
  339. *
  340. * Creates a bar that appends itself to the body of your page
  341. * and sticks to the bottom.
  342. *
  343. * The bar can be customized by adding tabs and indicators.
  344. * A data map is used to fill those controls with data provided
  345. * from datasets.
  346. */
  347. var DebugBar = PhpDebugBar.DebugBar = Widget.extend({
  348. className: "phpdebugbar " + csscls('minimized'),
  349. options: {
  350. bodyMarginBottom: true,
  351. bodyMarginBottomHeight: 0
  352. },
  353. initialize: function() {
  354. this.controls = {};
  355. this.dataMap = {};
  356. this.datasets = {};
  357. this.firstTabName = null;
  358. this.activePanelName = null;
  359. this.datesetTitleFormater = new DatasetTitleFormater(this);
  360. this.options.bodyMarginBottomHeight = parseInt($('body').css('margin-bottom'));
  361. this.registerResizeHandler();
  362. },
  363. /**
  364. * Register resize event, for resize debugbar with reponsive css.
  365. *
  366. * @this {DebugBar}
  367. */
  368. registerResizeHandler: function() {
  369. if (typeof this.resize.bind == 'undefined') return;
  370. var f = this.resize.bind(this);
  371. this.respCSSSize = 0;
  372. $(window).resize(f);
  373. setTimeout(f, 20);
  374. },
  375. /**
  376. * Resizes the debugbar to fit the current browser window
  377. */
  378. resize: function() {
  379. var contentSize = this.respCSSSize;
  380. if (this.respCSSSize == 0) {
  381. this.$header.find("> div > *:visible").each(function () {
  382. contentSize += $(this).outerWidth();
  383. });
  384. }
  385. var currentSize = this.$header.width();
  386. var cssClass = "phpdebugbar-mini-design";
  387. var bool = this.$header.hasClass(cssClass);
  388. if (currentSize <= contentSize && !bool) {
  389. this.respCSSSize = contentSize;
  390. this.$header.addClass(cssClass);
  391. } else if (contentSize < currentSize && bool) {
  392. this.respCSSSize = 0;
  393. this.$header.removeClass(cssClass);
  394. }
  395. // Reset height to ensure bar is still visible
  396. this.setHeight(this.$body.height());
  397. },
  398. /**
  399. * Initialiazes the UI
  400. *
  401. * @this {DebugBar}
  402. */
  403. render: function() {
  404. var self = this;
  405. this.$el.appendTo('body');
  406. this.$dragCapture = $('<div />').addClass(csscls('drag-capture')).appendTo(this.$el);
  407. this.$resizehdle = $('<div />').addClass(csscls('resize-handle')).appendTo(this.$el);
  408. this.$header = $('<div />').addClass(csscls('header')).appendTo(this.$el);
  409. this.$headerLeft = $('<div />').addClass(csscls('header-left')).appendTo(this.$header);
  410. this.$headerRight = $('<div />').addClass(csscls('header-right')).appendTo(this.$header);
  411. var $body = this.$body = $('<div />').addClass(csscls('body')).appendTo(this.$el);
  412. this.recomputeBottomOffset();
  413. // dragging of resize handle
  414. var pos_y, orig_h;
  415. this.$resizehdle.on('mousedown', function(e) {
  416. orig_h = $body.height(), pos_y = e.pageY;
  417. $body.parents().on('mousemove', mousemove).on('mouseup', mouseup);
  418. self.$dragCapture.show();
  419. e.preventDefault();
  420. });
  421. var mousemove = function(e) {
  422. var h = orig_h + (pos_y - e.pageY);
  423. self.setHeight(h);
  424. };
  425. var mouseup = function() {
  426. $body.parents().off('mousemove', mousemove).off('mouseup', mouseup);
  427. self.$dragCapture.hide();
  428. };
  429. // close button
  430. this.$closebtn = $('<a />').addClass(csscls('close-btn')).appendTo(this.$headerRight);
  431. this.$closebtn.click(function() {
  432. self.close();
  433. });
  434. // minimize button
  435. this.$minimizebtn = $('<a />').addClass(csscls('minimize-btn') ).appendTo(this.$headerRight);
  436. this.$minimizebtn.click(function() {
  437. self.minimize();
  438. });
  439. // maximize button
  440. this.$maximizebtn = $('<a />').addClass(csscls('maximize-btn') ).appendTo(this.$headerRight);
  441. this.$maximizebtn.click(function() {
  442. self.restore();
  443. });
  444. // restore button
  445. this.$restorebtn = $('<a />').addClass(csscls('restore-btn')).hide().appendTo(this.$el);
  446. this.$restorebtn.click(function() {
  447. self.restore();
  448. });
  449. // open button
  450. this.$openbtn = $('<a />').addClass(csscls('open-btn')).appendTo(this.$headerRight).hide();
  451. this.$openbtn.click(function() {
  452. self.openHandler.show(function(id, dataset) {
  453. self.addDataSet(dataset, id, "(opened)");
  454. self.showTab();
  455. });
  456. });
  457. // select box for data sets
  458. this.$datasets = $('<select />').addClass(csscls('datasets-switcher')).appendTo(this.$headerRight);
  459. this.$datasets.change(function() {
  460. self.dataChangeHandler(self.datasets[this.value]);
  461. self.showTab();
  462. });
  463. },
  464. /**
  465. * Sets the height of the debugbar body section
  466. * Forces the height to lie within a reasonable range
  467. * Stores the height in local storage so it can be restored
  468. * Resets the document body bottom offset
  469. *
  470. * @this {DebugBar}
  471. */
  472. setHeight: function(height) {
  473. var min_h = 40;
  474. var max_h = $(window).innerHeight() - this.$header.height() - 10;
  475. height = Math.min(height, max_h);
  476. height = Math.max(height, min_h);
  477. this.$body.css('height', height);
  478. localStorage.setItem('phpdebugbar-height', height);
  479. this.recomputeBottomOffset();
  480. },
  481. /**
  482. * Restores the state of the DebugBar using localStorage
  483. * This is not called by default in the constructor and
  484. * needs to be called by subclasses in their init() method
  485. *
  486. * @this {DebugBar}
  487. */
  488. restoreState: function() {
  489. // bar height
  490. var height = localStorage.getItem('phpdebugbar-height');
  491. this.setHeight(height || this.$body.height());
  492. // bar visibility
  493. var open = localStorage.getItem('phpdebugbar-open');
  494. if (open && open == '0') {
  495. this.close();
  496. } else {
  497. var visible = localStorage.getItem('phpdebugbar-visible');
  498. if (visible && visible == '1') {
  499. var tab = localStorage.getItem('phpdebugbar-tab');
  500. if (this.isTab(tab)) {
  501. this.showTab(tab);
  502. }
  503. }
  504. }
  505. },
  506. /**
  507. * Creates and adds a new tab
  508. *
  509. * @this {DebugBar}
  510. * @param {String} name Internal name
  511. * @param {Object} widget A widget object with an element property
  512. * @param {String} title The text in the tab, if not specified, name will be used
  513. * @return {Tab}
  514. */
  515. createTab: function(name, widget, title) {
  516. var tab = new Tab({
  517. title: title || (name.replace(/[_\-]/g, ' ').charAt(0).toUpperCase() + name.slice(1)),
  518. widget: widget
  519. });
  520. return this.addTab(name, tab);
  521. },
  522. /**
  523. * Adds a new tab
  524. *
  525. * @this {DebugBar}
  526. * @param {String} name Internal name
  527. * @param {Tab} tab Tab object
  528. * @return {Tab}
  529. */
  530. addTab: function(name, tab) {
  531. if (this.isControl(name)) {
  532. throw new Error(name + ' already exists');
  533. }
  534. var self = this;
  535. tab.$tab.appendTo(this.$headerLeft).click(function() {
  536. if (!self.isMinimized() && self.activePanelName == name) {
  537. self.minimize();
  538. } else {
  539. self.showTab(name);
  540. }
  541. });
  542. tab.$el.appendTo(this.$body);
  543. this.controls[name] = tab;
  544. if (this.firstTabName == null) {
  545. this.firstTabName = name;
  546. }
  547. return tab;
  548. },
  549. /**
  550. * Creates and adds an indicator
  551. *
  552. * @this {DebugBar}
  553. * @param {String} name Internal name
  554. * @param {String} icon
  555. * @param {String} tooltip
  556. * @param {String} position "right" or "left", default is "right"
  557. * @return {Indicator}
  558. */
  559. createIndicator: function(name, icon, tooltip, position) {
  560. var indicator = new Indicator({
  561. icon: icon,
  562. tooltip: tooltip
  563. });
  564. return this.addIndicator(name, indicator, position);
  565. },
  566. /**
  567. * Adds an indicator
  568. *
  569. * @this {DebugBar}
  570. * @param {String} name Internal name
  571. * @param {Indicator} indicator Indicator object
  572. * @return {Indicator}
  573. */
  574. addIndicator: function(name, indicator, position) {
  575. if (this.isControl(name)) {
  576. throw new Error(name + ' already exists');
  577. }
  578. if (position == 'left') {
  579. indicator.$el.insertBefore(this.$headerLeft.children().first());
  580. } else {
  581. indicator.$el.appendTo(this.$headerRight);
  582. }
  583. this.controls[name] = indicator;
  584. return indicator;
  585. },
  586. /**
  587. * Returns a control
  588. *
  589. * @param {String} name
  590. * @return {Object}
  591. */
  592. getControl: function(name) {
  593. if (this.isControl(name)) {
  594. return this.controls[name];
  595. }
  596. },
  597. /**
  598. * Checks if there's a control under the specified name
  599. *
  600. * @this {DebugBar}
  601. * @param {String} name
  602. * @return {Boolean}
  603. */
  604. isControl: function(name) {
  605. return typeof(this.controls[name]) != 'undefined';
  606. },
  607. /**
  608. * Checks if a tab with the specified name exists
  609. *
  610. * @this {DebugBar}
  611. * @param {String} name
  612. * @return {Boolean}
  613. */
  614. isTab: function(name) {
  615. return this.isControl(name) && this.controls[name] instanceof Tab;
  616. },
  617. /**
  618. * Checks if an indicator with the specified name exists
  619. *
  620. * @this {DebugBar}
  621. * @param {String} name
  622. * @return {Boolean}
  623. */
  624. isIndicator: function(name) {
  625. return this.isControl(name) && this.controls[name] instanceof Indicator;
  626. },
  627. /**
  628. * Removes all tabs and indicators from the debug bar and hides it
  629. *
  630. * @this {DebugBar}
  631. */
  632. reset: function() {
  633. this.minimize();
  634. var self = this;
  635. $.each(this.controls, function(name, control) {
  636. if (self.isTab(name)) {
  637. control.$tab.remove();
  638. }
  639. control.$el.remove();
  640. });
  641. this.controls = {};
  642. },
  643. /**
  644. * Open the debug bar and display the specified tab
  645. *
  646. * @this {DebugBar}
  647. * @param {String} name If not specified, display the first tab
  648. */
  649. showTab: function(name) {
  650. if (!name) {
  651. if (this.activePanelName) {
  652. name = this.activePanelName;
  653. } else {
  654. name = this.firstTabName;
  655. }
  656. }
  657. if (!this.isTab(name)) {
  658. throw new Error("Unknown tab '" + name + "'");
  659. }
  660. this.$resizehdle.show();
  661. this.$body.show();
  662. this.recomputeBottomOffset();
  663. $(this.$header).find('> div > .' + csscls('active')).removeClass(csscls('active'));
  664. $(this.$body).find('> .' + csscls('active')).removeClass(csscls('active'));
  665. this.controls[name].$tab.addClass(csscls('active'));
  666. this.controls[name].$el.addClass(csscls('active'));
  667. this.activePanelName = name;
  668. this.$el.removeClass(csscls('minimized'));
  669. localStorage.setItem('phpdebugbar-visible', '1');
  670. localStorage.setItem('phpdebugbar-tab', name);
  671. this.resize();
  672. },
  673. /**
  674. * Hide panels and minimize the debug bar
  675. *
  676. * @this {DebugBar}
  677. */
  678. minimize: function() {
  679. this.$header.find('> div > .' + csscls('active')).removeClass(csscls('active'));
  680. this.$body.hide();
  681. this.$resizehdle.hide();
  682. this.recomputeBottomOffset();
  683. localStorage.setItem('phpdebugbar-visible', '0');
  684. this.$el.addClass(csscls('minimized'));
  685. this.resize();
  686. },
  687. /**
  688. * Checks if the panel is minimized
  689. *
  690. * @return {Boolean}
  691. */
  692. isMinimized: function() {
  693. return this.$el.hasClass(csscls('minimized'));
  694. },
  695. /**
  696. * Close the debug bar
  697. *
  698. * @this {DebugBar}
  699. */
  700. close: function() {
  701. this.$resizehdle.hide();
  702. this.$header.hide();
  703. this.$body.hide();
  704. this.$restorebtn.show();
  705. localStorage.setItem('phpdebugbar-open', '0');
  706. this.$el.addClass(csscls('closed'));
  707. this.recomputeBottomOffset();
  708. },
  709. /**
  710. * Checks if the panel is closed
  711. *
  712. * @return {Boolean}
  713. */
  714. isClosed: function() {
  715. return this.$el.hasClass(csscls('closed'));
  716. },
  717. /**
  718. * Restore the debug bar
  719. *
  720. * @this {DebugBar}
  721. */
  722. restore: function() {
  723. this.$resizehdle.show();
  724. this.$header.show();
  725. this.$restorebtn.hide();
  726. localStorage.setItem('phpdebugbar-open', '1');
  727. var tab = localStorage.getItem('phpdebugbar-tab');
  728. if (this.isTab(tab)) {
  729. this.showTab(tab);
  730. } else {
  731. this.showTab();
  732. }
  733. this.$el.removeClass(csscls('closed'));
  734. this.resize();
  735. },
  736. /**
  737. * Recomputes the margin-bottom css property of the body so
  738. * that the debug bar never hides any content
  739. */
  740. recomputeBottomOffset: function() {
  741. if (this.options.bodyMarginBottom) {
  742. if (this.isClosed()) {
  743. return $('body').css('margin-bottom', this.options.bodyMarginBottomHeight || '');
  744. }
  745. var offset = parseInt(this.$el.height()) + (this.options.bodyMarginBottomHeight || 0);
  746. $('body').css('margin-bottom', offset);
  747. }
  748. },
  749. /**
  750. * Sets the data map used by dataChangeHandler to populate
  751. * indicators and widgets
  752. *
  753. * A data map is an object where properties are control names.
  754. * The value of each property should be an array where the first
  755. * item is the name of a property from the data object (nested properties
  756. * can be specified) and the second item the default value.
  757. *
  758. * Example:
  759. * {"memory": ["memory.peak_usage_str", "0B"]}
  760. *
  761. * @this {DebugBar}
  762. * @param {Object} map
  763. */
  764. setDataMap: function(map) {
  765. this.dataMap = map;
  766. },
  767. /**
  768. * Same as setDataMap() but appends to the existing map
  769. * rather than replacing it
  770. *
  771. * @this {DebugBar}
  772. * @param {Object} map
  773. */
  774. addDataMap: function(map) {
  775. $.extend(this.dataMap, map);
  776. },
  777. /**
  778. * Resets datasets and add one set of data
  779. *
  780. * For this method to be usefull, you need to specify
  781. * a dataMap using setDataMap()
  782. *
  783. * @this {DebugBar}
  784. * @param {Object} data
  785. * @return {String} Dataset's id
  786. */
  787. setData: function(data) {
  788. this.datasets = {};
  789. return this.addDataSet(data);
  790. },
  791. /**
  792. * Adds a dataset
  793. *
  794. * If more than one dataset are added, the dataset selector
  795. * will be displayed.
  796. *
  797. * For this method to be usefull, you need to specify
  798. * a dataMap using setDataMap()
  799. *
  800. * @this {DebugBar}
  801. * @param {Object} data
  802. * @param {String} id The name of this set, optional
  803. * @param {String} suffix
  804. * @param {Bool} show Whether to show the new dataset, optional (default: true)
  805. * @return {String} Dataset's id
  806. */
  807. addDataSet: function(data, id, suffix, show) {
  808. var label = this.datesetTitleFormater.format(id, data, suffix);
  809. id = id || (getObjectSize(this.datasets) + 1);
  810. this.datasets[id] = data;
  811. this.$datasets.append($('<option value="' + id + '">' + label + '</option>'));
  812. if (this.$datasets.children().length > 1) {
  813. this.$datasets.show();
  814. }
  815. if (typeof(show) == 'undefined' || show) {
  816. this.showDataSet(id);
  817. }
  818. return id;
  819. },
  820. /**
  821. * Loads a dataset using the open handler
  822. *
  823. * @param {String} id
  824. * @param {Bool} show Whether to show the new dataset, optional (default: true)
  825. */
  826. loadDataSet: function(id, suffix, callback, show) {
  827. if (!this.openHandler) {
  828. throw new Error('loadDataSet() needs an open handler');
  829. }
  830. var self = this;
  831. this.openHandler.load(id, function(data) {
  832. self.addDataSet(data, id, suffix, show);
  833. self.resize();
  834. callback && callback(data);
  835. });
  836. },
  837. /**
  838. * Returns the data from a dataset
  839. *
  840. * @this {DebugBar}
  841. * @param {String} id
  842. * @return {Object}
  843. */
  844. getDataSet: function(id) {
  845. return this.datasets[id];
  846. },
  847. /**
  848. * Switch the currently displayed dataset
  849. *
  850. * @this {DebugBar}
  851. * @param {String} id
  852. */
  853. showDataSet: function(id) {
  854. this.dataChangeHandler(this.datasets[id]);
  855. this.$datasets.val(id);
  856. },
  857. /**
  858. * Called when the current dataset is modified.
  859. *
  860. * @this {DebugBar}
  861. * @param {Object} data
  862. */
  863. dataChangeHandler: function(data) {
  864. var self = this;
  865. $.each(this.dataMap, function(key, def) {
  866. var d = getDictValue(data, def[0], def[1]);
  867. if (key.indexOf(':') != -1) {
  868. key = key.split(':');
  869. self.getControl(key[0]).set(key[1], d);
  870. } else {
  871. self.getControl(key).set('data', d);
  872. }
  873. });
  874. },
  875. /**
  876. * Sets the handler to open past dataset
  877. *
  878. * @this {DebugBar}
  879. * @param {object} handler
  880. */
  881. setOpenHandler: function(handler) {
  882. this.openHandler = handler;
  883. if (handler !== null) {
  884. this.$openbtn.show();
  885. } else {
  886. this.$openbtn.hide();
  887. }
  888. },
  889. /**
  890. * Returns the handler to open past dataset
  891. *
  892. * @this {DebugBar}
  893. * @return {object}
  894. */
  895. getOpenHandler: function() {
  896. return this.openHandler;
  897. }
  898. });
  899. DebugBar.Tab = Tab;
  900. DebugBar.Indicator = Indicator;
  901. // ------------------------------------------------------------------
  902. /**
  903. * AjaxHandler
  904. *
  905. * Extract data from headers of an XMLHttpRequest and adds a new dataset
  906. *
  907. * @param {Bool} autoShow Whether to immediately show new datasets, optional (default: true)
  908. */
  909. var AjaxHandler = PhpDebugBar.AjaxHandler = function(debugbar, headerName, autoShow) {
  910. this.debugbar = debugbar;
  911. this.headerName = headerName || 'phpdebugbar';
  912. this.autoShow = typeof(autoShow) == 'undefined' ? true : autoShow;
  913. };
  914. $.extend(AjaxHandler.prototype, {
  915. /**
  916. * Handles a Fetch API Response or an XMLHttpRequest
  917. *
  918. * @this {AjaxHandler}
  919. * @param {Response|XMLHttpRequest} response
  920. * @return {Bool}
  921. */
  922. handle: function(response) {
  923. // Check if the debugbar header is available
  924. if (this.isFetch(response) && !response.headers.has(this.headerName + '-id')) {
  925. return true;
  926. } else if (this.isXHR(response) && response.getAllResponseHeaders().indexOf(this.headerName) === -1) {
  927. return true;
  928. }
  929. if (!this.loadFromId(response)) {
  930. return this.loadFromData(response);
  931. }
  932. return true;
  933. },
  934. getHeader: function(response, header) {
  935. if (this.isFetch(response)) {
  936. return response.headers.get(header)
  937. }
  938. return response.getResponseHeader(header)
  939. },
  940. isFetch: function(response) {
  941. return Object.prototype.toString.call(response) == '[object Response]'
  942. },
  943. isXHR: function(response) {
  944. return Object.prototype.toString.call(response) == '[object XMLHttpRequest]'
  945. },
  946. /**
  947. * Checks if the HEADER-id exists and loads the dataset using the open handler
  948. *
  949. * @param {Response|XMLHttpRequest} response
  950. * @return {Bool}
  951. */
  952. loadFromId: function(response) {
  953. var id = this.extractIdFromHeaders(response);
  954. if (id && this.debugbar.openHandler) {
  955. this.debugbar.loadDataSet(id, "(ajax)", undefined, this.autoShow);
  956. return true;
  957. }
  958. return false;
  959. },
  960. /**
  961. * Extracts the id from the HEADER-id
  962. *
  963. * @param {Response|XMLHttpRequest} response
  964. * @return {String}
  965. */
  966. extractIdFromHeaders: function(response) {
  967. return this.getHeader(response, this.headerName + '-id');
  968. },
  969. /**
  970. * Checks if the HEADER exists and loads the dataset
  971. *
  972. * @param {Response|XMLHttpRequest} response
  973. * @return {Bool}
  974. */
  975. loadFromData: function(response) {
  976. var raw = this.extractDataFromHeaders(response);
  977. if (!raw) {
  978. return false;
  979. }
  980. var data = this.parseHeaders(raw);
  981. if (data.error) {
  982. throw new Error('Error loading debugbar data: ' + data.error);
  983. } else if(data.data) {
  984. this.debugbar.addDataSet(data.data, data.id, "(ajax)", this.autoShow);
  985. }
  986. return true;
  987. },
  988. /**
  989. * Extract the data as a string from headers of an XMLHttpRequest
  990. *
  991. * @this {AjaxHandler}
  992. * @param {Response|XMLHttpRequest} response
  993. * @return {string}
  994. */
  995. extractDataFromHeaders: function(response) {
  996. var data = this.getHeader(response, this.headerName);
  997. if (!data) {
  998. return;
  999. }
  1000. for (var i = 1;; i++) {
  1001. var header = this.getHeader(response, this.headerName + '-' + i);
  1002. if (!header) {
  1003. break;
  1004. }
  1005. data += header;
  1006. }
  1007. return decodeURIComponent(data);
  1008. },
  1009. /**
  1010. * Parses the string data into an object
  1011. *
  1012. * @this {AjaxHandler}
  1013. * @param {string} data
  1014. * @return {string}
  1015. */
  1016. parseHeaders: function(data) {
  1017. return JSON.parse(data);
  1018. },
  1019. /**
  1020. * Attaches an event listener to fetch
  1021. *
  1022. * @this {AjaxHandler}
  1023. */
  1024. bindToFetch: function() {
  1025. var self = this;
  1026. var proxied = window.fetch;
  1027. if (proxied !== undefined && proxied.polyfill !== undefined) {
  1028. return;
  1029. }
  1030. window.fetch = function () {
  1031. var promise = proxied.apply(this, arguments);
  1032. promise.then(function (response) {
  1033. self.handle(response);
  1034. });
  1035. return promise;
  1036. };
  1037. },
  1038. /**
  1039. * Attaches an event listener to jQuery.ajaxComplete()
  1040. *
  1041. * @this {AjaxHandler}
  1042. * @param {jQuery} jq Optional
  1043. */
  1044. bindToJquery: function(jq) {
  1045. var self = this;
  1046. jq(document).ajaxComplete(function(e, xhr, settings) {
  1047. if (!settings.ignoreDebugBarAjaxHandler) {
  1048. self.handle(xhr);
  1049. }
  1050. });
  1051. },
  1052. /**
  1053. * Attaches an event listener to XMLHttpRequest
  1054. *
  1055. * @this {AjaxHandler}
  1056. */
  1057. bindToXHR: function() {
  1058. var self = this;
  1059. var proxied = XMLHttpRequest.prototype.open;
  1060. XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {
  1061. var xhr = this;
  1062. this.addEventListener("readystatechange", function() {
  1063. var skipUrl = self.debugbar.openHandler ? self.debugbar.openHandler.get('url') : null;
  1064. if (xhr.readyState == 4 && url.indexOf(skipUrl) !== 0) {
  1065. self.handle(xhr);
  1066. }
  1067. }, false);
  1068. proxied.apply(this, Array.prototype.slice.call(arguments));
  1069. };
  1070. }
  1071. });
  1072. })(PhpDebugBar.$);