1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-18 17:12:43 +01:00
invoiceninja/public/packages/barryvdh/laravel-debugbar/debugbar.js
2013-11-26 14:45:07 +02:00

980 lines
28 KiB
JavaScript
Executable File

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 = $('<a href="javascript:" />').addClass(csscls('tab'));
this.bindAttr('title', $('<span />').addClass(csscls('text')).appendTo(this.$tab));
this.$badge = $('<span />').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 = $('<i />').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'], $('<span />').addClass(csscls('text')).appendTo(this.$el));
this.$tooltip = $('<span />').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 = $('<div />').addClass(csscls('header')).appendTo(this.$el);
var $body = this.$body = $('<div />').addClass(csscls('body')).appendTo(this.$el);
this.$resizehdle = $('<div />').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 = $('<a href="javascript:" />').addClass(csscls('minimize-btn')).appendTo(this.$header);
this.$minimizebtn.click(function() {
self.minimize();
});
// open button
this.$openbtn = $('<a href="javascript:" />').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 = $('<select />').addClass(csscls('datasets-switcher')).appendTo(this.$header);
this.$datasets.change(function() {
self.dataChangeHandler(self.datasets[this.value]);
self.showTab();
});
},
/**
* Restores the state of the DebugBar using localStorage
* This is not called by default in the constructor and
* needs to be called by subclasses in their init() method
*
* @this {DebugBar}
*/
restoreState: function() {
// bar height
var height = localStorage.getItem('phpdebugbar-height');
if (height) {
this.$body.css('height', height);
} else {
localStorage.setItem('phpdebugbar-height', this.$body.height());
}
// bar visibility
var visible = localStorage.getItem('phpdebugbar-visible');
if (visible && visible == '1') {
var tab = localStorage.getItem('phpdebugbar-tab');
if (this.isTab(tab)) {
this.showTab(tab);
}
}
},
/**
* Creates and adds a new tab
*
* @this {DebugBar}
* @param {String} name Internal name
* @param {Object} widget A widget object with an element property
* @param {String} title The text in the tab, if not specified, name will be used
* @return {Tab}
*/
createTab: function(name, widget, title) {
var tab = new Tab({
title: title || (name.replace(/[_\-]/g, ' ').charAt(0).toUpperCase() + name.slice(1)),
widget: widget
});
return this.addTab(name, tab);
},
/**
* Adds a new tab
*
* @this {DebugBar}
* @param {String} name Internal name
* @param {Tab} tab Tab object
* @return {Tab}
*/
addTab: function(name, tab) {
if (this.isControl(name)) {
throw new Error(name + ' already exists');
}
var self = this;
tab.$tab.appendTo(this.$header).click(function() {
if (!self.isMinimized() && self.activePanelName == name) {
self.minimize();
} else {
self.showTab(name);
}
});
tab.$el.appendTo(this.$body);
this.controls[name] = tab;
if (this.firstTabName == null) {
this.firstTabName = name;
}
return tab;
},
/**
* Creates and adds an indicator
*
* @this {DebugBar}
* @param {String} name Internal name
* @param {String} icon
* @param {String} tooltip
* @param {String} position "right" or "left", default is "right"
* @return {Indicator}
*/
createIndicator: function(name, icon, tooltip, position) {
var indicator = new Indicator({
icon: icon,
tooltip: tooltip,
position: position || 'right'
});
return this.addIndicator(name, indicator);
},
/**
* Adds an indicator
*
* @this {DebugBar}
* @param {String} name Internal name
* @param {Indicator} indicator Indicator object
* @return {Indicator}
*/
addIndicator: function(name, indicator) {
if (this.isControl(name)) {
throw new Error(name + ' already exists');
}
if (indicator.get('position') == 'right') {
indicator.$el.appendTo(this.$header);
} else {
indicator.$el.insertBefore(this.$header.children().first())
}
this.controls[name] = indicator;
return indicator;
},
/**
* Returns a control
*
* @param {String} name
* @return {Object}
*/
getControl: function(name) {
if (this.isControl(name)) {
return this.controls[name];
}
},
/**
* Checks if there's a control under the specified name
*
* @this {DebugBar}
* @param {String} name
* @return {Boolean}
*/
isControl: function(name) {
return typeof(this.controls[name]) != 'undefined';
},
/**
* Checks if a tab with the specified name exists
*
* @this {DebugBar}
* @param {String} name
* @return {Boolean}
*/
isTab: function(name) {
return this.isControl(name) && this.controls[name] instanceof Tab;
},
/**
* Checks if an indicator with the specified name exists
*
* @this {DebugBar}
* @param {String} name
* @return {Boolean}
*/
isIndicator: function(name) {
return this.isControl(name) && this.controls[name] instanceof Indicator;
},
/**
* Removes all tabs and indicators from the debug bar and hides it
*
* @this {DebugBar}
*/
reset: function() {
this.minimize();
var self = this;
$.each(this.controls, function(name, control) {
if (self.isTab(name)) {
control.$tab.remove();
}
control.$el.remove();
});
this.controls = {};
},
/**
* Open the debug bar and display the specified tab
*
* @this {DebugBar}
* @param {String} name If not specified, display the first tab
*/
showTab: function(name) {
if (!name) {
if (this.activePanelName) {
name = this.activePanelName;
} else {
name = this.firstTabName;
}
}
if (!this.isTab(name)) {
throw new Error("Unknown tab '" + name + "'");
}
this.$resizehdle.show();
this.$body.show();
this.$minimizebtn.show();
this.recomputeBottomOffset();
$(this.$header).find('> .' + csscls('active')).removeClass(csscls('active'));
$(this.$body).find('> .' + csscls('active')).removeClass(csscls('active'));
this.controls[name].$tab.addClass(csscls('active'));
this.controls[name].$el.addClass(csscls('active'));
this.activePanelName = name;
this.$el.removeClass(csscls('minimized'));
localStorage.setItem('phpdebugbar-visible', '1');
localStorage.setItem('phpdebugbar-tab', name);
},
/**
* Hide panels and "close" the debug bar
*
* @this {DebugBar}
*/
minimize: function() {
this.$header.find('> .' + csscls('active')).removeClass(csscls('active'));
this.$body.hide();
this.$minimizebtn.hide();
this.$resizehdle.hide();
this.recomputeBottomOffset();
localStorage.setItem('phpdebugbar-visible', '0');
this.$el.addClass(csscls('minimized'));
},
/**
* Checks if the panel is minimized
*
* @return {Boolean}
*/
isMinimized: function() {
return this.$el.hasClass(csscls('minimized'));
},
/**
* Recomputes the padding-bottom css property of the body so
* that the debug bar never hides any content
*/
recomputeBottomOffset: function() {
if (this.options.bodyPaddingBottom) {
$('body').css('padding-bottom', this.$el.height());
}
},
/**
* Sets the data map used by dataChangeHandler to populate
* indicators and widgets
*
* A data map is an object where properties are control names.
* The value of each property should be an array where the first
* item is the name of a property from the data object (nested properties
* can be specified) and the second item the default value.
*
* Example:
* {"memory": ["memory.peak_usage_str", "0B"]}
*
* @this {DebugBar}
* @param {Object} map
*/
setDataMap: function(map) {
this.dataMap = map;
},
/**
* Same as setDataMap() but appends to the existing map
* rather than replacing it
*
* @this {DebugBar}
* @param {Object} map
*/
addDataMap: function(map) {
$.extend(this.dataMap, map);
},
/**
* Resets datasets and add one set of data
*
* For this method to be usefull, you need to specify
* a dataMap using setDataMap()
*
* @this {DebugBar}
* @param {Object} data
* @return {String} Dataset's id
*/
setData: function(data) {
this.datasets = {};
return this.addDataSet(data);
},
/**
* Adds a dataset
*
* If more than one dataset are added, the dataset selector
* will be displayed.
*
* For this method to be usefull, you need to specify
* a dataMap using setDataMap()
*
* @this {DebugBar}
* @param {Object} data
* @param {String} id The name of this set, optional
* @param {String} suffix
* @return {String} Dataset's id
*/
addDataSet: function(data, id, suffix) {
var label = this.datesetTitleFormater.format(id, data, suffix);
id = id || (getObjectSize(this.datasets) + 1);
this.datasets[id] = data;
this.$datasets.append($('<option value="' + id + '">' + label + '</option>'));
if (this.$datasets.children().length > 1) {
this.$datasets.show();
}
this.showDataSet(id);
return id;
},
/**
* Loads a dataset using the open handler
*
* @param {String} id
*/
loadDataSet: function(id, suffix, callback) {
if (!this.openHandler) {
throw new Error('loadDataSet() needs an open handler');
}
var self = this;
this.openHandler.load(id, function(data) {
self.addDataSet(data, id, suffix);
callback && callback(data);
});
},
/**
* Returns the data from a dataset
*
* @this {DebugBar}
* @param {String} id
* @return {Object}
*/
getDataSet: function(id) {
return this.datasets[id];
},
/**
* Switch the currently displayed dataset
*
* @this {DebugBar}
* @param {String} id
*/
showDataSet: function(id) {
this.dataChangeHandler(this.datasets[id]);
this.$datasets.val(id);
},
/**
* Called when the current dataset is modified.
*
* @this {DebugBar}
* @param {Object} data
*/
dataChangeHandler: function(data) {
var self = this;
$.each(this.dataMap, function(key, def) {
var d = getDictValue(data, def[0], def[1]);
if (key.indexOf(':') != -1) {
key = key.split(':');
self.getControl(key[0]).set(key[1], d);
} else {
self.getControl(key).set('data', d);
}
});
},
/**
* Sets the handler to open past dataset
*
* @this {DebugBar}
* @param {object} handler
*/
setOpenHandler: function(handler) {
this.openHandler = handler;
if (handler !== null) {
this.$openbtn.show();
} else {
this.$openbtn.hide();
}
},
/**
* Returns the handler to open past dataset
*
* @this {DebugBar}
* @return {object}
*/
getOpenHandler: function() {
return this.openHandler;
}
});
DebugBar.Tab = Tab;
DebugBar.Indicator = Indicator;
// ------------------------------------------------------------------
/**
* AjaxHandler
*
* Extract data from headers of an XMLHttpRequest and adds a new dataset
*/
var AjaxHandler = PhpDebugBar.AjaxHandler = function(debugbar, headerName) {
this.debugbar = debugbar;
this.headerName = headerName || 'phpdebugbar';
};
$.extend(AjaxHandler.prototype, {
/**
* Handles an XMLHttpRequest
*
* @this {AjaxHandler}
* @param {XMLHttpRequest} xhr
* @return {Bool}
*/
handle: function(xhr) {
if (!this.loadFromId(xhr)) {
return this.loadFromData(xhr);
}
return true;
},
/**
* Checks if the HEADER-id exists and loads the dataset using the open handler
*
* @param {XMLHttpRequest} xhr
* @return {Bool}
*/
loadFromId: function(xhr) {
var id = this.extractIdFromHeaders(xhr);
if (id && this.debugbar.openHandler) {
this.debugbar.loadDataSet(id, "(ajax)");
return true;
}
return false;
},
/**
* Extracts the id from the HEADER-id
*
* @param {XMLHttpRequest} xhr
* @return {String}
*/
extractIdFromHeaders: function(xhr) {
return xhr.getResponseHeader(this.headerName + '-id');
},
/**
* Checks if the HEADER exists and loads the dataset
*
* @param {XMLHttpRequest} xhr
* @return {Bool}
*/
loadFromData: function(xhr) {
var raw = this.extractDataFromHeaders(xhr);
if (!raw) {
return false;
}
var data = this.parseHeaders(raw);
this.debugbar.addDataSet(data.data, data.id, "(ajax)");
return true;
},
/**
* Extract the data as a string from headers of an XMLHttpRequest
*
* @this {AjaxHandler}
* @param {XMLHttpRequest} xhr
* @return {string}
*/
extractDataFromHeaders: function(xhr) {
var data = xhr.getResponseHeader(this.headerName);
if (!data) {
return;
}
for (var i = 1;; i++) {
var header = xhr.getResponseHeader(this.headerName + '-' + i);
if (!header) {
break;
}
data += header;
}
return decodeURIComponent(data);
},
/**
* Parses the string data into an object
*
* @this {AjaxHandler}
* @param {string} data
* @return {string}
*/
parseHeaders: function(data) {
return JSON.parse(data);
},
/**
* Attaches an event listener to jQuery.ajaxComplete()
*
* @this {AjaxHandler}
* @param {jQuery} jq Optional
*/
bindToJquery: function(jq) {
var self = this;
jq(document).ajaxComplete(function(e, xhr, settings) {
if (!settings.ignoreDebugBarAjaxHandler) {
self.handle(xhr);
}
});
}
});
})(PhpDebugBar.$);