You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
312 lines
14 KiB
312 lines
14 KiB
/**
|
|
* jQuery dump plugin. Inspect object properties in a popup window.
|
|
*
|
|
* Copyright (c) 2012 Block Alexander
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Lesser General Public License for more details.
|
|
*/
|
|
(function ($) {
|
|
var __jqdump__dump_OFF = false; // global OFF switch
|
|
var position = { left: 100, top: 120 }; // popup window position
|
|
var defaultDepth = 2; // default dump depth
|
|
|
|
/**
|
|
* Dump object content in popup window with prettyprint and subnavigation
|
|
*
|
|
* <example>
|
|
* // dump navigator properties
|
|
* $.dump(window.navigator, 1, "window.navigator");
|
|
*
|
|
* // this may be used to disable all following function calls
|
|
* $.dump("off");
|
|
* </example>
|
|
*
|
|
* @param(obj): object to dump, or "off" string to disable all dump calls
|
|
* @param(iDepth): number, optional dumping depth, default 2
|
|
* @param(sHistoryPath): string, optional global properties path relative to the initial dump object value, used for display only.
|
|
* @returns: null
|
|
*/
|
|
$.dump = function (obj, iDepth, sHistoryPath) {
|
|
if (typeof(obj) == "string" && /^off$/i.test(obj)) { __jqdump__dump_OFF = true; }
|
|
return __jqdump__dump(obj, iDepth, sHistoryPath);
|
|
};
|
|
|
|
/**
|
|
* Dump object content in popup window with prettyprint and subnavigation
|
|
*
|
|
* <example>
|
|
* $("#element").dump(1);
|
|
* // same as
|
|
* $.dump($("#element"), 1, "(#element)");
|
|
* </example>
|
|
*
|
|
* Call type: window.dump/$.dump( ... )
|
|
* @param(iDepth): number, optional dumping depth, default 2
|
|
* @returns: null
|
|
*/
|
|
$.fn.dump = function (iDepth) {
|
|
return __jqdump__dump(this, iDepth, this.selector? "("+ this.selector +")" : "");
|
|
};
|
|
|
|
/**
|
|
* Dump object content in popup window with prettyprint and subnavigation
|
|
*
|
|
* @param(obj): object to dump
|
|
* @param(iDepth): number, optional dumping depth, default 2
|
|
* @param(sHistoryPath): string, optional global properties path relative to the initial dump object value, used for display only.
|
|
* @returns: null
|
|
*/
|
|
function __jqdump__dump (obj, iDepth, sHistoryPath) {
|
|
if (__jqdump__dump_OFF) { return null; }
|
|
|
|
// store current object value to allow continious/recursive dump browsing via window.opener.__jqdump__
|
|
__jqdump__ = {data: obj, dump: __jqdump__dump };
|
|
|
|
// provide defaults as needed
|
|
iDepth = (typeof(iDepth) == "number" && iDepth > 0? parseInt(iDepth, 10) : defaultDepth);
|
|
sHistoryPath = (typeof(sHistoryPath) == "string" && sHistoryPath.length > 0? sHistoryPath : "OBJECT");
|
|
|
|
// adjust new window position
|
|
position = { top: (position.top - 30) % 120, left: (position.left - 10) % 100 };
|
|
|
|
// try to popup blank page
|
|
var dumpWindow = window.open("about:blank", "_blank"
|
|
, "width=600,height=800,menubar=0,left="+ position.left +",top="+ position.top
|
|
+",location=0,toolbar=0,status=0,scrollbars=1,resizable=1", true);
|
|
|
|
// popup blocked?
|
|
if (!dumpWindow || dumpWindow.closed == true) {
|
|
if (confirm("Dump Window couldn't showup cause of popup blocker.\nContinue using current window?")) {
|
|
dumpWindow = window;
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// fill the page with dump content
|
|
dumpWindow.document.write("<html><head><title>"+ sHistoryPath +" @ "+ DateToHMSMString(new Date()) +"</title>"
|
|
+"<meta http-equiv='Content-Type' content='text/html;charset=utf-8'/>"
|
|
+"<style type='text/css'>"
|
|
+" body{background-color:#fff;color:#000;font-size:12px;margin-top:24px;}"
|
|
+" #toolbar{position:fixed;top:0px;right:0px;z-index:9999;}"
|
|
+" span.p.a:hover,span.p.a:hover+span {background-color: #B5F5FF;}"
|
|
+" a{text-decoration:none;}"
|
|
+" a:hover{text-decoration:underline;}"
|
|
+" .h{display:none;}" // hidden element
|
|
+" .a{cursor:pointer;}" // link like element
|
|
+" .s{color:#740;}" // string
|
|
+" .k{color:#427;font-weight:bold;font-style:italic;}" // key-word
|
|
+" .c{color:#666;font-style:italic;}" // comment
|
|
+" .u{color:#259;}" // reserved value
|
|
+" .p{color:#155;font-weight:bold;}" // punctuation
|
|
+" .d{color:#800;font-weight:bold;}" // diggit
|
|
+" .e{color:#900;font-style:italic;background-color:#FAA;}" // exception
|
|
+" .t{color:#080;font-weight:bold;}" // true boolean value
|
|
+" .f{color:#800;font-weight:bold;}" // false boolean value
|
|
+" .arg{color:#000;font-weight:normal;}" // number of function arguments
|
|
+"</style></head><body>"
|
|
+"<div id='toolbar'>"
|
|
+" <input type='button' id='btnClose' value='Close' onclick='window.close();'/>"
|
|
+" <input type='button' value='Collapse All' onclick='ToggleCollapse(true);'/>"
|
|
+" <input type='button' value='Expand All' onclick='ToggleCollapse(false);'/>"
|
|
+"</div>"
|
|
+"<code><pre>"+ sHistoryPath +" <span class='p'>=</span> "
|
|
+ __jqdump__next(obj, " ", iDepth, "__jqdump__.data", sHistoryPath)
|
|
+"<span class='p'>;</span></pre></code>"
|
|
// provide data and code to the parent window
|
|
+"<sc"+"ript type='text/javascript'><!-"+"-"
|
|
+"\n __jqdump__ = (window.opener? window.opener.__jqdump__ : window.__jqdump__);"
|
|
+"\n if (!__jqdump__) { __jqdump__ = {data: null, dump: function (o) { if (JSON) { alert(JSON.stringify(o)); } } }; }"
|
|
+"\n "
|
|
// focus the close button to allow fast window close by pressing button [space]
|
|
+"\n window.onload = function () {"
|
|
+"\n window.focus();"
|
|
+"\n document.getElementById('btnClose').focus();"
|
|
+"\n }"
|
|
+"\n "
|
|
+"\n function ToggleCollapse (bCollapse) {"
|
|
+"\n var span = document.getElementsByTagName('span');"
|
|
+"\n for (var i = 0; i < span.length; i++) {"
|
|
+"\n if (span[i].getAttribute('title') != 'collapse/expand' || (i == 1 && bCollapse)) { continue; }"
|
|
+"\n span[i].nextSibling.className = bCollapse? 'h' : '';"
|
|
+"\n }"
|
|
+"\n }"
|
|
+"\n-"+"-></sc"+"ript></body></html>"
|
|
);
|
|
|
|
// finalize writings
|
|
dumpWindow.document.close();
|
|
|
|
// ensure new window has data and code to continue further dumps
|
|
dumpWindow.__jqdump__ = {
|
|
data: __jqdump__.data
|
|
, dump: __jqdump__.dump
|
|
//*dbg*/, history: sHistoryPath
|
|
};
|
|
|
|
/**
|
|
* @param(obj): object to dump
|
|
* @param(sIndent): string, used for object properties alignment indentation
|
|
* @param(iDepth): number, dumping depth, defaults to 2
|
|
* @param(sContextPath): string, evaluable object properties path relative to the current obj value, used onclick event
|
|
* @param(sHistoryPath): string, global properties path relative to the initial dump obj value, used for display only
|
|
*/
|
|
function __jqdump__next (obj, sIndent, iDepth, sContextPath, sHistoryPath) {
|
|
var objType = typeof(obj);
|
|
if (null == obj && objType != "undefined") { return "<span class='u'>null</span>"; }
|
|
|
|
switch (objType) {
|
|
case "object": break;
|
|
case "undefined": return "<span class='u'>undefined</span>";
|
|
case "string": return obj.length? "\"<span class='s'>"+ htmlEscape(obj) +"</span>\"" : "\"\"";
|
|
case "number": return "<span class='d'>"+ obj.toString() +"</span>";
|
|
case "boolean": return "<span class='"+ (obj? "t" : "f") +"'>"+ obj.toString() +"</span>";
|
|
// allow dumping of function return value (simple function call without arguments)
|
|
case "function": return format(
|
|
"<a href='javascript:;' class='k'"
|
|
+" onclick='__jqdump__.dump((function(){try{return {0}();}catch(xcp){return {EXCEPTION_WRAPPER:xcp.toString()};}})()"
|
|
+","+ defaultDepth +",this.title);' title='{1}()'>func"+"tion({2})</a>"
|
|
, sContextPath
|
|
, sHistoryPath
|
|
, (obj.length? "<span class='arg'>"+ obj.length +"</span>" : "")
|
|
);
|
|
default: return "<span class='e'>/* Unknown object type: {"+ objType +"}*/</span>";
|
|
}
|
|
|
|
if (obj instanceof Date) { return "new Date(\""+ obj.toUTCString() +"\")"; }
|
|
|
|
if (iDepth == 0) { // stop here and allow deeper dumping by a click
|
|
return format("<a href='javascript:;' class='p' title='show more'"
|
|
+" onclick='__jqdump__.dump((function(){try{return {0};}catch(xcp){return {EXCEPTION_WRAPPER:xcp.toString()};}})(),"
|
|
+ defaultDepth +",\"{1}\");'"
|
|
+">{ ... }</a>"
|
|
, sContextPath
|
|
, sHistoryPath.replace(/"/g, "\\\""));
|
|
}
|
|
|
|
var bIsArray = (obj instanceof Array)
|
|
, sNewContextPath, sNewHistoryPath
|
|
, obja = [], prop;
|
|
|
|
var rv = [
|
|
"<span class='p a' title='collapse/expand'"
|
|
+" onclick='this.nextSibling.className=(this.nextSibling.className==\"h\"?\"\":\"h\")'"
|
|
+"> "+ (bIsArray? "[" : "{") +" </span><span>\n"
|
|
];
|
|
|
|
try {
|
|
// making sorted array of object property names
|
|
if (bIsArray) {
|
|
for (var i = 0; i < obj.length; i++) { obja.push(i); }
|
|
} else {
|
|
for (prop in obj) { obja.push(prop); }
|
|
obja.sort(function (a, b) {
|
|
return (isNaN(a)? (a.toLowerCase() >= b.toLowerCase()? 1 : -1) : (Number(a) >= Number(b))? 1 : -1);
|
|
});
|
|
}
|
|
|
|
// quering object with names via sorted array
|
|
for (var c = 0, length = obja.length; c < length; c++) {
|
|
try {
|
|
prop = obja[c];
|
|
// skip self properties
|
|
if (/__jqdump__/.test(prop)) { continue; }
|
|
|
|
if (bIsArray) { // array index:
|
|
sNewContextPath = format("{0}[\"{1}\"]", sContextPath, prop);
|
|
sNewHistoryPath = format("{0}[\"{1}\"]", sHistoryPath, prop);
|
|
rv.push(format(
|
|
"{0}<span class='a c' onclick='alert(this.title);' title='{1}'>/*{2}*/</span> "
|
|
, sIndent, sNewHistoryPath, prop
|
|
));
|
|
} else {// object property:
|
|
if (/(\W)|(^\d)/.test(prop)) {//- as string
|
|
sNewContextPath = format("{0}[\"{1}\"]", sContextPath, prop);
|
|
sNewHistoryPath = format("{0}[\"{1}\"]", sHistoryPath, htmlEscape(prop));
|
|
rv.push(format(
|
|
"{0}<span class='s a' onclick='alert(this.title);' title='{1}'>\"{2}\"</span> <span class='p'>:</span> "
|
|
, sIndent, sNewHistoryPath, htmlEscape(prop)
|
|
));
|
|
} else {//- as conventional variable name
|
|
sNewContextPath = format("{0}.{1}", sContextPath, prop);
|
|
sNewHistoryPath = format("{0}.{1}", sHistoryPath, htmlEscape(prop));
|
|
rv.push(format(
|
|
"{0}<span class='a' onclick='alert(this.title);' title='{1}'>{2}</span> <span class='p'>:</span> "
|
|
, sIndent, sNewHistoryPath, htmlEscape(prop)
|
|
));
|
|
}
|
|
}
|
|
|
|
rv.push(__jqdump__next(obj[prop], sIndent +" ", iDepth - 1, sNewContextPath, sNewHistoryPath));
|
|
} catch (xcp) {
|
|
rv.push(format("<span class='e'>/*{0} - {1}*/</span>", xcp.name, xcp.message));
|
|
}
|
|
|
|
rv.push((c < (obja.length-1)? "<span class='p'>,</span>\n" : "\n"));
|
|
}
|
|
} catch (xcp) {
|
|
rv.push(format("<span class='e'>/*{0} - {1}*/</span>", xcp.name, xcp.message));
|
|
}
|
|
|
|
rv.push(format("{0}</span><span class='p'>{1}</span>", sIndent.replace(" ", ""), (bIsArray? "]" : "}")));
|
|
|
|
return rv.join("");
|
|
};
|
|
|
|
/**
|
|
* Escape native string characters before writing it as html text
|
|
*/
|
|
function htmlEscape (str) {
|
|
// trying to speedup the process by checking the length
|
|
return str.length? str.replace(/&/g, "&").replace(/\</g, "<").replace(/\>/g, ">") : str;
|
|
};
|
|
|
|
/**
|
|
* Replaces each format item "{n}" in `this` string with the string equivalent of a corresponding
|
|
* parameters object's value. For example "{0}" reffers to the second argument, "{1}" to the third.
|
|
*
|
|
* @note: format identifier "{i}" may be repeated multiple times in any order as long as it reffers
|
|
* to corresponding position of the argument
|
|
* @param(0): first parameter is a format-string, containing meta information of insertion positions
|
|
* @param(...): any type, other arguments reffered by the format-string will be evaluated to string
|
|
* @return: string, formating result
|
|
*/
|
|
function format (/* ... */) {
|
|
var match = null, rv = arguments[0];
|
|
|
|
for (var c = 1, length = arguments.length; c < length; c++) {
|
|
match = new RegExp("\\{"+ (c - 1) +"\\}", "g");
|
|
if (match.test(rv)) {
|
|
rv = rv.replace(match, typeof(arguments[c]) == "undefined" ? "(undefined)" : arguments[c].toString());
|
|
}
|
|
}
|
|
|
|
match = null;
|
|
return rv;
|
|
};
|
|
|
|
/**
|
|
* Time to string in HH:MM:SS.mmm format
|
|
* @param(d): date object
|
|
*/
|
|
function DateToHMSMString (d) {
|
|
var iHours = d.getHours(), iMinutes = d.getMinutes(), iSeconds = d.getSeconds(), iMSeconds = d.getMilliseconds();
|
|
|
|
return (iHours < 10? "0" : "") + iHours +":"+ (iMinutes < 10? "0" : "") + iMinutes
|
|
+":"+ (iSeconds < 10? "0" : "") + iSeconds
|
|
+"."+ (iMSeconds < 100? "0" : "") + (iMSeconds < 10? "0" : "") + iMSeconds;
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
})(jQuery);
|