if (typeof(PhpDebugBar) == 'undefined') { // namespace var PhpDebugBar = {}; PhpDebugBar.$ = jQuery; } (function($) { if (typeof(localStorage) == 'undefined') { // provide mock localStorage object for dumb browsers localStorage = { setItem: function(key, value) {}, getItem: function(key) { return null; } }; } if (typeof(PhpDebugBar.utils) == 'undefined') { PhpDebugBar.utils = {}; } /** * Returns the value from an object property. * Using dots in the key, it is possible to retrieve nested property values * * @param {Object} dict * @param {String} key * @param {Object} default_value * @return {Object} */ var getDictValue = PhpDebugBar.utils.getDictValue = function(dict, key, default_value) { var d = dict, parts = key.split('.'); for (var i = 0; i < parts.length; i++) { if (!d[parts[i]]) { return default_value; } d = d[parts[i]]; } return d; } /** * Counts the number of properties in an object * * @param {Object} obj * @return {Integer} */ var getObjectSize = PhpDebugBar.utils.getObjectSize = function(obj) { if (Object.keys) { return Object.keys(obj).length; } var count = 0; for (var k in obj) { if (obj.hasOwnProperty(k)) { count++; } } return count; } /** * Returns a prefixed css class name * * @param {String} cls * @return {String} */ PhpDebugBar.utils.csscls = function(cls, prefix) { if (cls.indexOf(' ') > -1) { var clss = cls.split(' '), out = []; for (var i = 0, c = clss.length; i < c; i++) { out.push(PhpDebugBar.utils.csscls(clss[i], prefix)); } return out.join(' '); } if (cls.indexOf('.') === 0) { return '.' + prefix + cls.substr(1); } return prefix + cls; }; var csscls = function(cls) { return PhpDebugBar.utils.csscls(cls, 'phpdebugbar-'); }; // ------------------------------------------------------------------ /** * Base class for all elements with a visual component * * @param {Object} options * @constructor */ var Widget = PhpDebugBar.Widget = function(options) { this._attributes = $.extend({}, this.defaults); this._boundAttributes = {}; this.$el = $('<' + this.tagName + ' />'); if (this.className) { this.$el.addClass(this.className); } this.initialize.apply(this, [options || {}]); this.render.apply(this); }; $.extend(Widget.prototype, { tagName: 'div', className: null, defaults: {}, /** * Called after the constructor * * @param {Object} options */ initialize: function(options) { this.set(options); }, /** * Called after the constructor to render the element */ render: function() {}, /** * Sets the value of an attribute * * @param {String} attr Can also be an object to set multiple attributes at once * @param {Object} value */ set: function(attr, value) { if (typeof(attr) != 'string') { for (var k in attr) { this.set(k, attr[k]); } return; } this._attributes[attr] = value; if (typeof(this._boundAttributes[attr]) !== 'undefined') { for (var i = 0, c = this._boundAttributes[attr].length; i < c; i++) { this._boundAttributes[attr][i].apply(this, [value]); } } }, /** * Checks if an attribute exists and is not null * * @param {String} attr * @return {[type]} [description] */ has: function(attr) { return typeof(this._attributes[attr]) !== 'undefined' && this._attributes[attr] !== null; }, /** * Returns the value of an attribute * * @param {String} attr * @return {Object} */ get: function(attr) { return this._attributes[attr]; }, /** * Registers a callback function that will be called whenever the value of the attribute changes * * If cb is a jQuery element, text() will be used to fill the element * * @param {String} attr * @param {Function} cb */ bindAttr: function(attr, cb) { if ($.isArray(attr)) { for (var i = 0, c = attr.length; i < c; i++) { this.bindAttr(attr[i], cb); } return; } if (typeof(this._boundAttributes[attr]) == 'undefined') { this._boundAttributes[attr] = []; } if (typeof(cb) == 'object') { var el = cb; cb = function(value) { el.text(value || ''); }; } this._boundAttributes[attr].push(cb); if (this.has(attr)) { cb.apply(this, [this._attributes[attr]]); } } }); /** * Creates a subclass * * Code from Backbone.js * * @param {Array} props Prototype properties * @return {Function} */ Widget.extend = function(props) { var parent = this; var child = function() { return parent.apply(this, arguments); }; $.extend(child, parent); var Surrogate = function(){ this.constructor = child; }; Surrogate.prototype = parent.prototype; child.prototype = new Surrogate; $.extend(child.prototype, props); child.__super__ = parent.prototype; return child; }; // ------------------------------------------------------------------ /** * Tab * * A tab is composed of a tab label which is always visible and * a tab panel which is visible only when the tab is active. * * The panel must contain a widget. A widget is an object which has * an element property containing something appendable to a jQuery object. * * Options: * - title * - badge * - widget * - data: forward data to widget data */ var Tab = Widget.extend({ className: csscls('panel'), render: function() { this.$tab = $('').addClass(csscls('tab')); this.bindAttr('title', $('').addClass(csscls('text')).appendTo(this.$tab)); this.$badge = $('').addClass(csscls('badge')).appendTo(this.$tab); this.bindAttr('badge', function(value) { if (value !== null) { this.$badge.text(value); this.$badge.show(); } else { this.$badge.hide(); } }); this.bindAttr('widget', function(widget) { this.$el.empty().append(widget.$el); }); this.bindAttr('data', function(data) { if (this.has('widget')) { this.get('widget').set('data', data); } }) } }); // ------------------------------------------------------------------ /** * Indicator * * An indicator is a text and an icon to display single value information * right inside the always visible part of the debug bar * * Options: * - icon * - title * - tooltip * - position: "right" or "left" * - data: alias of title */ var Indicator = Widget.extend({ tagName: 'span', className: csscls('indicator'), defaults: { position: "right" }, render: function() { this.bindAttr('position', function(pos) { this.$el.css('float', pos); }); this.$icon = $('').appendTo(this.$el); this.bindAttr('icon', function(icon) { if (icon) { this.$icon.attr('class', 'icon-' + icon); } else { this.$icon.attr('class', ''); } }); this.bindAttr(['title', 'data'], $('').addClass(csscls('text')).appendTo(this.$el)); this.$tooltip = $('').addClass(csscls('tooltip disabled')).appendTo(this.$el); this.bindAttr('tooltip', function(tooltip) { if (tooltip) { this.$tooltip.text(tooltip).removeClass(csscls('disabled')); } else { this.$tooltip.addClass(csscls('disabled')); } }); } }); // ------------------------------------------------------------------ /** * Dataset title formater * * Formats the title of a dataset for the select box */ var DatasetTitleFormater = PhpDebugBar.DatasetTitleFormater = function(debugbar) { this.debugbar = debugbar; }; $.extend(DatasetTitleFormater.prototype, { /** * Formats the title of a dataset * * @this {DatasetTitleFormater} * @param {String} id * @param {Object} data * @param {String} suffix * @return {String} */ format: function(id, data, suffix) { if (suffix) { suffix = ' ' + suffix; } else { suffix = ''; } var nb = getObjectSize(this.debugbar.datasets) + 1; if (typeof(data['__meta']) === 'undefined') { return "#" + nb + suffix; } var filename = data['__meta']['uri'].substr(data['__meta']['uri'].lastIndexOf('/') + 1); var label = "#" + nb + " " + filename + suffix + ' (' + data['__meta']['datetime'].split(' ')[1] + ')'; return label; } }); // ------------------------------------------------------------------ /** * DebugBar * * Creates a bar that appends itself to the body of your page * and sticks to the bottom. * * The bar can be customized by adding tabs and indicators. * A data map is used to fill those controls with data provided * from datasets. */ var DebugBar = PhpDebugBar.DebugBar = Widget.extend({ className: "phpdebugbar " + csscls('minimized'), options: { bodyPaddingBottom: true }, initialize: function() { this.controls = {}; this.dataMap = {}; this.datasets = {}; this.firstTabName = null; this.activePanelName = null; this.datesetTitleFormater = new DatasetTitleFormater(this); }, /** * Initialiazes the UI * * @this {DebugBar} */ render: function() { var self = this; this.$el.appendTo('body'); this.$header = $('
').addClass(csscls('header')).appendTo(this.$el); var $body = this.$body = $('
').addClass(csscls('body')).appendTo(this.$el); this.$resizehdle = $('
').addClass(csscls('resize-handle')).appendTo(this.$body); this.recomputeBottomOffset(); // dragging of resize handle var dragging = false; this.$resizehdle.on('mousedown', function(e) { var orig_h = $body.height(), pos_y = e.pageY; dragging = true; $body.parents().on('mousemove', function(e) { if (dragging) { var h = orig_h + (pos_y - e.pageY); $body.css('height', h); localStorage.setItem('phpdebugbar-height', h); self.recomputeBottomOffset(); } }).on('mouseup', function() { dragging = false; }); e.preventDefault(); }); // minimize button this.$minimizebtn = $('').addClass(csscls('minimize-btn')).appendTo(this.$header); this.$minimizebtn.click(function() { self.minimize(); }); // open button this.$openbtn = $('').addClass(csscls('open-btn')).appendTo(this.$header).hide(); this.$openbtn.click(function() { self.openHandler.show(function(id, dataset) { self.addDataSet(dataset, id, "(opened)"); self.showTab(); }); }); // select box for data sets this.$datasets = $('