1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/domplate",
  7     "firebug/lib/locale",
  8     "firebug/lib/events",
  9     "firebug/lib/css",
 10     "firebug/lib/dom",
 11     "firebug/lib/http",
 12     "firebug/lib/string",
 13     "firebug/lib/json",
 14     "firebug/dom/toggleBranch",
 15     "firebug/lib/array",
 16     "firebug/lib/system",
 17     "firebug/dom/domPanel",
 18     "firebug/chrome/reps"
 19 ],
 20 function(Obj, Firebug, Domplate, Locale, Events, Css, Dom, Http, Str, Json,
 21     ToggleBranch, Arr, System) {
 22 
 23 // ********************************************************************************************* //
 24 
 25 // List of JSON content types.
 26 var contentTypes =
 27 {
 28     "text/plain": 1,
 29     "text/javascript": 1,
 30     "text/x-javascript": 1,
 31     "text/json": 1,
 32     "text/x-json": 1,
 33     "application/json": 1,
 34     "application/x-json": 1,
 35     "application/javascript": 1,
 36     "application/x-javascript": 1,
 37     "application/json-rpc": 1
 38 };
 39 
 40 // ********************************************************************************************* //
 41 // Model implementation
 42 
 43 Firebug.JSONViewerModel = Obj.extend(Firebug.Module,
 44 {
 45     dispatchName: "jsonViewer",
 46     contentTypes: contentTypes,
 47 
 48     initialize: function()
 49     {
 50         Firebug.NetMonitor.NetInfoBody.addListener(this);
 51         Firebug.registerUIListener(this);
 52     },
 53 
 54     shutdown: function()
 55     {
 56         Firebug.NetMonitor.NetInfoBody.removeListener(this);
 57         Firebug.unregisterUIListener(this);
 58     },
 59 
 60     onContextMenu: function(items, object, target, context, panel, popup)
 61     {
 62         if (panel.name != "net" && panel.name != "console")
 63             return;
 64 
 65         var memberLabel = Dom.getAncestorByClass(target, "memberLabel");
 66 
 67         if (!memberLabel)
 68             return;
 69 
 70         var row = Dom.getAncestorByClass(target, "memberRow");
 71         if (!row || !row.domObject.value)
 72             return;
 73 
 74         items.push({
 75            id: "fbNetCopyJSON",
 76            nol10n: true,
 77            label: Locale.$STRF("net.jsonviewer.Copy_JSON", [row.domObject.name]),
 78            command: Obj.bindFixed(this.copyJsonResponse, this, row)
 79         });
 80     },
 81 
 82     copyJsonResponse:function(row)
 83     {
 84         var value = JSON.stringify(row.domObject.value);
 85         if (value)
 86             System.copyToClipboard(value);
 87     },
 88 
 89     initTabBody: function(infoBox, file)
 90     {
 91         if (FBTrace.DBG_JSONVIEWER)
 92             FBTrace.sysout("jsonviewer.initTabBody", {infoBox: infoBox, file: file});
 93 
 94         // Let listeners to parse the JSON.
 95         Events.dispatch(this.fbListeners, "onParseJSON", [file]);
 96 
 97         // The JSON is still no there, try to parse most common cases.
 98         if (!file.jsonObject)
 99         {
100             if (this.isJSON(Http.safeGetContentType(file.request), file.responseText))
101                 file.jsonObject = this.parseJSON(file);
102         }
103 
104         // The jsonObject is created so, the JSON tab can be displayed.
105         if (file.jsonObject && Obj.hasProperties(file.jsonObject))
106         {
107             Firebug.NetMonitor.NetInfoBody.appendTab(infoBox, "JSON",
108                 Locale.$STR("jsonviewer.tab.JSON"));
109 
110             if (FBTrace.DBG_JSONVIEWER)
111                 FBTrace.sysout("jsonviewer.initTabBody; JSON object available " +
112                     (typeof(file.jsonObject) != "undefined"), file.jsonObject);
113         }
114     },
115 
116     isJSON: function(contentType, data)
117     {
118         // Workaround for JSON responses without proper content type
119         // Let's consider all responses starting with "{" as JSON. In the worst
120         // case there will be an exception when parsing. This means that no-JSON
121         // responses (and post data) (with "{") can be parsed unnecessarily,
122         // which represents a little overhead, but this happens only if the request
123         // is actually expanded by the user in the UI (Net & Console panels).
124         var responseText = data ? Str.trimLeft(data) : null;
125         if (responseText && responseText.charAt(0) === "{")
126             return true;
127 
128         if (!contentType)
129             return false;
130 
131         contentType = contentType.split(";")[0];
132         contentType = Str.trim(contentType);
133         return contentTypes[contentType];
134     },
135 
136     // Update listener for TabView
137     updateTabBody: function(infoBox, file, context)
138     {
139         var tab = infoBox.selectedTab;
140         var tabBody = infoBox.getElementsByClassName("netInfoJSONText").item(0);
141         if (!Css.hasClass(tab, "netInfoJSONTab") || tabBody.updated)
142             return;
143 
144         if (FBTrace.DBG_JSONVIEWER)
145             FBTrace.sysout("jsonviewer.updateTabBody", infoBox);
146 
147         tabBody.updated = true;
148         tabBody.context = context;
149 
150         this.Preview.render(tabBody, file, context);
151     },
152 
153     parseJSON: function(file)
154     {
155         var jsonString = new String(file.responseText);
156         return Json.parseJSONString(jsonString, "http://" + file.request.originalURI.host);
157     },
158 });
159 
160 // ********************************************************************************************* //
161 
162 with (Domplate) {
163 Firebug.JSONViewerModel.Preview = domplate(
164 {
165     bodyTag:
166         DIV({"class": "jsonPreview", _repObject: "$file"},
167             DIV({"class": "title"},
168                 DIV({"class": "sortLink", onclick: "$onSort", $sorted: "$sorted"},
169                     SPAN({"class": "doSort"}, Locale.$STR("jsonviewer.sort")),
170                     SPAN({"class": "doNotSort"}, Locale.$STR("jsonviewer.do not sort"))
171                 )
172             ),
173             DIV({"class": "jsonPreviewBody"})
174         ),
175 
176     onSort: function(event)
177     {
178         var target = event.target;
179         var sortLink = Dom.getAncestorByClass(target, "sortLink");
180         if (!sortLink)
181             return;
182 
183         Events.cancelEvent(event);
184 
185         Css.toggleClass(sortLink, "sorted");
186         Firebug.Options.set("sortJsonPreview", !Firebug.sortJsonPreview);
187 
188         var preview = Dom.getAncestorByClass(sortLink, "jsonPreview");
189         var body = Dom.getAncestorByClass(sortLink, "netInfoJSONText");
190         if (!body)
191         {
192             if (FBTrace.DBG_ERRORS)
193                 FBTrace.sysout("jsonViewer.onSort; ERROR body is null");
194             return;
195         }
196 
197         this.render(body, preview.repObject, body.context);
198     },
199 
200     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
201 
202     render: function(body, file, context)
203     {
204         if (!file.jsonObject)
205             return;
206 
207         if (!body.jsonTree)
208             body.jsonTree = new JSONTreePlate();
209 
210         var input = {file: file, sorted: Firebug.sortJsonPreview};
211         parentNode = this.bodyTag.replace(input, body, this);
212         parentNode = parentNode.getElementsByClassName("jsonPreviewBody").item(0);
213 
214         body.jsonTree.render(file.jsonObject, parentNode, context);
215     }
216 })};
217 
218 // ********************************************************************************************* //
219 
220 function JSONTreePlate()
221 {
222     // Used by Firebug.DOMPanel.DirTable domplate.
223     this.toggles = new ToggleBranch.ToggleBranch();
224 }
225 
226 // xxxHonza: this object is *not* a panel (using Firebug terminology), but
227 // there is no other way how to subclass the DOM Tree than to derive from the DOMBasePanel.
228 // Better solution would be to have a middle object between DirTablePlate and DOMBasePanel.
229 JSONTreePlate.prototype = Obj.extend(Firebug.DOMBasePanel.prototype,
230 {
231     dispatchName: "JSONTreePlate",
232 
233     render: function(jsonObject, parentNode, context)
234     {
235         try
236         {
237             this.panelNode = parentNode;
238             this.context = context;
239 
240             var members = this.getMembers(jsonObject, 0, context);
241             this.expandMembers(members, this.toggles, 0, 0, context);
242             this.showMembers(members, false, false);
243         }
244         catch (err)
245         {
246             if (FBTrace.DBG_ERRORS || FBTrace.DBG_JSONVIEWER)
247                 FBTrace.sysout("jsonviewer.render; EXCEPTION", err);
248         }
249     },
250 
251     getMembers: function(object, level, context)
252     {
253         if (!level)
254             level = 0;
255 
256         var members = [];
257 
258         for (var name in object)
259         {
260             var val = object[name];
261             this.addMember(object, "user", members, name, val, level, 0);
262         }
263 
264         function sortName(a, b) { return a.name > b.name ? 1 : -1; }
265 
266         // Sort only if it isn't an array (issue 4382).
267         if (Firebug.sortJsonPreview && !Arr.isArray(object, context.window))
268             members.sort(sortName);
269 
270         return members;
271     }
272 });
273 
274 // ********************************************************************************************* //
275 // Registration
276 
277 Firebug.registerModule(Firebug.JSONViewerModel);
278 
279 return Firebug.JSONViewerModel;
280 
281 // ********************************************************************************************* //
282 });
283