1if (typeof(PhpDebugBar) == 'undefined') { 2 // namespace 3 var PhpDebugBar = {}; 4 PhpDebugBar.$ = jQuery; 5} 6 7(function($) { 8 9 if (typeof(localStorage) == 'undefined') { 10 // provide mock localStorage object for dumb browsers 11 localStorage = { 12 setItem: function(key, value) {}, 13 getItem: function(key) { return null; } 14 }; 15 } 16 17 if (typeof(PhpDebugBar.utils) == 'undefined') { 18 PhpDebugBar.utils = {}; 19 } 20 21 /** 22 * Returns the value from an object property. 23 * Using dots in the key, it is possible to retrieve nested property values 24 * 25 * @param {Object} dict 26 * @param {String} key 27 * @param {Object} default_value 28 * @return {Object} 29 */ 30 var getDictValue = PhpDebugBar.utils.getDictValue = function(dict, key, default_value) { 31 var d = dict, parts = key.split('.'); 32 for (var i = 0; i < parts.length; i++) { 33 if (!d[parts[i]]) { 34 return default_value; 35 } 36 d = d[parts[i]]; 37 } 38 return d; 39 } 40 41 /** 42 * Counts the number of properties in an object 43 * 44 * @param {Object} obj 45 * @return {Integer} 46 */ 47 var getObjectSize = PhpDebugBar.utils.getObjectSize = function(obj) { 48 if (Object.keys) { 49 return Object.keys(obj).length; 50 } 51 var count = 0; 52 for (var k in obj) { 53 if (obj.hasOwnProperty(k)) { 54 count++; 55 } 56 } 57 return count; 58 } 59 60 /** 61 * Returns a prefixed css class name 62 * 63 * @param {String} cls 64 * @return {String} 65 */ 66 PhpDebugBar.utils.csscls = function(cls, prefix) { 67 if (cls.indexOf(' ') > -1) { 68 var clss = cls.split(' '), out = []; 69 for (var i = 0, c = clss.length; i < c; i++) { 70 out.push(PhpDebugBar.utils.csscls(clss[i], prefix)); 71 } 72 return out.join(' '); 73 } 74 if (cls.indexOf('.') === 0) { 75 return '.' + prefix + cls.substr(1); 76 } 77 return prefix + cls; 78 }; 79 80 /** 81 * Creates a partial function of csscls where the second 82 * argument is already defined 83 * 84 * @param {string} prefix 85 * @return {Function} 86 */ 87 PhpDebugBar.utils.makecsscls = function(prefix) { 88 var f = function(cls) { 89 return PhpDebugBar.utils.csscls(cls, prefix); 90 }; 91 return f; 92 } 93 94 var csscls = PhpDebugBar.utils.makecsscls('phpdebugbar-'); 95 96 97 // ------------------------------------------------------------------ 98 99 /** 100 * Base class for all elements with a visual component 101 * 102 * @param {Object} options 103 * @constructor 104 */ 105 var Widget = PhpDebugBar.Widget = function(options) { 106 this._attributes = $.extend({}, this.defaults); 107 this._boundAttributes = {}; 108 this.$el = $('<' + this.tagName + ' />'); 109 if (this.className) { 110 this.$el.addClass(this.className); 111 } 112 this.initialize.apply(this, [options || {}]); 113 this.render.apply(this); 114 }; 115 116 $.extend(Widget.prototype, { 117 118 tagName: 'div', 119 120 className: null, 121 122 defaults: {}, 123 124 /** 125 * Called after the constructor 126 * 127 * @param {Object} options 128 */ 129 initialize: function(options) { 130 this.set(options); 131 }, 132 133 /** 134 * Called after the constructor to render the element 135 */ 136 render: function() {}, 137 138 /** 139 * Sets the value of an attribute 140 * 141 * @param {String} attr Can also be an object to set multiple attributes at once 142 * @param {Object} value 143 */ 144 set: function(attr, value) { 145 if (typeof(attr) != 'string') { 146 for (var k in attr) { 147 this.set(k, attr[k]); 148 } 149 return; 150 } 151 152 this._attributes[attr] = value; 153 if (typeof(this._boundAttributes[attr]) !== 'undefined') { 154 for (var i = 0, c = this._boundAttributes[attr].length; i < c; i++) { 155 this._boundAttributes[attr][i].apply(this, [value]); 156 } 157 } 158 }, 159 160 /** 161 * Checks if an attribute exists and is not null 162 * 163 * @param {String} attr 164 * @return {[type]} [description] 165 */ 166 has: function(attr) { 167 return typeof(this._attributes[attr]) !== 'undefined' && this._attributes[attr] !== null; 168 }, 169 170 /** 171 * Returns the value of an attribute 172 * 173 * @param {String} attr 174 * @return {Object} 175 */ 176 get: function(attr) { 177 return this._attributes[attr]; 178 }, 179 180 /** 181 * Registers a callback function that will be called whenever the value of the attribute changes 182 * 183 * If cb is a jQuery element, text() will be used to fill the element 184 * 185 * @param {String} attr 186 * @param {Function} cb 187 */ 188 bindAttr: function(attr, cb) { 189 if (Array.isArray(attr)) { 190 for (var i = 0, c = attr.length; i < c; i++) { 191 this.bindAttr(attr[i], cb); 192 } 193 return; 194 } 195 196 if (typeof(this._boundAttributes[attr]) == 'undefined') { 197 this._boundAttributes[attr] = []; 198 } 199 if (typeof(cb) == 'object') { 200 var el = cb; 201 cb = function(value) { el.text(value || ''); }; 202 } 203 this._boundAttributes[attr].push(cb); 204 if (this.has(attr)) { 205 cb.apply(this, [this._attributes[attr]]); 206 } 207 } 208 209 }); 210 211 212 /** 213 * Creates a subclass 214 * 215 * Code from Backbone.js 216 * 217 * @param {Array} props Prototype properties 218 * @return {Function} 219 */ 220 Widget.extend = function(props) { 221 var parent = this; 222 223 var child = function() { return parent.apply(this, arguments); }; 224 $.extend(child, parent); 225 226 var Surrogate = function() { this.constructor = child; }; 227 Surrogate.prototype = parent.prototype; 228 child.prototype = new Surrogate; 229 $.extend(child.prototype, props); 230 231 child.__super__ = parent.prototype; 232 233 return child; 234 }; 235 236 // ------------------------------------------------------------------ 237 238 /** 239 * Tab 240 * 241 * A tab is composed of a tab label which is always visible and 242 * a tab panel which is visible only when the tab is active. 243 * 244 * The panel must contain a widget. A widget is an object which has 245 * an element property containing something appendable to a jQuery object. 246 * 247 * Options: 248 * - title 249 * - badge 250 * - widget 251 * - data: forward data to widget data 252 */ 253 var Tab = Widget.extend({ 254 255 className: csscls('panel'), 256 257 render: function() { 258 this.$tab = $('<a />').addClass(csscls('tab')); 259 this.$icon = $('<i />').appendTo(this.$tab); 260 this.bindAttr('icon', function(icon) { 261 if (icon) { 262 this.$icon.attr('class', 'phpdebugbar-fa phpdebugbar-fa-' + icon); 263 } else { 264 this.$icon.attr('class', ''); 265 } 266 }); 267 268 this.bindAttr('title', $('<span />').addClass(csscls('text')).appendTo(this.$tab)); 269 270 this.$badge = $('<span />').addClass(csscls('badge')).appendTo(this.$tab); 271 this.bindAttr('badge', function(value) { 272 if (value !== null) { 273 this.$badge.text(value); 274 this.$badge.addClass(csscls('visible')); 275 } else { 276 this.$badge.removeClass(csscls('visible')); 277 } 278 }); 279 280 this.bindAttr('widget', function(widget) { 281 this.$el.empty().append(widget.$el); 282 }); 283 284 this.bindAttr('data', function(data) { 285 if (this.has('widget')) { 286 this.get('widget').set('data', data); 287 this.$tab.attr('data-empty', $.isEmptyObject(data) || data.count === 0); 288 } 289 }) 290 } 291 292 }); 293 294 // ------------------------------------------------------------------ 295 296 /** 297 * Indicator 298 * 299 * An indicator is a text and an icon to display single value information 300 * right inside the always visible part of the debug bar 301 * 302 * Options: 303 * - icon 304 * - title 305 * - tooltip 306 * - data: alias of title 307 */ 308 var Indicator = Widget.extend({ 309 310 tagName: 'span', 311 312 className: csscls('indicator'), 313 314 render: function() { 315 this.$icon = $('<i />').appendTo(this.$el); 316 this.bindAttr('icon', function(icon) { 317 if (icon) { 318 this.$icon.attr('class', 'phpdebugbar-fa phpdebugbar-fa-' + icon); 319 } else { 320 this.$icon.attr('class', ''); 321 } 322 }); 323 324 this.bindAttr('link', function(link) { 325 if (link) { 326 this.$el.on('click', () => { 327 this.get('debugbar').showTab(link); 328 }).css('cursor', 'pointer') 329 } else { 330 this.$el.off('click', false).css('cursor', '') 331 } 332 }); 333 334 this.bindAttr(['title', 'data'], $('<span />').addClass(csscls('text')).appendTo(this.$el)); 335 336 this.$tooltip = $('<span />').addClass(csscls('tooltip disabled')).appendTo(this.$el); 337 this.bindAttr('tooltip', function(tooltip) { 338 if (tooltip) {
339 var dl = $('<dl />'); 340 if (Array.isArray(tooltip) || typeof tooltip === 'object') { 341 $.each(tooltip, function(key, value) { 342 $('<dt />').text(key).appendTo(dl); 343 $('<dd />').text(value).appendTo(dl); 344 }); 345 this.$tooltip.html(dl).removeClass(csscls('disabled')); 346 } else { 347 this.$tooltip.text(tooltip).removeClass(csscls('disabled')); 348 } 349 } else { 350 this.$tooltip.addClass(csscls('disabled')); 351 } 352 }); 353 } 354 355 }); 356 357 358 /** 359 * Displays datasets in a table 360 * 361 */ 362 var Settings = Widget.extend({ 363 364 tagName: 'form', 365 366 className: csscls('settings'), 367 368 settings: {}, 369 370 initialize: function(options) { 371 this.set(options); 372 373 var debugbar = this.get('debugbar'); 374 this.settings = JSON.parse(localStorage.getItem('phpdebugbar-settings')) || {}; 375 376 $.each(debugbar.options, (key, value)=> { 377 if (key in this.settings) { 378 debugbar.options[key] = this.settings[key]; 379 } 380 381 // Theme requires dark/light mode detection 382 if (key === 'theme') { 383 debugbar.setTheme(debugbar.options[key]); 384 } else { 385 debugbar.$el.attr('data-' + key, debugbar.options[key]); 386 } 387 }) 388 }, 389 390 clearSettings: function() { 391 var debugbar = this.get('debugbar'); 392 393 // Remove item from storage 394 localStorage.removeItem('phpdebugbar-settings'); 395 localStorage.removeItem('phpdebugbar-ajaxhandler-autoshow'); 396 this.settings = {}; 397 398 // Reset options 399 debugbar.options = { ...debugbar.defaultOptions }; 400 401 // Reset ajax handler 402 if (debugbar.ajaxHandler) { 403 var autoshow = debugbar.ajaxHandler.defaultAutoShow; 404 debugbar.ajaxHandler.setAutoShow(autoshow); 405 this.set('autoshow', autoshow); 406 if(debugbar.controls['__datasets']) { 407 debugbar.controls['__datasets'].get('widget').set('autoshow', $(this).is(':checked')); 408 } 409 } 410 411 this.initialize(debugbar.options); 412 }, 413 414 storeSetting: function(key, value) { 415 this.settings[key] = value; 416 417 var debugbar = this.get('debugbar'); 418 debugbar.options[key] = value; 419 if (key !== 'theme') { 420 debugbar.$el.attr('data-' + key, value); 421 } 422 423 localStorage.setItem('phpdebugbar-settings', JSON.stringify(this.settings)); 424 }, 425 426 render: function() { 427 this.$el.empty(); 428 429 var debugbar = this.get('debugbar'); 430 var self = this; 431 432 var fields = {}; 433 434 // Set Theme 435 fields["Theme"] = $('<select>' + 436 '<option value="auto">Auto (System preference)</option>' + 437 '<option value="light">Light</option>' + 438 '<option value="dark">Dark</option>' + 439 '</select>') 440 .val(debugbar.options.theme) 441 .on('change', function() { 442 self.storeSetting('theme', $(this).val()) 443 debugbar.setTheme($(this).val()); 444 }); 445 446 fields["Open Button Position"] = $('<select>' + 447 '<option value="bottomLeft">Bottom Left</option>' + 448 '<option value="bottomRight">Bottom Right</option>' + 449 '<option value="topLeft">Top Left</option>' + 450 '<option value="topRight">Top Right</option>' + 451 '</select>') 452 .val(debugbar.options.openBtnPosition) 453 .on('change', function() { 454 self.storeSetting('openBtnPosition', $(this).val()) 455 }); 456 457 this.$hideEmptyTabs = $('<input type=checkbox>') 458 .prop('checked', debugbar.options.hideEmptyTabs) 459 .on('click', function() { 460 self.storeSetting('hideEmptyTabs', $(this).is(':checked')); 461 }); 462 463 fields["Hide Empty Tabs"] = $('<label/>').append(this.$hideEmptyTabs, 'Hide empty tabs until they have data'); 464 465 this.$autoshow = $('<input type=checkbox>') 466 .prop('checked', debugbar.ajaxHandler && debugbar.ajaxHandler.autoShow) 467 .on('click', function() { 468 if (debugbar.ajaxHandler) { 469 debugbar.ajaxHandler.setAutoShow($(this).is(':checked')); 470 } 471 if (debugbar.controls['__datasets']) { 472 debugbar.controls['__datasets'].get('widget').set('autoshow', $(this).is(':checked')); 473 } 474 }); 475 476 this.bindAttr('autoshow', function() { 477 this.$autoshow.prop('checked', this.get('autoshow')).closest('.form-row').show(); 478 }) 479 fields["Autoshow"] = $('<label/>').append(this.$autoshow, 'Automatically show new incoming Ajax requests'); 480 481 fields["Reset to defaults"] = $('<button>Reset settings</button>').on('click', function(e) { 482 e.preventDefault(); 483 self.clearSettings(); 484 self.render(); 485 }) 486 487 $.each(fields, function(key, value) { 488 $('<div />').addClass(csscls('form-row')).append( 489 $('<div />').addClass(csscls('form-label')).text(key), 490 $('<div />').addClass(csscls('form-input')).html(value) 491 ).appendTo(self.$el); 492 493 }) 494 495 if (!this.ajaxHandler) { 496 this.$autoshow.closest('.form-row').hide(); 497 } 498 }, 499 500 }); 501 502 // ------------------------------------------------------------------ 503 504 /** 505 * Dataset title formater 506 * 507 * Formats the title of a dataset for the select box 508 */ 509 var DatasetTitleFormater = PhpDebugBar.DatasetTitleFormater = function(debugbar) { 510 this.debugbar = debugbar; 511 }; 512 513 $.extend(DatasetTitleFormater.prototype, { 514 515 /** 516 * Formats the title of a dataset 517 * 518 * @this {DatasetTitleFormater} 519 * @param {String} id 520 * @param {Object} data 521 * @param {String} suffix 522 * @return {String} 523 */ 524 format: function(id, data, suffix, nb) { 525 if (suffix) { 526 suffix = ' ' + suffix; 527 } else { 528 suffix = ''; 529 } 530 531 var nb = nb || getObjectSize(this.debugbar.datasets) ; 532 533 if (typeof(data['__meta']) === 'undefined') { 534 return "#" + nb + suffix; 535 } 536 537 var uri = data['__meta']['uri'].split('/'), filename = uri.pop(); 538 539 // URI ends in a trailing /, avoid returning an empty string 540 if (!filename) { 541 filename = (uri.pop() || '') + '/'; // add the trailing '/' back 542 } 543 544 // filename is a number, path could be like /action/{id} 545 if (uri.length && !isNaN(filename)) { 546 filename = uri.pop() + '/' + filename; 547 } 548 549 // truncate the filename in the label, if it's too long 550 var maxLength = 150; 551 if (filename.length > maxLength) { 552 filename = filename.substr(0, maxLength) + '...'; 553 } 554
555 var label = "#" + nb + " " + filename + suffix + ' (' + data['__meta']['datetime'].split(' ')[1] + ')'; 556 return label; 557 } 558 559 }); 560 561 // ------------------------------------------------------------------ 562 563 564 /** 565 * DebugBar 566 * 567 * Creates a bar that appends itself to the body of your page 568 * and sticks to the bottom. 569 * 570 * The bar can be customized by adding tabs and indicators. 571 * A data map is used to fill those controls with data provided 572 * from datasets. 573 */ 574 var DebugBar = PhpDebugBar.DebugBar = Widget.extend({ 575 576 className: "phpdebugbar " + csscls('minimized'), 577 578 options: { 579 bodyMarginBottom: true, 580 theme: 'auto', 581 openBtnPosition: 'bottomLeft', 582 hideEmptyTabs: false, 583 }, 584 585 initialize: function(options = {}) { 586 this.options = $.extend(this.options, options); 587 this.defaultOptions = { ...this.options }; 588 this.controls = {}; 589 this.dataMap = {}; 590 this.datasets = {}; 591 this.firstTabName = null; 592 this.activePanelName = null; 593 this.activeDatasetId = null; 594 this.datesetTitleFormater = new DatasetTitleFormater(this); 595 this.bodyMarginBottomHeight = parseInt($('body').css('margin-bottom')); 596 try { 597 this.isIframe = window.self !== window.top && window.top.phpdebugbar; 598 } catch (error) { 599 this.isIframe = false; 600 } 601 this.registerResizeHandler(); 602 this.registerMediaListener(); 603 604 // Attach settings 605 this.settings = new PhpDebugBar.DebugBar.Tab({"icon":"sliders", "title":"Settings", "widget": new Settings({ 606 'debugbar': this, 607 })}); 608 }, 609 610 /** 611 * Register resize event, for resize debugbar with reponsive css. 612 * 613 * @this {DebugBar} 614 */ 615 registerResizeHandler: function() { 616 if (typeof this.resize.bind == 'undefined' || this.isIframe) return; 617 618 var f = this.resize.bind(this); 619 this.respCSSSize = 0; 620 $(window).resize(f); 621 setTimeout(f, 20); 622 }, 623 624 registerMediaListener: function() { 625 const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); 626 mediaQueryList.addEventListener('change', event => { 627 if (this.options.theme === 'auto') { 628 this.setTheme('auto'); 629 } 630 }) 631 }, 632 633 setTheme: function(theme) { 634 this.options.theme = theme; 635 636 if (theme === 'auto') { 637 const mediaQueryList = window.matchMedia("(prefers-color-scheme: dark)"); 638 theme = mediaQueryList.matches ? 'dark' : 'light'; 639 } 640 641 this.$el.attr('data-theme', theme) 642 if (this.openHandler) { 643 this.openHandler.$el.attr('data-theme', theme) 644 } 645 }, 646 647 /** 648 * Resizes the debugbar to fit the current browser window 649 */ 650 resize: function() { 651 if (this.isIframe) return; 652 653 var contentSize = this.respCSSSize; 654 if (this.respCSSSize == 0) { 655 this.$header.find("> *:visible").each(function () { 656 contentSize += $(this).outerWidth(true); 657 }); 658 } 659 660 var currentSize = this.$header.width(); 661 var cssClass = csscls("mini-design"); 662 var bool = this.$header.hasClass(cssClass); 663 664 if (currentSize <= contentSize && !bool) { 665 this.respCSSSize = contentSize; 666 this.$header.addClass(cssClass); 667 } else if (contentSize < currentSize && bool) { 668 this.respCSSSize = 0; 669 this.$header.removeClass(cssClass); 670 } 671 672 // Reset height to ensure bar is still visible 673 this.setHeight(this.$body.height()); 674 }, 675 676 /** 677 * Initialiazes the UI 678 * 679 * @this {DebugBar} 680 */ 681 render: function() { 682 if (this.isIframe) { 683 this.$el.hide(); 684 } 685 686 var self = this; 687 this.$el.appendTo('body'); 688 this.$dragCapture = $('<div />').addClass(csscls('drag-capture')).appendTo(this.$el); 689 this.$resizehdle = $('<div />').addClass(csscls('resize-handle')).appendTo(this.$el);
690 this.$header = $('<div />').addClass(csscls('header')).appendTo(this.$el); 691 this.$headerBtn = $('<a />').addClass(csscls('restore-btn')).appendTo(this.$header); 692 this.$headerBtn.click(function() { 693 self.close(); 694 }); 695 this.$headerLeft = $('<div />').addClass(csscls('header-left')).appendTo(this.$header); 696 this.$headerRight = $('<div />').addClass(csscls('header-right')).appendTo(this.$header); 697 var $body = this.$body = $('<div />').addClass(csscls('body')).appendTo(this.$el); 698 this.recomputeBottomOffset(); 699 700 // dragging of resize handle 701 var pos_y, orig_h; 702 this.$resizehdle.on('mousedown', function(e) { 703 orig_h = $body.height(), pos_y = e.pageY; 704 $body.parents().on('mousemove', mousemove).on('mouseup', mouseup); 705 self.$dragCapture.show(); 706 e.preventDefault(); 707 }); 708 var mousemove = function(e) { 709 var h = orig_h + (pos_y - e.pageY); 710 self.setHeight(h); 711 }; 712 var mouseup = function() { 713 $body.parents().off('mousemove', mousemove).off('mouseup', mouseup); 714 self.$dragCapture.hide(); 715 }; 716 717 // close button 718 this.$closebtn = $('<a />').addClass(csscls('close-btn')).appendTo(this.$headerRight); 719 this.$closebtn.click(function() { 720 self.close(); 721 }); 722 723 // minimize button 724 this.$minimizebtn = $('<a />').addClass(csscls('minimize-btn') ).appendTo(this.$headerRight); 725 this.$minimizebtn.click(function() { 726 self.minimize(); 727 }); 728 729 // maximize button 730 this.$maximizebtn = $('<a />').addClass(csscls('maximize-btn') ).appendTo(this.$headerRight); 731 this.$maximizebtn.click(function() { 732 self.restore(); 733 }); 734 735 // restore button 736 this.$restorebtn = $('<a />').addClass(csscls('restore-btn')).hide().appendTo(this.$el); 737 this.$restorebtn.click(function() { 738 self.restore(); 739 }); 740 741 // open button 742 this.$openbtn = $('<a />').addClass(csscls('open-btn')).appendTo(this.$headerRight).hide(); 743 this.$openbtn.click(function() { 744 self.openHandler.show(function(id, dataset) { 745 self.addDataSet(dataset, id, "(opened)"); 746 self.showTab(); 747 }); 748 }); 749 750 // select box for data sets 751 this.$datasets = $('<select />').addClass(csscls('datasets-switcher')).attr('name', 'datasets-switcher') 752 .appendTo(this.$headerRight); 753 this.$datasets.change(function() { 754 self.showDataSet(this.value); 755 }); 756 757 this.controls['__settings'] = this.settings; 758 this.settings.$tab.addClass(csscls('tab-settings')); 759 this.settings.$tab.attr('data-collector', '__settings'); 760 this.settings.$el.attr('data-collector', '__settings'); 761 this.settings.$tab.insertAfter(this.$minimizebtn).show(); 762 this.settings.$tab.click(() => { 763 if (!this.isMinimized() && this.activePanelName == '__settings') { 764 this.minimize(); 765 } else { 766 this.showTab('__settings'); 767 this.settings.get('widget').render(); 768 } 769 }); 770 this.settings.$el.appendTo(this.$body); 771 }, 772 773 /** 774 * Sets the height of the debugbar body section 775 * Forces the height to lie within a reasonable range 776 * Stores the height in local storage so it can be restored 777 * Resets the document body bottom offset 778 * 779 * @this {DebugBar} 780 */ 781 setHeight: function(height) { 782 var min_h = 40; 783 var max_h = window.innerHeight - this.$header.height() - 10; 784 height = Math.min(height, max_h); 785 height = Math.max(height, min_h); 786 this.$body.css('height', height); 787 localStorage.setItem('phpdebugbar-height', height); 788 this.recomputeBottomOffset(); 789 }, 790 791 /** 792 * Restores the state of the DebugBar using localStorage 793 * This is not called by default in the constructor and 794 * needs to be called by subclasses in their init() method 795 * 796 * @this {DebugBar} 797 */ 798 restoreState: function() { 799 if (this.isIframe) return; 800 // bar height 801 var height = localStorage.getItem('phpdebugbar-height'); 802 this.setHeight(height || this.$body.height()); 803 804 // bar visibility 805 var open = localStorage.getItem('phpdebugbar-open'); 806 if (open && open == '0') { 807 this.close(); 808 } else { 809 var visible = localStorage.getItem('phpdebugbar-visible'); 810 if (visible && visible == '1') { 811 var tab = localStorage.getItem('phpdebugbar-tab'); 812 if (this.isTab(tab)) { 813 this.showTab(tab); 814 } else { 815 this.showTab(); 816 } 817 } 818 } 819 }, 820 821 /** 822 * Creates and adds a new tab 823 * 824 * @this {DebugBar} 825 * @param {String} name Internal name 826 * @param {Object} widget A widget object with an element property 827 * @param {String} title The text in the tab, if not specified, name will be used 828 * @return {Tab} 829 */ 830 createTab: function(name, widget, title) { 831 var tab = new Tab({ 832 title: title || (name.replace(/[_\-]/g, ' ').charAt(0).toUpperCase() + name.slice(1)), 833 widget: widget 834 }); 835 return this.addTab(name, tab); 836 }, 837 838 /**
839 * Adds a new tab 840 * 841 * @this {DebugBar} 842 * @param {String} name Internal name 843 * @param {Tab} tab Tab object 844 * @return {Tab} 845 */ 846 addTab: function(name, tab) { 847 if (this.isControl(name)) { 848 throw new Error(name + ' already exists'); 849 } 850 851 var self = this; 852 tab.$tab.appendTo(this.$headerLeft).click(function() { 853 if (!self.isMinimized() && self.activePanelName == name) { 854 self.minimize(); 855 } else { 856 self.showTab(name); 857 } 858 }) 859 tab.$tab.attr('data-empty', true); 860 tab.$tab.attr('data-collector', name); 861 tab.$el.attr('data-collector', name); 862 tab.$el.appendTo(this.$body); 863 864 this.controls[name] = tab; 865 if (this.firstTabName == null) { 866 this.firstTabName = name; 867 } 868 return tab; 869 }, 870 871 /** 872 * Creates and adds an indicator 873 * 874 * @this {DebugBar} 875 * @param {String} name Internal name 876 * @param {String} icon 877 * @param {String|Object} tooltip 878 * @param {String} position "right" or "left", default is "right" 879 * @return {Indicator} 880 */ 881 createIndicator: function(name, icon, tooltip, position) { 882 var indicator = new Indicator({ 883 icon: icon, 884 tooltip: tooltip 885 }); 886 return this.addIndicator(name, indicator, position); 887 }, 888 889 /** 890 * Adds an indicator 891 * 892 * @this {DebugBar} 893 * @param {String} name Internal name 894 * @param {Indicator} indicator Indicator object 895 * @return {Indicator} 896 */ 897 addIndicator: function(name, indicator, position) { 898 if (this.isControl(name)) { 899 throw new Error(name + ' already exists'); 900 } 901 902 indicator.set('debugbar', this); 903 904 if (position == 'left') { 905 indicator.$el.insertBefore(this.$headerLeft.children().first()); 906 } else { 907 indicator.$el.appendTo(this.$headerRight); 908 } 909 910 this.controls[name] = indicator; 911 return indicator; 912 }, 913 914 /** 915 * Returns a control 916 * 917 * @param {String} name 918 * @return {Object} 919 */ 920 getControl: function(name) { 921 if (this.isControl(name)) { 922 return this.controls[name]; 923 } 924 }, 925 926 /** 927 * Checks if there's a control under the specified name 928 * 929 * @this {DebugBar} 930 * @param {String} name 931 * @return {Boolean} 932 */ 933 isControl: function(name) { 934 return typeof(this.controls[name]) != 'undefined'; 935 }, 936 937 /** 938 * Checks if a tab with the specified name exists 939 * 940 * @this {DebugBar} 941 * @param {String} name 942 * @return {Boolean} 943 */ 944 isTab: function(name) { 945 return this.isControl(name) && this.controls[name] instanceof Tab; 946 }, 947 948 /** 949 * Checks if an indicator with the specified name exists 950 * 951 * @this {DebugBar} 952 * @param {String} name 953 * @return {Boolean} 954 */ 955 isIndicator: function(name) { 956 return this.isControl(name) && this.controls[name] instanceof Indicator; 957 }, 958 959 /** 960 * Removes all tabs and indicators from the debug bar and hides it 961 * 962 * @this {DebugBar} 963 */ 964 reset: function() { 965 this.minimize(); 966 var self = this; 967 $.each(this.controls, function(name, control) { 968 if (self.isTab(name)) { 969 control.$tab.remove(); 970 } 971 control.$el.remove(); 972 }); 973 this.controls = {}; 974 }, 975 976 /** 977 * Open the debug bar and display the specified tab 978 * 979 * @this {DebugBar} 980 * @param {String} name If not specified, display the first tab 981 */ 982 showTab: function(name) { 983 if (!name) { 984 if (this.activePanelName) { 985 name = this.activePanelName; 986 } else { 987 name = this.firstTabName; 988 } 989 } 990 991 if (!this.isTab(name)) {
992 throw new Error("Unknown tab '" + name + "'"); 993 } 994 995 this.$resizehdle.show(); 996 this.$body.show(); 997 this.recomputeBottomOffset(); 998 999 $(this.$header).find('> div > .' + csscls('active')).removeClass(csscls('active')); 1000 $(this.$body).find('> .' + csscls('active')).removeClass(csscls('active')); 1001 1002 this.controls[name].$tab.addClass(csscls('active')); 1003 this.controls[name].$el.addClass(csscls('active')); 1004 this.activePanelName = name; 1005 1006 this.$el.removeClass(csscls('minimized')); 1007 localStorage.setItem('phpdebugbar-visible', '1'); 1008 localStorage.setItem('phpdebugbar-tab', name); 1009 1010 this.resize(); 1011 }, 1012 1013 /** 1014 * Hide panels and minimize the debug bar 1015 * 1016 * @this {DebugBar} 1017 */ 1018 minimize: function() { 1019 this.$header.find('> div > .' + csscls('active')).removeClass(csscls('active')); 1020 this.$body.hide(); 1021 this.$resizehdle.hide(); 1022 this.recomputeBottomOffset(); 1023 localStorage.setItem('phpdebugbar-visible', '0'); 1024 this.$el.addClass(csscls('minimized')); 1025 this.resize(); 1026 }, 1027 1028 /** 1029 * Checks if the panel is minimized 1030 * 1031 * @return {Boolean} 1032 */ 1033 isMinimized: function() { 1034 return this.$el.hasClass(csscls('minimized')); 1035 }, 1036 1037 /** 1038 * Close the debug bar 1039 * 1040 * @this {DebugBar} 1041 */ 1042 close: function() { 1043 this.$resizehdle.hide(); 1044 this.$header.hide(); 1045 this.$body.hide(); 1046 this.$restorebtn.show(); 1047 localStorage.setItem('phpdebugbar-open', '0'); 1048 this.$el.addClass(csscls('closed')); 1049 this.recomputeBottomOffset(); 1050 }, 1051 1052 /** 1053 * Checks if the panel is closed 1054 * 1055 * @return {Boolean} 1056 */ 1057 isClosed: function() { 1058 return this.$el.hasClass(csscls('closed')); 1059 }, 1060 1061 /** 1062 * Restore the debug bar 1063 * 1064 * @this {DebugBar} 1065 */ 1066 restore: function() { 1067 this.$resizehdle.show(); 1068 this.$header.show(); 1069 this.$restorebtn.hide(); 1070 localStorage.setItem('phpdebugbar-open', '1'); 1071 var tab = localStorage.getItem('phpdebugbar-tab'); 1072 if (this.isTab(tab)) { 1073 this.showTab(tab); 1074 } else { 1075 this.showTab(); 1076 } 1077 this.$el.removeClass(csscls('closed')); 1078 this.resize(); 1079 }, 1080 1081 /** 1082 * Recomputes the margin-bottom css property of the body so 1083 * that the debug bar never hides any content 1084 */ 1085 recomputeBottomOffset: function() { 1086 if (this.options.bodyMarginBottom) { 1087 if (this.isClosed()) { 1088 return $('body').css('margin-bottom', this.bodyMarginBottomHeight || ''); 1089 } 1090 1091 var offset = parseInt(this.$el.height()) + (this.bodyMarginBottomHeight || 0); 1092 $('body').css('margin-bottom', offset); 1093 } 1094 }, 1095 1096 /** 1097 * Sets the data map used by dataChangeHandler to populate 1098 * indicators and widgets 1099 * 1100 * A data map is an object where properties are control names. 1101 * The value of each property should be an array where the first 1102 * item is the name of a property from the data object (nested properties 1103 * can be specified) and the second item the default value. 1104 * 1105 * Example: 1106 * {"memory": ["memory.peak_usage_str", "0B"]} 1107 * 1108 * @this {DebugBar} 1109 * @param {Object} map 1110 */ 1111 setDataMap: function(map) { 1112 this.dataMap = map; 1113 }, 1114 1115 /** 1116 * Same as setDataMap() but appends to the existing map 1117 * rather than replacing it 1118 * 1119 * @this {DebugBar} 1120 * @param {Object} map 1121 */ 1122 addDataMap: function(map) { 1123 $.extend(this.dataMap, map); 1124 }, 1125 1126 /** 1127 * Resets datasets and add one set of data 1128 * 1129 * For this method to be usefull, you need to specify 1130 * a dataMap using setDataMap() 1131 * 1132 * @this {DebugBar} 1133 * @param {Object} data 1134 * @return {String} Dataset's id 1135 */ 1136 setData: function(data) { 1137 this.datasets = {}; 1138 return this.addDataSet(data); 1139 }, 1140 1141 /** 1142 * Adds a dataset 1143 * 1144 * If more than one dataset are added, the dataset selector 1145 * will be displayed. 1146 * 1147 * For this method to be usefull, you need to specify 1148 * a dataMap using setDataMap() 1149 * 1150 * @this {DebugBar} 1151 * @param {Object} data 1152 * @param {String} id The name of this set, optional 1153 * @param {String} suffix 1154 * @param {Bool} show Whether to show the new dataset, optional (default: true) 1155 * @return {String} Dataset's id 1156 */ 1157 addDataSet: function(data, id, suffix, show) { 1158 if (!data || !data.__meta) return; 1159 if (this.isIframe) { 1160 window.top.phpdebugbar.addDataSet(data, id, '(iframe)' + (suffix || ''), show); 1161 return; 1162 } 1163 1164 var nb = getObjectSize(this.datasets) + 1; 1165 id = id || nb; 1166 data.__meta['nb'] = nb; 1167 data.__meta['suffix'] = suffix; 1168 this.datasets[id] = data; 1169 1170 var label = this.datesetTitleFormater.format(id, this.datasets[id], suffix, nb); 1171 1172 if (this.datasetTab) { 1173 this.datasetTab.set('data', this.datasets); 1174 var datasetSize = getObjectSize(this.datasets); 1175 this.datasetTab.set('badge', datasetSize > 1 ? datasetSize : null); 1176 this.datasetTab.$tab.show(); 1177 } 1178 1179 this.$datasets.append($('<option value="' + id + '">' + label + '</option>')); 1180 if (this.$datasets.children().length > 1) { 1181 this.$datasets.show(); 1182 } 1183 1184 if (typeof(show) == 'undefined' || show) { 1185 this.showDataSet(id); 1186 } 1187 1188 this.resize(); 1189 1190 return id; 1191 }, 1192 1193 /** 1194 * Loads a dataset using the open handler 1195 * 1196 * @param {String} id 1197 * @param {Bool} show Whether to show the new dataset, optional (default: true) 1198 */
1199 loadDataSet: function(id, suffix, callback, show) { 1200 if (!this.openHandler) { 1201 throw new Error('loadDataSet() needs an open handler'); 1202 } 1203 var self = this; 1204 this.openHandler.load(id, function(data) { 1205 self.addDataSet(data, id, suffix, show); 1206 self.resize(); 1207 callback && callback(data); 1208 }); 1209 }, 1210 1211 /** 1212 * Returns the data from a dataset 1213 * 1214 * @this {DebugBar} 1215 * @param {String} id 1216 * @return {Object} 1217 */ 1218 getDataSet: function(id) { 1219 return this.datasets[id]; 1220 }, 1221 1222 /** 1223 * Switch the currently displayed dataset 1224 * 1225 * @this {DebugBar} 1226 * @param {String} id 1227 */ 1228 showDataSet: function(id) { 1229 this.activeDatasetId = id; 1230 this.dataChangeHandler(this.datasets[id]); 1231 1232 if (this.$datasets.val() !== id) { 1233 this.$datasets.val(id); 1234 } 1235 1236 if (this.datasetTab) { 1237 this.datasetTab.get('widget').set('id', id); 1238 } 1239 }, 1240 1241 /** 1242 * Called when the current dataset is modified. 1243 * 1244 * @this {DebugBar} 1245 * @param {Object} data 1246 */ 1247 dataChangeHandler: function(data) { 1248 var self = this; 1249 $.each(this.dataMap, function(key, def) { 1250 var d = getDictValue(data, def[0], def[1]); 1251 if (key.indexOf(':') != -1) { 1252 key = key.split(':'); 1253 self.getControl(key[0]).set(key[1], d); 1254 } else { 1255 self.getControl(key).set('data', d); 1256 } 1257 }); 1258 self.resize(); 1259 }, 1260 1261 /** 1262 * Sets the handler to open past dataset 1263 * 1264 * @this {DebugBar} 1265 * @param {object} handler 1266 */ 1267 setOpenHandler: function(handler) { 1268 this.openHandler = handler; 1269 this.openHandler.$el.attr('data-theme', this.$el.attr('data-theme')); 1270 if (handler !== null) { 1271 this.$openbtn.show(); 1272 } else { 1273 this.$openbtn.hide(); 1274 } 1275 }, 1276 1277 /** 1278 * Returns the handler to open past dataset 1279 * 1280 * @this {DebugBar} 1281 * @return {object} 1282 */ 1283 getOpenHandler: function() { 1284 return this.openHandler; 1285 }, 1286 1287 enableAjaxHandlerTab: function() { 1288 this.datasetTab = new PhpDebugBar.DebugBar.Tab({"icon":"history", "title":"Request history", "widget": new PhpDebugBar.Widgets.DatasetWidget({ 1289 'debugbar': this 1290 })}); 1291 this.datasetTab.$tab.addClass(csscls('tab-history')); 1292 this.datasetTab.$tab.attr('data-collector', '__datasets'); 1293 this.datasetTab.$el.attr('data-collector', '__datasets'); 1294 this.datasetTab.$tab.insertAfter(this.$openbtn).hide(); 1295 this.datasetTab.$tab.click(() => { 1296 if (!this.isMinimized() && this.activePanelName == '__datasets') { 1297 this.minimize(); 1298 } else { 1299 this.showTab('__datasets'); 1300 } 1301 }); 1302 this.datasetTab.$el.appendTo(this.$body); 1303 this.controls['__datasets'] = this.datasetTab; 1304 }, 1305 }); 1306 1307 DebugBar.Tab = Tab; 1308 DebugBar.Indicator = Indicator; 1309 1310 // ------------------------------------------------------------------ 1311 1312 /** 1313 * AjaxHandler 1314 * 1315 * Extract data from headers of an XMLHttpRequest and adds a new dataset 1316 * 1317 * @param {Bool} autoShow Whether to immediately show new datasets, optional (default: true) 1318 */ 1319 var AjaxHandler = PhpDebugBar.AjaxHandler = function(debugbar, headerName, autoShow) { 1320 this.debugbar = debugbar; 1321 this.headerName = headerName || 'phpdebugbar'; 1322 this.autoShow = typeof(autoShow) == 'undefined' ? true : autoShow; 1323 this.defaultAutoShow = this.autoShow; 1324 if (localStorage.getItem('phpdebugbar-ajaxhandler-autoshow') !== null) { 1325 this.autoShow = localStorage.getItem('phpdebugbar-ajaxhandler-autoshow') == '1'; 1326 } 1327 if (debugbar.controls['__settings']) { 1328 debugbar.controls['__settings'].get('widget').set('autoshow', this.autoShow); 1329 } 1330 }; 1331 1332 $.extend(AjaxHandler.prototype, { 1333 1334 /** 1335 * Handles a Fetch API Response or an XMLHttpRequest 1336 * 1337 * @this {AjaxHandler} 1338 * @param {Response|XMLHttpRequest} response 1339 * @return {Bool} 1340 */ 1341 handle: function(response) { 1342 // Check if the debugbar header is available 1343 if (this.isFetch(response) && !response.headers.has(this.headerName + '-id')) { 1344 return true; 1345 } else if (this.isXHR(response) && response.getAllResponseHeaders().indexOf(this.headerName) === -1) { 1346 return true; 1347 } 1348 if (!this.loadFromId(response)) { 1349 return this.loadFromData(response); 1350 } 1351 return true; 1352 }, 1353 1354 getHeader: function(response, header) { 1355 if (this.isFetch(response)) { 1356 return response.headers.get(header) 1357 } 1358 1359 return response.getResponseHeader(header) 1360 }, 1361 1362 isFetch: function(response) { 1363 return Object.prototype.toString.call(response) == '[object Response]' 1364 }, 1365 1366 isXHR: function(response) { 1367 return Object.prototype.toString.call(response) == '[object XMLHttpRequest]' 1368 }, 1369
1370 setAutoShow: function(autoshow) { 1371 this.autoShow = autoshow; 1372 localStorage.setItem('phpdebugbar-ajaxhandler-autoshow', autoshow ? '1' : '0'); 1373 }, 1374 1375 /** 1376 * Checks if the HEADER-id exists and loads the dataset using the open handler 1377 * 1378 * @param {Response|XMLHttpRequest} response 1379 * @return {Bool} 1380 */ 1381 loadFromId: function(response) { 1382 var id = this.extractIdFromHeaders(response); 1383 if (id && this.debugbar.openHandler) { 1384 this.debugbar.loadDataSet(id, "(ajax)", undefined, this.autoShow); 1385 return true; 1386 } 1387 return false; 1388 }, 1389 1390 /** 1391 * Extracts the id from the HEADER-id 1392 * 1393 * @param {Response|XMLHttpRequest} response 1394 * @return {String} 1395 */ 1396 extractIdFromHeaders: function(response) { 1397 return this.getHeader(response, this.headerName + '-id'); 1398 }, 1399 1400 /** 1401 * Checks if the HEADER exists and loads the dataset 1402 * 1403 * @param {Response|XMLHttpRequest} response 1404 * @return {Bool} 1405 */ 1406 loadFromData: function(response) { 1407 var raw = this.extractDataFromHeaders(response); 1408 if (!raw) { 1409 return false; 1410 } 1411 1412 var data = this.parseHeaders(raw); 1413 if (data.error) { 1414 throw new Error('Error loading debugbar data: ' + data.error); 1415 } else if(data.data) { 1416 this.debugbar.addDataSet(data.data, data.id, "(ajax)", this.autoShow); 1417 } 1418 return true; 1419 }, 1420 1421 /** 1422 * Extract the data as a string from headers of an XMLHttpRequest 1423 * 1424 * @this {AjaxHandler} 1425 * @param {Response|XMLHttpRequest} response 1426 * @return {string} 1427 */ 1428 extractDataFromHeaders: function(response) { 1429 var data = this.getHeader(response, this.headerName); 1430 if (!data) { 1431 return; 1432 } 1433 for (var i = 1;; i++) { 1434 var header = this.getHeader(response, this.headerName + '-' + i); 1435 if (!header) { 1436 break; 1437 } 1438 data += header; 1439 } 1440 return decodeURIComponent(data); 1441 }, 1442 1443 /** 1444 * Parses the string data into an object 1445 * 1446 * @this {AjaxHandler} 1447 * @param {string} data 1448 * @return {string} 1449 */ 1450 parseHeaders: function(data) { 1451 return JSON.parse(data); 1452 }, 1453 1454 /** 1455 * Attaches an event listener to fetch 1456 * 1457 * @this {AjaxHandler} 1458 */ 1459 bindToFetch: function() { 1460 var self = this; 1461 var proxied = window.fetch; 1462 1463 if (proxied !== undefined && proxied.polyfill !== undefined) { 1464 return; 1465 } 1466 1467 window.fetch = function () { 1468 var promise = proxied.apply(this, arguments); 1469 1470 promise.then(function (response) { 1471 self.handle(response); 1472 }).catch(function(reason) { 1473 // Fetch request failed or aborted via AbortController.abort(). 1474 // Catch is required to not trigger React's error handler. 1475 }); 1476 1477 return promise; 1478 }; 1479 }, 1480 1481 /** 1482 * @deprecated use bindToXHR instead 1483 */ 1484 bindToJquery: function(jq) { 1485 var self = this;
1486 jq(document).ajaxComplete(function(e, xhr, settings) { 1487 if (!settings.ignoreDebugBarAjaxHandler) { 1488 self.handle(xhr); 1489 } 1490 }); 1491 }, 1492 1493 /** 1494 * Attaches an event listener to XMLHttpRequest 1495 * 1496 * @this {AjaxHandler} 1497 */ 1498 bindToXHR: function() { 1499 var self = this; 1500 var proxied = XMLHttpRequest.prototype.open; 1501 XMLHttpRequest.prototype.open = function(method, url, async, user, pass) { 1502 var xhr = this; 1503 this.addEventListener("readystatechange", function() { 1504 var skipUrl = self.debugbar.openHandler ? self.debugbar.openHandler.get('url') : null; 1505 var href = (typeof url === 'string') ? url : url.href; 1506 1507 if (xhr.readyState == 4 && href.indexOf(skipUrl) !== 0) { 1508 self.handle(xhr); 1509 } 1510 }, false); 1511 proxied.apply(this, Array.prototype.slice.call(arguments)); 1512 }; 1513 } 1514 1515 }); 1516 1517})(PhpDebugBar.$);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.