|
1 /* -*- Mode: javascript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 "use strict"; |
|
7 |
|
8 const Ci = Components.interfaces; |
|
9 const Cu = Components.utils; |
|
10 |
|
11 const DBG_STRINGS_URI = "chrome://browser/locale/devtools/debugger.properties"; |
|
12 const LAZY_EMPTY_DELAY = 150; // ms |
|
13 const LAZY_EXPAND_DELAY = 50; // ms |
|
14 const SCROLL_PAGE_SIZE_DEFAULT = 0; |
|
15 const APPEND_PAGE_SIZE_DEFAULT = 500; |
|
16 const PAGE_SIZE_SCROLL_HEIGHT_RATIO = 100; |
|
17 const PAGE_SIZE_MAX_JUMPS = 30; |
|
18 const SEARCH_ACTION_MAX_DELAY = 300; // ms |
|
19 const ITEM_FLASH_DURATION = 300 // ms |
|
20 |
|
21 Cu.import("resource://gre/modules/Services.jsm"); |
|
22 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
23 Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
|
24 Cu.import("resource://gre/modules/devtools/event-emitter.js"); |
|
25 Cu.import("resource://gre/modules/devtools/DevToolsUtils.jsm"); |
|
26 Cu.import("resource://gre/modules/Task.jsm"); |
|
27 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); |
|
28 |
|
29 XPCOMUtils.defineLazyModuleGetter(this, "devtools", |
|
30 "resource://gre/modules/devtools/Loader.jsm"); |
|
31 |
|
32 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
33 "resource://gre/modules/PluralForm.jsm"); |
|
34 |
|
35 XPCOMUtils.defineLazyServiceGetter(this, "clipboardHelper", |
|
36 "@mozilla.org/widget/clipboardhelper;1", |
|
37 "nsIClipboardHelper"); |
|
38 |
|
39 Object.defineProperty(this, "WebConsoleUtils", { |
|
40 get: function() { |
|
41 return devtools.require("devtools/toolkit/webconsole/utils").Utils; |
|
42 }, |
|
43 configurable: true, |
|
44 enumerable: true |
|
45 }); |
|
46 |
|
47 Object.defineProperty(this, "NetworkHelper", { |
|
48 get: function() { |
|
49 return devtools.require("devtools/toolkit/webconsole/network-helper"); |
|
50 }, |
|
51 configurable: true, |
|
52 enumerable: true |
|
53 }); |
|
54 |
|
55 this.EXPORTED_SYMBOLS = ["VariablesView", "escapeHTML"]; |
|
56 |
|
57 /** |
|
58 * Debugger localization strings. |
|
59 */ |
|
60 const STR = Services.strings.createBundle(DBG_STRINGS_URI); |
|
61 |
|
62 /** |
|
63 * A tree view for inspecting scopes, objects and properties. |
|
64 * Iterable via "for (let [id, scope] of instance) { }". |
|
65 * Requires the devtools common.css and debugger.css skin stylesheets. |
|
66 * |
|
67 * To allow replacing variable or property values in this view, provide an |
|
68 * "eval" function property. To allow replacing variable or property names, |
|
69 * provide a "switch" function. To handle deleting variables or properties, |
|
70 * provide a "delete" function. |
|
71 * |
|
72 * @param nsIDOMNode aParentNode |
|
73 * The parent node to hold this view. |
|
74 * @param object aFlags [optional] |
|
75 * An object contaning initialization options for this view. |
|
76 * e.g. { lazyEmpty: true, searchEnabled: true ... } |
|
77 */ |
|
78 this.VariablesView = function VariablesView(aParentNode, aFlags = {}) { |
|
79 this._store = []; // Can't use a Map because Scope names needn't be unique. |
|
80 this._itemsByElement = new WeakMap(); |
|
81 this._prevHierarchy = new Map(); |
|
82 this._currHierarchy = new Map(); |
|
83 |
|
84 this._parent = aParentNode; |
|
85 this._parent.classList.add("variables-view-container"); |
|
86 this._parent.classList.add("theme-body"); |
|
87 this._appendEmptyNotice(); |
|
88 |
|
89 this._onSearchboxInput = this._onSearchboxInput.bind(this); |
|
90 this._onSearchboxKeyPress = this._onSearchboxKeyPress.bind(this); |
|
91 this._onViewKeyPress = this._onViewKeyPress.bind(this); |
|
92 this._onViewKeyDown = this._onViewKeyDown.bind(this); |
|
93 |
|
94 // Create an internal scrollbox container. |
|
95 this._list = this.document.createElement("scrollbox"); |
|
96 this._list.setAttribute("orient", "vertical"); |
|
97 this._list.addEventListener("keypress", this._onViewKeyPress, false); |
|
98 this._list.addEventListener("keydown", this._onViewKeyDown, false); |
|
99 this._parent.appendChild(this._list); |
|
100 |
|
101 for (let name in aFlags) { |
|
102 this[name] = aFlags[name]; |
|
103 } |
|
104 |
|
105 EventEmitter.decorate(this); |
|
106 }; |
|
107 |
|
108 VariablesView.prototype = { |
|
109 /** |
|
110 * Helper setter for populating this container with a raw object. |
|
111 * |
|
112 * @param object aObject |
|
113 * The raw object to display. You can only provide this object |
|
114 * if you want the variables view to work in sync mode. |
|
115 */ |
|
116 set rawObject(aObject) { |
|
117 this.empty(); |
|
118 this.addScope() |
|
119 .addItem("", { enumerable: true }) |
|
120 .populate(aObject, { sorted: true }); |
|
121 }, |
|
122 |
|
123 /** |
|
124 * Adds a scope to contain any inspected variables. |
|
125 * |
|
126 * This new scope will be considered the parent of any other scope |
|
127 * added afterwards. |
|
128 * |
|
129 * @param string aName |
|
130 * The scope's name (e.g. "Local", "Global" etc.). |
|
131 * @return Scope |
|
132 * The newly created Scope instance. |
|
133 */ |
|
134 addScope: function(aName = "") { |
|
135 this._removeEmptyNotice(); |
|
136 this._toggleSearchVisibility(true); |
|
137 |
|
138 let scope = new Scope(this, aName); |
|
139 this._store.push(scope); |
|
140 this._itemsByElement.set(scope._target, scope); |
|
141 this._currHierarchy.set(aName, scope); |
|
142 scope.header = !!aName; |
|
143 |
|
144 return scope; |
|
145 }, |
|
146 |
|
147 /** |
|
148 * Removes all items from this container. |
|
149 * |
|
150 * @param number aTimeout [optional] |
|
151 * The number of milliseconds to delay the operation if |
|
152 * lazy emptying of this container is enabled. |
|
153 */ |
|
154 empty: function(aTimeout = this.lazyEmptyDelay) { |
|
155 // If there are no items in this container, emptying is useless. |
|
156 if (!this._store.length) { |
|
157 return; |
|
158 } |
|
159 |
|
160 this._store.length = 0; |
|
161 this._itemsByElement.clear(); |
|
162 this._prevHierarchy = this._currHierarchy; |
|
163 this._currHierarchy = new Map(); // Don't clear, this is just simple swapping. |
|
164 |
|
165 // Check if this empty operation may be executed lazily. |
|
166 if (this.lazyEmpty && aTimeout > 0) { |
|
167 this._emptySoon(aTimeout); |
|
168 return; |
|
169 } |
|
170 |
|
171 while (this._list.hasChildNodes()) { |
|
172 this._list.firstChild.remove(); |
|
173 } |
|
174 |
|
175 this._appendEmptyNotice(); |
|
176 this._toggleSearchVisibility(false); |
|
177 }, |
|
178 |
|
179 /** |
|
180 * Emptying this container and rebuilding it immediately afterwards would |
|
181 * result in a brief redraw flicker, because the previously expanded nodes |
|
182 * may get asynchronously re-expanded, after fetching the prototype and |
|
183 * properties from a server. |
|
184 * |
|
185 * To avoid such behaviour, a normal container list is rebuild, but not |
|
186 * immediately attached to the parent container. The old container list |
|
187 * is kept around for a short period of time, hopefully accounting for the |
|
188 * data fetching delay. In the meantime, any operations can be executed |
|
189 * normally. |
|
190 * |
|
191 * @see VariablesView.empty |
|
192 * @see VariablesView.commitHierarchy |
|
193 */ |
|
194 _emptySoon: function(aTimeout) { |
|
195 let prevList = this._list; |
|
196 let currList = this._list = this.document.createElement("scrollbox"); |
|
197 |
|
198 this.window.setTimeout(() => { |
|
199 prevList.removeEventListener("keypress", this._onViewKeyPress, false); |
|
200 prevList.removeEventListener("keydown", this._onViewKeyDown, false); |
|
201 currList.addEventListener("keypress", this._onViewKeyPress, false); |
|
202 currList.addEventListener("keydown", this._onViewKeyDown, false); |
|
203 currList.setAttribute("orient", "vertical"); |
|
204 |
|
205 this._parent.removeChild(prevList); |
|
206 this._parent.appendChild(currList); |
|
207 |
|
208 if (!this._store.length) { |
|
209 this._appendEmptyNotice(); |
|
210 this._toggleSearchVisibility(false); |
|
211 } |
|
212 }, aTimeout); |
|
213 }, |
|
214 |
|
215 /** |
|
216 * Optional DevTools toolbox containing this VariablesView. Used to |
|
217 * communicate with the inspector and highlighter. |
|
218 */ |
|
219 toolbox: null, |
|
220 |
|
221 /** |
|
222 * The controller for this VariablesView, if it has one. |
|
223 */ |
|
224 controller: null, |
|
225 |
|
226 /** |
|
227 * The amount of time (in milliseconds) it takes to empty this view lazily. |
|
228 */ |
|
229 lazyEmptyDelay: LAZY_EMPTY_DELAY, |
|
230 |
|
231 /** |
|
232 * Specifies if this view may be emptied lazily. |
|
233 * @see VariablesView.prototype.empty |
|
234 */ |
|
235 lazyEmpty: false, |
|
236 |
|
237 /** |
|
238 * Specifies if nodes in this view may be searched lazily. |
|
239 */ |
|
240 lazySearch: true, |
|
241 |
|
242 /** |
|
243 * The number of elements in this container to jump when Page Up or Page Down |
|
244 * keys are pressed. If falsy, then the page size will be based on the |
|
245 * container height. |
|
246 */ |
|
247 scrollPageSize: SCROLL_PAGE_SIZE_DEFAULT, |
|
248 |
|
249 /** |
|
250 * The maximum number of elements allowed in a scope, variable or property |
|
251 * that allows pagination when appending children. |
|
252 */ |
|
253 appendPageSize: APPEND_PAGE_SIZE_DEFAULT, |
|
254 |
|
255 /** |
|
256 * Function called each time a variable or property's value is changed via |
|
257 * user interaction. If null, then value changes are disabled. |
|
258 * |
|
259 * This property is applied recursively onto each scope in this view and |
|
260 * affects only the child nodes when they're created. |
|
261 */ |
|
262 eval: null, |
|
263 |
|
264 /** |
|
265 * Function called each time a variable or property's name is changed via |
|
266 * user interaction. If null, then name changes are disabled. |
|
267 * |
|
268 * This property is applied recursively onto each scope in this view and |
|
269 * affects only the child nodes when they're created. |
|
270 */ |
|
271 switch: null, |
|
272 |
|
273 /** |
|
274 * Function called each time a variable or property is deleted via |
|
275 * user interaction. If null, then deletions are disabled. |
|
276 * |
|
277 * This property is applied recursively onto each scope in this view and |
|
278 * affects only the child nodes when they're created. |
|
279 */ |
|
280 delete: null, |
|
281 |
|
282 /** |
|
283 * Function called each time a property is added via user interaction. If |
|
284 * null, then property additions are disabled. |
|
285 * |
|
286 * This property is applied recursively onto each scope in this view and |
|
287 * affects only the child nodes when they're created. |
|
288 */ |
|
289 new: null, |
|
290 |
|
291 /** |
|
292 * Specifies if after an eval or switch operation, the variable or property |
|
293 * which has been edited should be disabled. |
|
294 */ |
|
295 preventDisableOnChange: false, |
|
296 |
|
297 /** |
|
298 * Specifies if, whenever a variable or property descriptor is available, |
|
299 * configurable, enumerable, writable, frozen, sealed and extensible |
|
300 * attributes should not affect presentation. |
|
301 * |
|
302 * This flag is applied recursively onto each scope in this view and |
|
303 * affects only the child nodes when they're created. |
|
304 */ |
|
305 preventDescriptorModifiers: false, |
|
306 |
|
307 /** |
|
308 * The tooltip text shown on a variable or property's value if an |eval| |
|
309 * function is provided, in order to change the variable or property's value. |
|
310 * |
|
311 * This flag is applied recursively onto each scope in this view and |
|
312 * affects only the child nodes when they're created. |
|
313 */ |
|
314 editableValueTooltip: STR.GetStringFromName("variablesEditableValueTooltip"), |
|
315 |
|
316 /** |
|
317 * The tooltip text shown on a variable or property's name if a |switch| |
|
318 * function is provided, in order to change the variable or property's name. |
|
319 * |
|
320 * This flag is applied recursively onto each scope in this view and |
|
321 * affects only the child nodes when they're created. |
|
322 */ |
|
323 editableNameTooltip: STR.GetStringFromName("variablesEditableNameTooltip"), |
|
324 |
|
325 /** |
|
326 * The tooltip text shown on a variable or property's edit button if an |
|
327 * |eval| function is provided and a getter/setter descriptor is present, |
|
328 * in order to change the variable or property to a plain value. |
|
329 * |
|
330 * This flag is applied recursively onto each scope in this view and |
|
331 * affects only the child nodes when they're created. |
|
332 */ |
|
333 editButtonTooltip: STR.GetStringFromName("variablesEditButtonTooltip"), |
|
334 |
|
335 /** |
|
336 * The tooltip text shown on a variable or property's value if that value is |
|
337 * a DOMNode that can be highlighted and selected in the inspector. |
|
338 * |
|
339 * This flag is applied recursively onto each scope in this view and |
|
340 * affects only the child nodes when they're created. |
|
341 */ |
|
342 domNodeValueTooltip: STR.GetStringFromName("variablesDomNodeValueTooltip"), |
|
343 |
|
344 /** |
|
345 * The tooltip text shown on a variable or property's delete button if a |
|
346 * |delete| function is provided, in order to delete the variable or property. |
|
347 * |
|
348 * This flag is applied recursively onto each scope in this view and |
|
349 * affects only the child nodes when they're created. |
|
350 */ |
|
351 deleteButtonTooltip: STR.GetStringFromName("variablesCloseButtonTooltip"), |
|
352 |
|
353 /** |
|
354 * Specifies the context menu attribute set on variables and properties. |
|
355 * |
|
356 * This flag is applied recursively onto each scope in this view and |
|
357 * affects only the child nodes when they're created. |
|
358 */ |
|
359 contextMenuId: "", |
|
360 |
|
361 /** |
|
362 * The separator label between the variables or properties name and value. |
|
363 * |
|
364 * This flag is applied recursively onto each scope in this view and |
|
365 * affects only the child nodes when they're created. |
|
366 */ |
|
367 separatorStr: STR.GetStringFromName("variablesSeparatorLabel"), |
|
368 |
|
369 /** |
|
370 * Specifies if enumerable properties and variables should be displayed. |
|
371 * These variables and properties are visible by default. |
|
372 * @param boolean aFlag |
|
373 */ |
|
374 set enumVisible(aFlag) { |
|
375 this._enumVisible = aFlag; |
|
376 |
|
377 for (let scope of this._store) { |
|
378 scope._enumVisible = aFlag; |
|
379 } |
|
380 }, |
|
381 |
|
382 /** |
|
383 * Specifies if non-enumerable properties and variables should be displayed. |
|
384 * These variables and properties are visible by default. |
|
385 * @param boolean aFlag |
|
386 */ |
|
387 set nonEnumVisible(aFlag) { |
|
388 this._nonEnumVisible = aFlag; |
|
389 |
|
390 for (let scope of this._store) { |
|
391 scope._nonEnumVisible = aFlag; |
|
392 } |
|
393 }, |
|
394 |
|
395 /** |
|
396 * Specifies if only enumerable properties and variables should be displayed. |
|
397 * Both types of these variables and properties are visible by default. |
|
398 * @param boolean aFlag |
|
399 */ |
|
400 set onlyEnumVisible(aFlag) { |
|
401 if (aFlag) { |
|
402 this.enumVisible = true; |
|
403 this.nonEnumVisible = false; |
|
404 } else { |
|
405 this.enumVisible = true; |
|
406 this.nonEnumVisible = true; |
|
407 } |
|
408 }, |
|
409 |
|
410 /** |
|
411 * Sets if the variable and property searching is enabled. |
|
412 * @param boolean aFlag |
|
413 */ |
|
414 set searchEnabled(aFlag) aFlag ? this._enableSearch() : this._disableSearch(), |
|
415 |
|
416 /** |
|
417 * Gets if the variable and property searching is enabled. |
|
418 * @return boolean |
|
419 */ |
|
420 get searchEnabled() !!this._searchboxContainer, |
|
421 |
|
422 /** |
|
423 * Sets the text displayed for the searchbox in this container. |
|
424 * @param string aValue |
|
425 */ |
|
426 set searchPlaceholder(aValue) { |
|
427 if (this._searchboxNode) { |
|
428 this._searchboxNode.setAttribute("placeholder", aValue); |
|
429 } |
|
430 this._searchboxPlaceholder = aValue; |
|
431 }, |
|
432 |
|
433 /** |
|
434 * Gets the text displayed for the searchbox in this container. |
|
435 * @return string |
|
436 */ |
|
437 get searchPlaceholder() this._searchboxPlaceholder, |
|
438 |
|
439 /** |
|
440 * Enables variable and property searching in this view. |
|
441 * Use the "searchEnabled" setter to enable searching. |
|
442 */ |
|
443 _enableSearch: function() { |
|
444 // If searching was already enabled, no need to re-enable it again. |
|
445 if (this._searchboxContainer) { |
|
446 return; |
|
447 } |
|
448 let document = this.document; |
|
449 let ownerNode = this._parent.parentNode; |
|
450 |
|
451 let container = this._searchboxContainer = document.createElement("hbox"); |
|
452 container.className = "devtools-toolbar"; |
|
453 |
|
454 // Hide the variables searchbox container if there are no variables or |
|
455 // properties to display. |
|
456 container.hidden = !this._store.length; |
|
457 |
|
458 let searchbox = this._searchboxNode = document.createElement("textbox"); |
|
459 searchbox.className = "variables-view-searchinput devtools-searchinput"; |
|
460 searchbox.setAttribute("placeholder", this._searchboxPlaceholder); |
|
461 searchbox.setAttribute("type", "search"); |
|
462 searchbox.setAttribute("flex", "1"); |
|
463 searchbox.addEventListener("input", this._onSearchboxInput, false); |
|
464 searchbox.addEventListener("keypress", this._onSearchboxKeyPress, false); |
|
465 |
|
466 container.appendChild(searchbox); |
|
467 ownerNode.insertBefore(container, this._parent); |
|
468 }, |
|
469 |
|
470 /** |
|
471 * Disables variable and property searching in this view. |
|
472 * Use the "searchEnabled" setter to disable searching. |
|
473 */ |
|
474 _disableSearch: function() { |
|
475 // If searching was already disabled, no need to re-disable it again. |
|
476 if (!this._searchboxContainer) { |
|
477 return; |
|
478 } |
|
479 this._searchboxContainer.remove(); |
|
480 this._searchboxNode.removeEventListener("input", this._onSearchboxInput, false); |
|
481 this._searchboxNode.removeEventListener("keypress", this._onSearchboxKeyPress, false); |
|
482 |
|
483 this._searchboxContainer = null; |
|
484 this._searchboxNode = null; |
|
485 }, |
|
486 |
|
487 /** |
|
488 * Sets the variables searchbox container hidden or visible. |
|
489 * It's hidden by default. |
|
490 * |
|
491 * @param boolean aVisibleFlag |
|
492 * Specifies the intended visibility. |
|
493 */ |
|
494 _toggleSearchVisibility: function(aVisibleFlag) { |
|
495 // If searching was already disabled, there's no need to hide it. |
|
496 if (!this._searchboxContainer) { |
|
497 return; |
|
498 } |
|
499 this._searchboxContainer.hidden = !aVisibleFlag; |
|
500 }, |
|
501 |
|
502 /** |
|
503 * Listener handling the searchbox input event. |
|
504 */ |
|
505 _onSearchboxInput: function() { |
|
506 this.scheduleSearch(this._searchboxNode.value); |
|
507 }, |
|
508 |
|
509 /** |
|
510 * Listener handling the searchbox key press event. |
|
511 */ |
|
512 _onSearchboxKeyPress: function(e) { |
|
513 switch(e.keyCode) { |
|
514 case e.DOM_VK_RETURN: |
|
515 this._onSearchboxInput(); |
|
516 return; |
|
517 case e.DOM_VK_ESCAPE: |
|
518 this._searchboxNode.value = ""; |
|
519 this._onSearchboxInput(); |
|
520 return; |
|
521 } |
|
522 }, |
|
523 |
|
524 /** |
|
525 * Schedules searching for variables or properties matching the query. |
|
526 * |
|
527 * @param string aToken |
|
528 * The variable or property to search for. |
|
529 * @param number aWait |
|
530 * The amount of milliseconds to wait until draining. |
|
531 */ |
|
532 scheduleSearch: function(aToken, aWait) { |
|
533 // Check if this search operation may not be executed lazily. |
|
534 if (!this.lazySearch) { |
|
535 this._doSearch(aToken); |
|
536 return; |
|
537 } |
|
538 |
|
539 // The amount of time to wait for the requests to settle. |
|
540 let maxDelay = SEARCH_ACTION_MAX_DELAY; |
|
541 let delay = aWait === undefined ? maxDelay / aToken.length : aWait; |
|
542 |
|
543 // Allow requests to settle down first. |
|
544 setNamedTimeout("vview-search", delay, () => this._doSearch(aToken)); |
|
545 }, |
|
546 |
|
547 /** |
|
548 * Performs a case insensitive search for variables or properties matching |
|
549 * the query, and hides non-matched items. |
|
550 * |
|
551 * If aToken is falsy, then all the scopes are unhidden and expanded, |
|
552 * while the available variables and properties inside those scopes are |
|
553 * just unhidden. |
|
554 * |
|
555 * @param string aToken |
|
556 * The variable or property to search for. |
|
557 */ |
|
558 _doSearch: function(aToken) { |
|
559 for (let scope of this._store) { |
|
560 switch (aToken) { |
|
561 case "": |
|
562 case null: |
|
563 case undefined: |
|
564 scope.expand(); |
|
565 scope._performSearch(""); |
|
566 break; |
|
567 default: |
|
568 scope._performSearch(aToken.toLowerCase()); |
|
569 break; |
|
570 } |
|
571 } |
|
572 }, |
|
573 |
|
574 /** |
|
575 * Find the first item in the tree of visible items in this container that |
|
576 * matches the predicate. Searches in visual order (the order seen by the |
|
577 * user). Descends into each scope to check the scope and its children. |
|
578 * |
|
579 * @param function aPredicate |
|
580 * A function that returns true when a match is found. |
|
581 * @return Scope | Variable | Property |
|
582 * The first visible scope, variable or property, or null if nothing |
|
583 * is found. |
|
584 */ |
|
585 _findInVisibleItems: function(aPredicate) { |
|
586 for (let scope of this._store) { |
|
587 let result = scope._findInVisibleItems(aPredicate); |
|
588 if (result) { |
|
589 return result; |
|
590 } |
|
591 } |
|
592 return null; |
|
593 }, |
|
594 |
|
595 /** |
|
596 * Find the last item in the tree of visible items in this container that |
|
597 * matches the predicate. Searches in reverse visual order (opposite of the |
|
598 * order seen by the user). Descends into each scope to check the scope and |
|
599 * its children. |
|
600 * |
|
601 * @param function aPredicate |
|
602 * A function that returns true when a match is found. |
|
603 * @return Scope | Variable | Property |
|
604 * The last visible scope, variable or property, or null if nothing |
|
605 * is found. |
|
606 */ |
|
607 _findInVisibleItemsReverse: function(aPredicate) { |
|
608 for (let i = this._store.length - 1; i >= 0; i--) { |
|
609 let scope = this._store[i]; |
|
610 let result = scope._findInVisibleItemsReverse(aPredicate); |
|
611 if (result) { |
|
612 return result; |
|
613 } |
|
614 } |
|
615 return null; |
|
616 }, |
|
617 |
|
618 /** |
|
619 * Gets the scope at the specified index. |
|
620 * |
|
621 * @param number aIndex |
|
622 * The scope's index. |
|
623 * @return Scope |
|
624 * The scope if found, undefined if not. |
|
625 */ |
|
626 getScopeAtIndex: function(aIndex) { |
|
627 return this._store[aIndex]; |
|
628 }, |
|
629 |
|
630 /** |
|
631 * Recursively searches this container for the scope, variable or property |
|
632 * displayed by the specified node. |
|
633 * |
|
634 * @param nsIDOMNode aNode |
|
635 * The node to search for. |
|
636 * @return Scope | Variable | Property |
|
637 * The matched scope, variable or property, or null if nothing is found. |
|
638 */ |
|
639 getItemForNode: function(aNode) { |
|
640 return this._itemsByElement.get(aNode); |
|
641 }, |
|
642 |
|
643 /** |
|
644 * Gets the scope owning a Variable or Property. |
|
645 * |
|
646 * @param Variable | Property |
|
647 * The variable or property to retrieven the owner scope for. |
|
648 * @return Scope |
|
649 * The owner scope. |
|
650 */ |
|
651 getOwnerScopeForVariableOrProperty: function(aItem) { |
|
652 if (!aItem) { |
|
653 return null; |
|
654 } |
|
655 // If this is a Scope, return it. |
|
656 if (!(aItem instanceof Variable)) { |
|
657 return aItem; |
|
658 } |
|
659 // If this is a Variable or Property, find its owner scope. |
|
660 if (aItem instanceof Variable && aItem.ownerView) { |
|
661 return this.getOwnerScopeForVariableOrProperty(aItem.ownerView); |
|
662 } |
|
663 return null; |
|
664 }, |
|
665 |
|
666 /** |
|
667 * Gets the parent scopes for a specified Variable or Property. |
|
668 * The returned list will not include the owner scope. |
|
669 * |
|
670 * @param Variable | Property |
|
671 * The variable or property for which to find the parent scopes. |
|
672 * @return array |
|
673 * A list of parent Scopes. |
|
674 */ |
|
675 getParentScopesForVariableOrProperty: function(aItem) { |
|
676 let scope = this.getOwnerScopeForVariableOrProperty(aItem); |
|
677 return this._store.slice(0, Math.max(this._store.indexOf(scope), 0)); |
|
678 }, |
|
679 |
|
680 /** |
|
681 * Gets the currently focused scope, variable or property in this view. |
|
682 * |
|
683 * @return Scope | Variable | Property |
|
684 * The focused scope, variable or property, or null if nothing is found. |
|
685 */ |
|
686 getFocusedItem: function() { |
|
687 let focused = this.document.commandDispatcher.focusedElement; |
|
688 return this.getItemForNode(focused); |
|
689 }, |
|
690 |
|
691 /** |
|
692 * Focuses the first visible scope, variable, or property in this container. |
|
693 */ |
|
694 focusFirstVisibleItem: function() { |
|
695 let focusableItem = this._findInVisibleItems(item => item.focusable); |
|
696 if (focusableItem) { |
|
697 this._focusItem(focusableItem); |
|
698 } |
|
699 this._parent.scrollTop = 0; |
|
700 this._parent.scrollLeft = 0; |
|
701 }, |
|
702 |
|
703 /** |
|
704 * Focuses the last visible scope, variable, or property in this container. |
|
705 */ |
|
706 focusLastVisibleItem: function() { |
|
707 let focusableItem = this._findInVisibleItemsReverse(item => item.focusable); |
|
708 if (focusableItem) { |
|
709 this._focusItem(focusableItem); |
|
710 } |
|
711 this._parent.scrollTop = this._parent.scrollHeight; |
|
712 this._parent.scrollLeft = 0; |
|
713 }, |
|
714 |
|
715 /** |
|
716 * Focuses the next scope, variable or property in this view. |
|
717 */ |
|
718 focusNextItem: function() { |
|
719 this.focusItemAtDelta(+1); |
|
720 }, |
|
721 |
|
722 /** |
|
723 * Focuses the previous scope, variable or property in this view. |
|
724 */ |
|
725 focusPrevItem: function() { |
|
726 this.focusItemAtDelta(-1); |
|
727 }, |
|
728 |
|
729 /** |
|
730 * Focuses another scope, variable or property in this view, based on |
|
731 * the index distance from the currently focused item. |
|
732 * |
|
733 * @param number aDelta |
|
734 * A scalar specifying by how many items should the selection change. |
|
735 */ |
|
736 focusItemAtDelta: function(aDelta) { |
|
737 let direction = aDelta > 0 ? "advanceFocus" : "rewindFocus"; |
|
738 let distance = Math.abs(Math[aDelta > 0 ? "ceil" : "floor"](aDelta)); |
|
739 while (distance--) { |
|
740 if (!this._focusChange(direction)) { |
|
741 break; // Out of bounds. |
|
742 } |
|
743 } |
|
744 }, |
|
745 |
|
746 /** |
|
747 * Focuses the next or previous scope, variable or property in this view. |
|
748 * |
|
749 * @param string aDirection |
|
750 * Either "advanceFocus" or "rewindFocus". |
|
751 * @return boolean |
|
752 * False if the focus went out of bounds and the first or last element |
|
753 * in this view was focused instead. |
|
754 */ |
|
755 _focusChange: function(aDirection) { |
|
756 let commandDispatcher = this.document.commandDispatcher; |
|
757 let prevFocusedElement = commandDispatcher.focusedElement; |
|
758 let currFocusedItem = null; |
|
759 |
|
760 do { |
|
761 commandDispatcher.suppressFocusScroll = true; |
|
762 commandDispatcher[aDirection](); |
|
763 |
|
764 // Make sure the newly focused item is a part of this view. |
|
765 // If the focus goes out of bounds, revert the previously focused item. |
|
766 if (!(currFocusedItem = this.getFocusedItem())) { |
|
767 prevFocusedElement.focus(); |
|
768 return false; |
|
769 } |
|
770 } while (!currFocusedItem.focusable); |
|
771 |
|
772 // Focus remained within bounds. |
|
773 return true; |
|
774 }, |
|
775 |
|
776 /** |
|
777 * Focuses a scope, variable or property and makes sure it's visible. |
|
778 * |
|
779 * @param aItem Scope | Variable | Property |
|
780 * The item to focus. |
|
781 * @param boolean aCollapseFlag |
|
782 * True if the focused item should also be collapsed. |
|
783 * @return boolean |
|
784 * True if the item was successfully focused. |
|
785 */ |
|
786 _focusItem: function(aItem, aCollapseFlag) { |
|
787 if (!aItem.focusable) { |
|
788 return false; |
|
789 } |
|
790 if (aCollapseFlag) { |
|
791 aItem.collapse(); |
|
792 } |
|
793 aItem._target.focus(); |
|
794 this.boxObject.ensureElementIsVisible(aItem._arrow); |
|
795 return true; |
|
796 }, |
|
797 |
|
798 /** |
|
799 * Listener handling a key press event on the view. |
|
800 */ |
|
801 _onViewKeyPress: function(e) { |
|
802 let item = this.getFocusedItem(); |
|
803 |
|
804 // Prevent scrolling when pressing navigation keys. |
|
805 ViewHelpers.preventScrolling(e); |
|
806 |
|
807 switch (e.keyCode) { |
|
808 case e.DOM_VK_UP: |
|
809 // Always rewind focus. |
|
810 this.focusPrevItem(true); |
|
811 return; |
|
812 |
|
813 case e.DOM_VK_DOWN: |
|
814 // Always advance focus. |
|
815 this.focusNextItem(true); |
|
816 return; |
|
817 |
|
818 case e.DOM_VK_LEFT: |
|
819 // Collapse scopes, variables and properties before rewinding focus. |
|
820 if (item._isExpanded && item._isArrowVisible) { |
|
821 item.collapse(); |
|
822 } else { |
|
823 this._focusItem(item.ownerView); |
|
824 } |
|
825 return; |
|
826 |
|
827 case e.DOM_VK_RIGHT: |
|
828 // Nothing to do here if this item never expands. |
|
829 if (!item._isArrowVisible) { |
|
830 return; |
|
831 } |
|
832 // Expand scopes, variables and properties before advancing focus. |
|
833 if (!item._isExpanded) { |
|
834 item.expand(); |
|
835 } else { |
|
836 this.focusNextItem(true); |
|
837 } |
|
838 return; |
|
839 |
|
840 case e.DOM_VK_PAGE_UP: |
|
841 // Rewind a certain number of elements based on the container height. |
|
842 this.focusItemAtDelta(-(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight / |
|
843 PAGE_SIZE_SCROLL_HEIGHT_RATIO), |
|
844 PAGE_SIZE_MAX_JUMPS))); |
|
845 return; |
|
846 |
|
847 case e.DOM_VK_PAGE_DOWN: |
|
848 // Advance a certain number of elements based on the container height. |
|
849 this.focusItemAtDelta(+(this.scrollPageSize || Math.min(Math.floor(this._list.scrollHeight / |
|
850 PAGE_SIZE_SCROLL_HEIGHT_RATIO), |
|
851 PAGE_SIZE_MAX_JUMPS))); |
|
852 return; |
|
853 |
|
854 case e.DOM_VK_HOME: |
|
855 this.focusFirstVisibleItem(); |
|
856 return; |
|
857 |
|
858 case e.DOM_VK_END: |
|
859 this.focusLastVisibleItem(); |
|
860 return; |
|
861 |
|
862 case e.DOM_VK_RETURN: |
|
863 // Start editing the value or name of the Variable or Property. |
|
864 if (item instanceof Variable) { |
|
865 if (e.metaKey || e.altKey || e.shiftKey) { |
|
866 item._activateNameInput(); |
|
867 } else { |
|
868 item._activateValueInput(); |
|
869 } |
|
870 } |
|
871 return; |
|
872 |
|
873 case e.DOM_VK_DELETE: |
|
874 case e.DOM_VK_BACK_SPACE: |
|
875 // Delete the Variable or Property if allowed. |
|
876 if (item instanceof Variable) { |
|
877 item._onDelete(e); |
|
878 } |
|
879 return; |
|
880 |
|
881 case e.DOM_VK_INSERT: |
|
882 item._onAddProperty(e); |
|
883 return; |
|
884 } |
|
885 }, |
|
886 |
|
887 /** |
|
888 * Listener handling a key down event on the view. |
|
889 */ |
|
890 _onViewKeyDown: function(e) { |
|
891 if (e.keyCode == e.DOM_VK_C) { |
|
892 // Copy current selection to clipboard. |
|
893 if (e.ctrlKey || e.metaKey) { |
|
894 let item = this.getFocusedItem(); |
|
895 clipboardHelper.copyString( |
|
896 item._nameString + item.separatorStr + item._valueString |
|
897 ); |
|
898 } |
|
899 } |
|
900 }, |
|
901 |
|
902 /** |
|
903 * Sets the text displayed in this container when there are no available items. |
|
904 * @param string aValue |
|
905 */ |
|
906 set emptyText(aValue) { |
|
907 if (this._emptyTextNode) { |
|
908 this._emptyTextNode.setAttribute("value", aValue); |
|
909 } |
|
910 this._emptyTextValue = aValue; |
|
911 this._appendEmptyNotice(); |
|
912 }, |
|
913 |
|
914 /** |
|
915 * Creates and appends a label signaling that this container is empty. |
|
916 */ |
|
917 _appendEmptyNotice: function() { |
|
918 if (this._emptyTextNode || !this._emptyTextValue) { |
|
919 return; |
|
920 } |
|
921 |
|
922 let label = this.document.createElement("label"); |
|
923 label.className = "variables-view-empty-notice"; |
|
924 label.setAttribute("value", this._emptyTextValue); |
|
925 |
|
926 this._parent.appendChild(label); |
|
927 this._emptyTextNode = label; |
|
928 }, |
|
929 |
|
930 /** |
|
931 * Removes the label signaling that this container is empty. |
|
932 */ |
|
933 _removeEmptyNotice: function() { |
|
934 if (!this._emptyTextNode) { |
|
935 return; |
|
936 } |
|
937 |
|
938 this._parent.removeChild(this._emptyTextNode); |
|
939 this._emptyTextNode = null; |
|
940 }, |
|
941 |
|
942 /** |
|
943 * Gets if all values should be aligned together. |
|
944 * @return boolean |
|
945 */ |
|
946 get alignedValues() { |
|
947 return this._alignedValues; |
|
948 }, |
|
949 |
|
950 /** |
|
951 * Sets if all values should be aligned together. |
|
952 * @param boolean aFlag |
|
953 */ |
|
954 set alignedValues(aFlag) { |
|
955 this._alignedValues = aFlag; |
|
956 if (aFlag) { |
|
957 this._parent.setAttribute("aligned-values", ""); |
|
958 } else { |
|
959 this._parent.removeAttribute("aligned-values"); |
|
960 } |
|
961 }, |
|
962 |
|
963 /** |
|
964 * Gets if action buttons (like delete) should be placed at the beginning or |
|
965 * end of a line. |
|
966 * @return boolean |
|
967 */ |
|
968 get actionsFirst() { |
|
969 return this._actionsFirst; |
|
970 }, |
|
971 |
|
972 /** |
|
973 * Sets if action buttons (like delete) should be placed at the beginning or |
|
974 * end of a line. |
|
975 * @param boolean aFlag |
|
976 */ |
|
977 set actionsFirst(aFlag) { |
|
978 this._actionsFirst = aFlag; |
|
979 if (aFlag) { |
|
980 this._parent.setAttribute("actions-first", ""); |
|
981 } else { |
|
982 this._parent.removeAttribute("actions-first"); |
|
983 } |
|
984 }, |
|
985 |
|
986 /** |
|
987 * Gets the parent node holding this view. |
|
988 * @return nsIDOMNode |
|
989 */ |
|
990 get boxObject() this._list.boxObject.QueryInterface(Ci.nsIScrollBoxObject), |
|
991 |
|
992 /** |
|
993 * Gets the parent node holding this view. |
|
994 * @return nsIDOMNode |
|
995 */ |
|
996 get parentNode() this._parent, |
|
997 |
|
998 /** |
|
999 * Gets the owner document holding this view. |
|
1000 * @return nsIHTMLDocument |
|
1001 */ |
|
1002 get document() this._document || (this._document = this._parent.ownerDocument), |
|
1003 |
|
1004 /** |
|
1005 * Gets the default window holding this view. |
|
1006 * @return nsIDOMWindow |
|
1007 */ |
|
1008 get window() this._window || (this._window = this.document.defaultView), |
|
1009 |
|
1010 _document: null, |
|
1011 _window: null, |
|
1012 |
|
1013 _store: null, |
|
1014 _itemsByElement: null, |
|
1015 _prevHierarchy: null, |
|
1016 _currHierarchy: null, |
|
1017 |
|
1018 _enumVisible: true, |
|
1019 _nonEnumVisible: true, |
|
1020 _alignedValues: false, |
|
1021 _actionsFirst: false, |
|
1022 |
|
1023 _parent: null, |
|
1024 _list: null, |
|
1025 _searchboxNode: null, |
|
1026 _searchboxContainer: null, |
|
1027 _searchboxPlaceholder: "", |
|
1028 _emptyTextNode: null, |
|
1029 _emptyTextValue: "" |
|
1030 }; |
|
1031 |
|
1032 VariablesView.NON_SORTABLE_CLASSES = [ |
|
1033 "Array", |
|
1034 "Int8Array", |
|
1035 "Uint8Array", |
|
1036 "Uint8ClampedArray", |
|
1037 "Int16Array", |
|
1038 "Uint16Array", |
|
1039 "Int32Array", |
|
1040 "Uint32Array", |
|
1041 "Float32Array", |
|
1042 "Float64Array" |
|
1043 ]; |
|
1044 |
|
1045 /** |
|
1046 * Determine whether an object's properties should be sorted based on its class. |
|
1047 * |
|
1048 * @param string aClassName |
|
1049 * The class of the object. |
|
1050 */ |
|
1051 VariablesView.isSortable = function(aClassName) { |
|
1052 return VariablesView.NON_SORTABLE_CLASSES.indexOf(aClassName) == -1; |
|
1053 }; |
|
1054 |
|
1055 /** |
|
1056 * Generates the string evaluated when performing simple value changes. |
|
1057 * |
|
1058 * @param Variable | Property aItem |
|
1059 * The current variable or property. |
|
1060 * @param string aCurrentString |
|
1061 * The trimmed user inputted string. |
|
1062 * @param string aPrefix [optional] |
|
1063 * Prefix for the symbolic name. |
|
1064 * @return string |
|
1065 * The string to be evaluated. |
|
1066 */ |
|
1067 VariablesView.simpleValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") { |
|
1068 return aPrefix + aItem._symbolicName + "=" + aCurrentString; |
|
1069 }; |
|
1070 |
|
1071 /** |
|
1072 * Generates the string evaluated when overriding getters and setters with |
|
1073 * plain values. |
|
1074 * |
|
1075 * @param Property aItem |
|
1076 * The current getter or setter property. |
|
1077 * @param string aCurrentString |
|
1078 * The trimmed user inputted string. |
|
1079 * @param string aPrefix [optional] |
|
1080 * Prefix for the symbolic name. |
|
1081 * @return string |
|
1082 * The string to be evaluated. |
|
1083 */ |
|
1084 VariablesView.overrideValueEvalMacro = function(aItem, aCurrentString, aPrefix = "") { |
|
1085 let property = "\"" + aItem._nameString + "\""; |
|
1086 let parent = aPrefix + aItem.ownerView._symbolicName || "this"; |
|
1087 |
|
1088 return "Object.defineProperty(" + parent + "," + property + "," + |
|
1089 "{ value: " + aCurrentString + |
|
1090 ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" + |
|
1091 ", configurable: true" + |
|
1092 ", writable: true" + |
|
1093 "})"; |
|
1094 }; |
|
1095 |
|
1096 /** |
|
1097 * Generates the string evaluated when performing getters and setters changes. |
|
1098 * |
|
1099 * @param Property aItem |
|
1100 * The current getter or setter property. |
|
1101 * @param string aCurrentString |
|
1102 * The trimmed user inputted string. |
|
1103 * @param string aPrefix [optional] |
|
1104 * Prefix for the symbolic name. |
|
1105 * @return string |
|
1106 * The string to be evaluated. |
|
1107 */ |
|
1108 VariablesView.getterOrSetterEvalMacro = function(aItem, aCurrentString, aPrefix = "") { |
|
1109 let type = aItem._nameString; |
|
1110 let propertyObject = aItem.ownerView; |
|
1111 let parentObject = propertyObject.ownerView; |
|
1112 let property = "\"" + propertyObject._nameString + "\""; |
|
1113 let parent = aPrefix + parentObject._symbolicName || "this"; |
|
1114 |
|
1115 switch (aCurrentString) { |
|
1116 case "": |
|
1117 case "null": |
|
1118 case "undefined": |
|
1119 let mirrorType = type == "get" ? "set" : "get"; |
|
1120 let mirrorLookup = type == "get" ? "__lookupSetter__" : "__lookupGetter__"; |
|
1121 |
|
1122 // If the parent object will end up without any getter or setter, |
|
1123 // morph it into a plain value. |
|
1124 if ((type == "set" && propertyObject.getter.type == "undefined") || |
|
1125 (type == "get" && propertyObject.setter.type == "undefined")) { |
|
1126 // Make sure the right getter/setter to value override macro is applied |
|
1127 // to the target object. |
|
1128 return propertyObject.evaluationMacro(propertyObject, "undefined", aPrefix); |
|
1129 } |
|
1130 |
|
1131 // Construct and return the getter/setter removal evaluation string. |
|
1132 // e.g: Object.defineProperty(foo, "bar", { |
|
1133 // get: foo.__lookupGetter__("bar"), |
|
1134 // set: undefined, |
|
1135 // enumerable: true, |
|
1136 // configurable: true |
|
1137 // }) |
|
1138 return "Object.defineProperty(" + parent + "," + property + "," + |
|
1139 "{" + mirrorType + ":" + parent + "." + mirrorLookup + "(" + property + ")" + |
|
1140 "," + type + ":" + undefined + |
|
1141 ", enumerable: " + parent + ".propertyIsEnumerable(" + property + ")" + |
|
1142 ", configurable: true" + |
|
1143 "})"; |
|
1144 |
|
1145 default: |
|
1146 // Wrap statements inside a function declaration if not already wrapped. |
|
1147 if (!aCurrentString.startsWith("function")) { |
|
1148 let header = "function(" + (type == "set" ? "value" : "") + ")"; |
|
1149 let body = ""; |
|
1150 // If there's a return statement explicitly written, always use the |
|
1151 // standard function definition syntax |
|
1152 if (aCurrentString.contains("return ")) { |
|
1153 body = "{" + aCurrentString + "}"; |
|
1154 } |
|
1155 // If block syntax is used, use the whole string as the function body. |
|
1156 else if (aCurrentString.startsWith("{")) { |
|
1157 body = aCurrentString; |
|
1158 } |
|
1159 // Prefer an expression closure. |
|
1160 else { |
|
1161 body = "(" + aCurrentString + ")"; |
|
1162 } |
|
1163 aCurrentString = header + body; |
|
1164 } |
|
1165 |
|
1166 // Determine if a new getter or setter should be defined. |
|
1167 let defineType = type == "get" ? "__defineGetter__" : "__defineSetter__"; |
|
1168 |
|
1169 // Make sure all quotes are escaped in the expression's syntax, |
|
1170 let defineFunc = "eval(\"(" + aCurrentString.replace(/"/g, "\\$&") + ")\")"; |
|
1171 |
|
1172 // Construct and return the getter/setter evaluation string. |
|
1173 // e.g: foo.__defineGetter__("bar", eval("(function() { return 42; })")) |
|
1174 return parent + "." + defineType + "(" + property + "," + defineFunc + ")"; |
|
1175 } |
|
1176 }; |
|
1177 |
|
1178 /** |
|
1179 * Function invoked when a getter or setter is deleted. |
|
1180 * |
|
1181 * @param Property aItem |
|
1182 * The current getter or setter property. |
|
1183 */ |
|
1184 VariablesView.getterOrSetterDeleteCallback = function(aItem) { |
|
1185 aItem._disable(); |
|
1186 |
|
1187 // Make sure the right getter/setter to value override macro is applied |
|
1188 // to the target object. |
|
1189 aItem.ownerView.eval(aItem, ""); |
|
1190 |
|
1191 return true; // Don't hide the element. |
|
1192 }; |
|
1193 |
|
1194 |
|
1195 /** |
|
1196 * A Scope is an object holding Variable instances. |
|
1197 * Iterable via "for (let [name, variable] of instance) { }". |
|
1198 * |
|
1199 * @param VariablesView aView |
|
1200 * The view to contain this scope. |
|
1201 * @param string aName |
|
1202 * The scope's name. |
|
1203 * @param object aFlags [optional] |
|
1204 * Additional options or flags for this scope. |
|
1205 */ |
|
1206 function Scope(aView, aName, aFlags = {}) { |
|
1207 this.ownerView = aView; |
|
1208 |
|
1209 this._onClick = this._onClick.bind(this); |
|
1210 this._openEnum = this._openEnum.bind(this); |
|
1211 this._openNonEnum = this._openNonEnum.bind(this); |
|
1212 |
|
1213 // Inherit properties and flags from the parent view. You can override |
|
1214 // each of these directly onto any scope, variable or property instance. |
|
1215 this.scrollPageSize = aView.scrollPageSize; |
|
1216 this.appendPageSize = aView.appendPageSize; |
|
1217 this.eval = aView.eval; |
|
1218 this.switch = aView.switch; |
|
1219 this.delete = aView.delete; |
|
1220 this.new = aView.new; |
|
1221 this.preventDisableOnChange = aView.preventDisableOnChange; |
|
1222 this.preventDescriptorModifiers = aView.preventDescriptorModifiers; |
|
1223 this.editableNameTooltip = aView.editableNameTooltip; |
|
1224 this.editableValueTooltip = aView.editableValueTooltip; |
|
1225 this.editButtonTooltip = aView.editButtonTooltip; |
|
1226 this.deleteButtonTooltip = aView.deleteButtonTooltip; |
|
1227 this.domNodeValueTooltip = aView.domNodeValueTooltip; |
|
1228 this.contextMenuId = aView.contextMenuId; |
|
1229 this.separatorStr = aView.separatorStr; |
|
1230 |
|
1231 this._init(aName.trim(), aFlags); |
|
1232 } |
|
1233 |
|
1234 Scope.prototype = { |
|
1235 /** |
|
1236 * Whether this Scope should be prefetched when it is remoted. |
|
1237 */ |
|
1238 shouldPrefetch: true, |
|
1239 |
|
1240 /** |
|
1241 * Whether this Scope should paginate its contents. |
|
1242 */ |
|
1243 allowPaginate: false, |
|
1244 |
|
1245 /** |
|
1246 * The class name applied to this scope's target element. |
|
1247 */ |
|
1248 targetClassName: "variables-view-scope", |
|
1249 |
|
1250 /** |
|
1251 * Create a new Variable that is a child of this Scope. |
|
1252 * |
|
1253 * @param string aName |
|
1254 * The name of the new Property. |
|
1255 * @param object aDescriptor |
|
1256 * The variable's descriptor. |
|
1257 * @return Variable |
|
1258 * The newly created child Variable. |
|
1259 */ |
|
1260 _createChild: function(aName, aDescriptor) { |
|
1261 return new Variable(this, aName, aDescriptor); |
|
1262 }, |
|
1263 |
|
1264 /** |
|
1265 * Adds a child to contain any inspected properties. |
|
1266 * |
|
1267 * @param string aName |
|
1268 * The child's name. |
|
1269 * @param object aDescriptor |
|
1270 * Specifies the value and/or type & class of the child, |
|
1271 * or 'get' & 'set' accessor properties. If the type is implicit, |
|
1272 * it will be inferred from the value. If this parameter is omitted, |
|
1273 * a property without a value will be added (useful for branch nodes). |
|
1274 * e.g. - { value: 42 } |
|
1275 * - { value: true } |
|
1276 * - { value: "nasu" } |
|
1277 * - { value: { type: "undefined" } } |
|
1278 * - { value: { type: "null" } } |
|
1279 * - { value: { type: "object", class: "Object" } } |
|
1280 * - { get: { type: "object", class: "Function" }, |
|
1281 * set: { type: "undefined" } } |
|
1282 * @param boolean aRelaxed [optional] |
|
1283 * Pass true if name duplicates should be allowed. |
|
1284 * You probably shouldn't do it. Use this with caution. |
|
1285 * @return Variable |
|
1286 * The newly created Variable instance, null if it already exists. |
|
1287 */ |
|
1288 addItem: function(aName = "", aDescriptor = {}, aRelaxed = false) { |
|
1289 if (this._store.has(aName) && !aRelaxed) { |
|
1290 return null; |
|
1291 } |
|
1292 |
|
1293 let child = this._createChild(aName, aDescriptor); |
|
1294 this._store.set(aName, child); |
|
1295 this._variablesView._itemsByElement.set(child._target, child); |
|
1296 this._variablesView._currHierarchy.set(child._absoluteName, child); |
|
1297 child.header = !!aName; |
|
1298 |
|
1299 return child; |
|
1300 }, |
|
1301 |
|
1302 /** |
|
1303 * Adds items for this variable. |
|
1304 * |
|
1305 * @param object aItems |
|
1306 * An object containing some { name: descriptor } data properties, |
|
1307 * specifying the value and/or type & class of the variable, |
|
1308 * or 'get' & 'set' accessor properties. If the type is implicit, |
|
1309 * it will be inferred from the value. |
|
1310 * e.g. - { someProp0: { value: 42 }, |
|
1311 * someProp1: { value: true }, |
|
1312 * someProp2: { value: "nasu" }, |
|
1313 * someProp3: { value: { type: "undefined" } }, |
|
1314 * someProp4: { value: { type: "null" } }, |
|
1315 * someProp5: { value: { type: "object", class: "Object" } }, |
|
1316 * someProp6: { get: { type: "object", class: "Function" }, |
|
1317 * set: { type: "undefined" } } } |
|
1318 * @param object aOptions [optional] |
|
1319 * Additional options for adding the properties. Supported options: |
|
1320 * - sorted: true to sort all the properties before adding them |
|
1321 * - callback: function invoked after each item is added |
|
1322 * @param string aKeysType [optional] |
|
1323 * Helper argument in the case of paginated items. Can be either |
|
1324 * "just-strings" or "just-numbers". Humans shouldn't use this argument. |
|
1325 */ |
|
1326 addItems: function(aItems, aOptions = {}, aKeysType = "") { |
|
1327 let names = Object.keys(aItems); |
|
1328 |
|
1329 // Building the view when inspecting an object with a very large number of |
|
1330 // properties may take a long time. To avoid blocking the UI, group |
|
1331 // the items into several lazily populated pseudo-items. |
|
1332 let exceedsThreshold = names.length >= this.appendPageSize; |
|
1333 let shouldPaginate = exceedsThreshold && aKeysType != "just-strings"; |
|
1334 if (shouldPaginate && this.allowPaginate) { |
|
1335 // Group the items to append into two separate arrays, one containing |
|
1336 // number-like keys, the other one containing string keys. |
|
1337 if (aKeysType == "just-numbers") { |
|
1338 var numberKeys = names; |
|
1339 var stringKeys = []; |
|
1340 } else { |
|
1341 var numberKeys = []; |
|
1342 var stringKeys = []; |
|
1343 for (let name of names) { |
|
1344 // Be very careful. Avoid Infinity, NaN and non Natural number keys. |
|
1345 let coerced = +name; |
|
1346 if (Number.isInteger(coerced) && coerced > -1) { |
|
1347 numberKeys.push(name); |
|
1348 } else { |
|
1349 stringKeys.push(name); |
|
1350 } |
|
1351 } |
|
1352 } |
|
1353 |
|
1354 // This object contains a very large number of properties, but they're |
|
1355 // almost all strings that can't be coerced to numbers. Don't paginate. |
|
1356 if (numberKeys.length < this.appendPageSize) { |
|
1357 this.addItems(aItems, aOptions, "just-strings"); |
|
1358 return; |
|
1359 } |
|
1360 |
|
1361 // Slices a section of the { name: descriptor } data properties. |
|
1362 let paginate = (aArray, aBegin = 0, aEnd = aArray.length) => { |
|
1363 let store = {} |
|
1364 for (let i = aBegin; i < aEnd; i++) { |
|
1365 let name = aArray[i]; |
|
1366 store[name] = aItems[name]; |
|
1367 } |
|
1368 return store; |
|
1369 }; |
|
1370 |
|
1371 // Creates a pseudo-item that populates itself with the data properties |
|
1372 // from the corresponding page range. |
|
1373 let createRangeExpander = (aArray, aBegin, aEnd, aOptions, aKeyTypes) => { |
|
1374 let rangeVar = this.addItem(aArray[aBegin] + Scope.ellipsis + aArray[aEnd - 1]); |
|
1375 rangeVar.onexpand = () => { |
|
1376 let pageItems = paginate(aArray, aBegin, aEnd); |
|
1377 rangeVar.addItems(pageItems, aOptions, aKeyTypes); |
|
1378 } |
|
1379 rangeVar.showArrow(); |
|
1380 rangeVar.target.setAttribute("pseudo-item", ""); |
|
1381 }; |
|
1382 |
|
1383 // Divide the number keys into quarters. |
|
1384 let page = +Math.round(numberKeys.length / 4).toPrecision(1); |
|
1385 createRangeExpander(numberKeys, 0, page, aOptions, "just-numbers"); |
|
1386 createRangeExpander(numberKeys, page, page * 2, aOptions, "just-numbers"); |
|
1387 createRangeExpander(numberKeys, page * 2, page * 3, aOptions, "just-numbers"); |
|
1388 createRangeExpander(numberKeys, page * 3, numberKeys.length, aOptions, "just-numbers"); |
|
1389 |
|
1390 // Append all the string keys together. |
|
1391 this.addItems(paginate(stringKeys), aOptions, "just-strings"); |
|
1392 return; |
|
1393 } |
|
1394 |
|
1395 // Sort all of the properties before adding them, if preferred. |
|
1396 if (aOptions.sorted && aKeysType != "just-numbers") { |
|
1397 names.sort(); |
|
1398 } |
|
1399 |
|
1400 // Add the properties to the current scope. |
|
1401 for (let name of names) { |
|
1402 let descriptor = aItems[name]; |
|
1403 let item = this.addItem(name, descriptor); |
|
1404 |
|
1405 if (aOptions.callback) { |
|
1406 aOptions.callback(item, descriptor.value); |
|
1407 } |
|
1408 } |
|
1409 }, |
|
1410 |
|
1411 /** |
|
1412 * Remove this Scope from its parent and remove all children recursively. |
|
1413 */ |
|
1414 remove: function() { |
|
1415 let view = this._variablesView; |
|
1416 view._store.splice(view._store.indexOf(this), 1); |
|
1417 view._itemsByElement.delete(this._target); |
|
1418 view._currHierarchy.delete(this._nameString); |
|
1419 |
|
1420 this._target.remove(); |
|
1421 |
|
1422 for (let variable of this._store.values()) { |
|
1423 variable.remove(); |
|
1424 } |
|
1425 }, |
|
1426 |
|
1427 /** |
|
1428 * Gets the variable in this container having the specified name. |
|
1429 * |
|
1430 * @param string aName |
|
1431 * The name of the variable to get. |
|
1432 * @return Variable |
|
1433 * The matched variable, or null if nothing is found. |
|
1434 */ |
|
1435 get: function(aName) { |
|
1436 return this._store.get(aName); |
|
1437 }, |
|
1438 |
|
1439 /** |
|
1440 * Recursively searches for the variable or property in this container |
|
1441 * displayed by the specified node. |
|
1442 * |
|
1443 * @param nsIDOMNode aNode |
|
1444 * The node to search for. |
|
1445 * @return Variable | Property |
|
1446 * The matched variable or property, or null if nothing is found. |
|
1447 */ |
|
1448 find: function(aNode) { |
|
1449 for (let [, variable] of this._store) { |
|
1450 let match; |
|
1451 if (variable._target == aNode) { |
|
1452 match = variable; |
|
1453 } else { |
|
1454 match = variable.find(aNode); |
|
1455 } |
|
1456 if (match) { |
|
1457 return match; |
|
1458 } |
|
1459 } |
|
1460 return null; |
|
1461 }, |
|
1462 |
|
1463 /** |
|
1464 * Determines if this scope is a direct child of a parent variables view, |
|
1465 * scope, variable or property. |
|
1466 * |
|
1467 * @param VariablesView | Scope | Variable | Property |
|
1468 * The parent to check. |
|
1469 * @return boolean |
|
1470 * True if the specified item is a direct child, false otherwise. |
|
1471 */ |
|
1472 isChildOf: function(aParent) { |
|
1473 return this.ownerView == aParent; |
|
1474 }, |
|
1475 |
|
1476 /** |
|
1477 * Determines if this scope is a descendant of a parent variables view, |
|
1478 * scope, variable or property. |
|
1479 * |
|
1480 * @param VariablesView | Scope | Variable | Property |
|
1481 * The parent to check. |
|
1482 * @return boolean |
|
1483 * True if the specified item is a descendant, false otherwise. |
|
1484 */ |
|
1485 isDescendantOf: function(aParent) { |
|
1486 if (this.isChildOf(aParent)) { |
|
1487 return true; |
|
1488 } |
|
1489 |
|
1490 // Recurse to parent if it is a Scope, Variable, or Property. |
|
1491 if (this.ownerView instanceof Scope) { |
|
1492 return this.ownerView.isDescendantOf(aParent); |
|
1493 } |
|
1494 |
|
1495 return false; |
|
1496 }, |
|
1497 |
|
1498 /** |
|
1499 * Shows the scope. |
|
1500 */ |
|
1501 show: function() { |
|
1502 this._target.hidden = false; |
|
1503 this._isContentVisible = true; |
|
1504 |
|
1505 if (this.onshow) { |
|
1506 this.onshow(this); |
|
1507 } |
|
1508 }, |
|
1509 |
|
1510 /** |
|
1511 * Hides the scope. |
|
1512 */ |
|
1513 hide: function() { |
|
1514 this._target.hidden = true; |
|
1515 this._isContentVisible = false; |
|
1516 |
|
1517 if (this.onhide) { |
|
1518 this.onhide(this); |
|
1519 } |
|
1520 }, |
|
1521 |
|
1522 /** |
|
1523 * Expands the scope, showing all the added details. |
|
1524 */ |
|
1525 expand: function() { |
|
1526 if (this._isExpanded || this._isLocked) { |
|
1527 return; |
|
1528 } |
|
1529 if (this._variablesView._enumVisible) { |
|
1530 this._openEnum(); |
|
1531 } |
|
1532 if (this._variablesView._nonEnumVisible) { |
|
1533 Services.tm.currentThread.dispatch({ run: this._openNonEnum }, 0); |
|
1534 } |
|
1535 this._isExpanded = true; |
|
1536 |
|
1537 if (this.onexpand) { |
|
1538 this.onexpand(this); |
|
1539 } |
|
1540 }, |
|
1541 |
|
1542 /** |
|
1543 * Collapses the scope, hiding all the added details. |
|
1544 */ |
|
1545 collapse: function() { |
|
1546 if (!this._isExpanded || this._isLocked) { |
|
1547 return; |
|
1548 } |
|
1549 this._arrow.removeAttribute("open"); |
|
1550 this._enum.removeAttribute("open"); |
|
1551 this._nonenum.removeAttribute("open"); |
|
1552 this._isExpanded = false; |
|
1553 |
|
1554 if (this.oncollapse) { |
|
1555 this.oncollapse(this); |
|
1556 } |
|
1557 }, |
|
1558 |
|
1559 /** |
|
1560 * Toggles between the scope's collapsed and expanded state. |
|
1561 */ |
|
1562 toggle: function(e) { |
|
1563 if (e && e.button != 0) { |
|
1564 // Only allow left-click to trigger this event. |
|
1565 return; |
|
1566 } |
|
1567 this.expanded ^= 1; |
|
1568 |
|
1569 // Make sure the scope and its contents are visibile. |
|
1570 for (let [, variable] of this._store) { |
|
1571 variable.header = true; |
|
1572 variable._matched = true; |
|
1573 } |
|
1574 if (this.ontoggle) { |
|
1575 this.ontoggle(this); |
|
1576 } |
|
1577 }, |
|
1578 |
|
1579 /** |
|
1580 * Shows the scope's title header. |
|
1581 */ |
|
1582 showHeader: function() { |
|
1583 if (this._isHeaderVisible || !this._nameString) { |
|
1584 return; |
|
1585 } |
|
1586 this._target.removeAttribute("untitled"); |
|
1587 this._isHeaderVisible = true; |
|
1588 }, |
|
1589 |
|
1590 /** |
|
1591 * Hides the scope's title header. |
|
1592 * This action will automatically expand the scope. |
|
1593 */ |
|
1594 hideHeader: function() { |
|
1595 if (!this._isHeaderVisible) { |
|
1596 return; |
|
1597 } |
|
1598 this.expand(); |
|
1599 this._target.setAttribute("untitled", ""); |
|
1600 this._isHeaderVisible = false; |
|
1601 }, |
|
1602 |
|
1603 /** |
|
1604 * Shows the scope's expand/collapse arrow. |
|
1605 */ |
|
1606 showArrow: function() { |
|
1607 if (this._isArrowVisible) { |
|
1608 return; |
|
1609 } |
|
1610 this._arrow.removeAttribute("invisible"); |
|
1611 this._isArrowVisible = true; |
|
1612 }, |
|
1613 |
|
1614 /** |
|
1615 * Hides the scope's expand/collapse arrow. |
|
1616 */ |
|
1617 hideArrow: function() { |
|
1618 if (!this._isArrowVisible) { |
|
1619 return; |
|
1620 } |
|
1621 this._arrow.setAttribute("invisible", ""); |
|
1622 this._isArrowVisible = false; |
|
1623 }, |
|
1624 |
|
1625 /** |
|
1626 * Gets the visibility state. |
|
1627 * @return boolean |
|
1628 */ |
|
1629 get visible() this._isContentVisible, |
|
1630 |
|
1631 /** |
|
1632 * Gets the expanded state. |
|
1633 * @return boolean |
|
1634 */ |
|
1635 get expanded() this._isExpanded, |
|
1636 |
|
1637 /** |
|
1638 * Gets the header visibility state. |
|
1639 * @return boolean |
|
1640 */ |
|
1641 get header() this._isHeaderVisible, |
|
1642 |
|
1643 /** |
|
1644 * Gets the twisty visibility state. |
|
1645 * @return boolean |
|
1646 */ |
|
1647 get twisty() this._isArrowVisible, |
|
1648 |
|
1649 /** |
|
1650 * Gets the expand lock state. |
|
1651 * @return boolean |
|
1652 */ |
|
1653 get locked() this._isLocked, |
|
1654 |
|
1655 /** |
|
1656 * Sets the visibility state. |
|
1657 * @param boolean aFlag |
|
1658 */ |
|
1659 set visible(aFlag) aFlag ? this.show() : this.hide(), |
|
1660 |
|
1661 /** |
|
1662 * Sets the expanded state. |
|
1663 * @param boolean aFlag |
|
1664 */ |
|
1665 set expanded(aFlag) aFlag ? this.expand() : this.collapse(), |
|
1666 |
|
1667 /** |
|
1668 * Sets the header visibility state. |
|
1669 * @param boolean aFlag |
|
1670 */ |
|
1671 set header(aFlag) aFlag ? this.showHeader() : this.hideHeader(), |
|
1672 |
|
1673 /** |
|
1674 * Sets the twisty visibility state. |
|
1675 * @param boolean aFlag |
|
1676 */ |
|
1677 set twisty(aFlag) aFlag ? this.showArrow() : this.hideArrow(), |
|
1678 |
|
1679 /** |
|
1680 * Sets the expand lock state. |
|
1681 * @param boolean aFlag |
|
1682 */ |
|
1683 set locked(aFlag) this._isLocked = aFlag, |
|
1684 |
|
1685 /** |
|
1686 * Specifies if this target node may be focused. |
|
1687 * @return boolean |
|
1688 */ |
|
1689 get focusable() { |
|
1690 // Check if this target node is actually visibile. |
|
1691 if (!this._nameString || |
|
1692 !this._isContentVisible || |
|
1693 !this._isHeaderVisible || |
|
1694 !this._isMatch) { |
|
1695 return false; |
|
1696 } |
|
1697 // Check if all parent objects are expanded. |
|
1698 let item = this; |
|
1699 |
|
1700 // Recurse while parent is a Scope, Variable, or Property |
|
1701 while ((item = item.ownerView) && item instanceof Scope) { |
|
1702 if (!item._isExpanded) { |
|
1703 return false; |
|
1704 } |
|
1705 } |
|
1706 return true; |
|
1707 }, |
|
1708 |
|
1709 /** |
|
1710 * Focus this scope. |
|
1711 */ |
|
1712 focus: function() { |
|
1713 this._variablesView._focusItem(this); |
|
1714 }, |
|
1715 |
|
1716 /** |
|
1717 * Adds an event listener for a certain event on this scope's title. |
|
1718 * @param string aName |
|
1719 * @param function aCallback |
|
1720 * @param boolean aCapture |
|
1721 */ |
|
1722 addEventListener: function(aName, aCallback, aCapture) { |
|
1723 this._title.addEventListener(aName, aCallback, aCapture); |
|
1724 }, |
|
1725 |
|
1726 /** |
|
1727 * Removes an event listener for a certain event on this scope's title. |
|
1728 * @param string aName |
|
1729 * @param function aCallback |
|
1730 * @param boolean aCapture |
|
1731 */ |
|
1732 removeEventListener: function(aName, aCallback, aCapture) { |
|
1733 this._title.removeEventListener(aName, aCallback, aCapture); |
|
1734 }, |
|
1735 |
|
1736 /** |
|
1737 * Gets the id associated with this item. |
|
1738 * @return string |
|
1739 */ |
|
1740 get id() this._idString, |
|
1741 |
|
1742 /** |
|
1743 * Gets the name associated with this item. |
|
1744 * @return string |
|
1745 */ |
|
1746 get name() this._nameString, |
|
1747 |
|
1748 /** |
|
1749 * Gets the displayed value for this item. |
|
1750 * @return string |
|
1751 */ |
|
1752 get displayValue() this._valueString, |
|
1753 |
|
1754 /** |
|
1755 * Gets the class names used for the displayed value. |
|
1756 * @return string |
|
1757 */ |
|
1758 get displayValueClassName() this._valueClassName, |
|
1759 |
|
1760 /** |
|
1761 * Gets the element associated with this item. |
|
1762 * @return nsIDOMNode |
|
1763 */ |
|
1764 get target() this._target, |
|
1765 |
|
1766 /** |
|
1767 * Initializes this scope's id, view and binds event listeners. |
|
1768 * |
|
1769 * @param string aName |
|
1770 * The scope's name. |
|
1771 * @param object aFlags [optional] |
|
1772 * Additional options or flags for this scope. |
|
1773 */ |
|
1774 _init: function(aName, aFlags) { |
|
1775 this._idString = generateId(this._nameString = aName); |
|
1776 this._displayScope(aName, this.targetClassName, "devtools-toolbar"); |
|
1777 this._addEventListeners(); |
|
1778 this.parentNode.appendChild(this._target); |
|
1779 }, |
|
1780 |
|
1781 /** |
|
1782 * Creates the necessary nodes for this scope. |
|
1783 * |
|
1784 * @param string aName |
|
1785 * The scope's name. |
|
1786 * @param string aTargetClassName |
|
1787 * A custom class name for this scope's target element. |
|
1788 * @param string aTitleClassName [optional] |
|
1789 * A custom class name for this scope's title element. |
|
1790 */ |
|
1791 _displayScope: function(aName, aTargetClassName, aTitleClassName = "") { |
|
1792 let document = this.document; |
|
1793 |
|
1794 let element = this._target = document.createElement("vbox"); |
|
1795 element.id = this._idString; |
|
1796 element.className = aTargetClassName; |
|
1797 |
|
1798 let arrow = this._arrow = document.createElement("hbox"); |
|
1799 arrow.className = "arrow"; |
|
1800 |
|
1801 let name = this._name = document.createElement("label"); |
|
1802 name.className = "plain name"; |
|
1803 name.setAttribute("value", aName); |
|
1804 |
|
1805 let title = this._title = document.createElement("hbox"); |
|
1806 title.className = "title " + aTitleClassName; |
|
1807 title.setAttribute("align", "center"); |
|
1808 |
|
1809 let enumerable = this._enum = document.createElement("vbox"); |
|
1810 let nonenum = this._nonenum = document.createElement("vbox"); |
|
1811 enumerable.className = "variables-view-element-details enum"; |
|
1812 nonenum.className = "variables-view-element-details nonenum"; |
|
1813 |
|
1814 title.appendChild(arrow); |
|
1815 title.appendChild(name); |
|
1816 |
|
1817 element.appendChild(title); |
|
1818 element.appendChild(enumerable); |
|
1819 element.appendChild(nonenum); |
|
1820 }, |
|
1821 |
|
1822 /** |
|
1823 * Adds the necessary event listeners for this scope. |
|
1824 */ |
|
1825 _addEventListeners: function() { |
|
1826 this._title.addEventListener("mousedown", this._onClick, false); |
|
1827 }, |
|
1828 |
|
1829 /** |
|
1830 * The click listener for this scope's title. |
|
1831 */ |
|
1832 _onClick: function(e) { |
|
1833 if (this.editing || |
|
1834 e.button != 0 || |
|
1835 e.target == this._editNode || |
|
1836 e.target == this._deleteNode || |
|
1837 e.target == this._addPropertyNode) { |
|
1838 return; |
|
1839 } |
|
1840 this.toggle(); |
|
1841 this.focus(); |
|
1842 }, |
|
1843 |
|
1844 /** |
|
1845 * Opens the enumerable items container. |
|
1846 */ |
|
1847 _openEnum: function() { |
|
1848 this._arrow.setAttribute("open", ""); |
|
1849 this._enum.setAttribute("open", ""); |
|
1850 }, |
|
1851 |
|
1852 /** |
|
1853 * Opens the non-enumerable items container. |
|
1854 */ |
|
1855 _openNonEnum: function() { |
|
1856 this._nonenum.setAttribute("open", ""); |
|
1857 }, |
|
1858 |
|
1859 /** |
|
1860 * Specifies if enumerable properties and variables should be displayed. |
|
1861 * @param boolean aFlag |
|
1862 */ |
|
1863 set _enumVisible(aFlag) { |
|
1864 for (let [, variable] of this._store) { |
|
1865 variable._enumVisible = aFlag; |
|
1866 |
|
1867 if (!this._isExpanded) { |
|
1868 continue; |
|
1869 } |
|
1870 if (aFlag) { |
|
1871 this._enum.setAttribute("open", ""); |
|
1872 } else { |
|
1873 this._enum.removeAttribute("open"); |
|
1874 } |
|
1875 } |
|
1876 }, |
|
1877 |
|
1878 /** |
|
1879 * Specifies if non-enumerable properties and variables should be displayed. |
|
1880 * @param boolean aFlag |
|
1881 */ |
|
1882 set _nonEnumVisible(aFlag) { |
|
1883 for (let [, variable] of this._store) { |
|
1884 variable._nonEnumVisible = aFlag; |
|
1885 |
|
1886 if (!this._isExpanded) { |
|
1887 continue; |
|
1888 } |
|
1889 if (aFlag) { |
|
1890 this._nonenum.setAttribute("open", ""); |
|
1891 } else { |
|
1892 this._nonenum.removeAttribute("open"); |
|
1893 } |
|
1894 } |
|
1895 }, |
|
1896 |
|
1897 /** |
|
1898 * Performs a case insensitive search for variables or properties matching |
|
1899 * the query, and hides non-matched items. |
|
1900 * |
|
1901 * @param string aLowerCaseQuery |
|
1902 * The lowercased name of the variable or property to search for. |
|
1903 */ |
|
1904 _performSearch: function(aLowerCaseQuery) { |
|
1905 for (let [, variable] of this._store) { |
|
1906 let currentObject = variable; |
|
1907 let lowerCaseName = variable._nameString.toLowerCase(); |
|
1908 let lowerCaseValue = variable._valueString.toLowerCase(); |
|
1909 |
|
1910 // Non-matched variables or properties require a corresponding attribute. |
|
1911 if (!lowerCaseName.contains(aLowerCaseQuery) && |
|
1912 !lowerCaseValue.contains(aLowerCaseQuery)) { |
|
1913 variable._matched = false; |
|
1914 } |
|
1915 // Variable or property is matched. |
|
1916 else { |
|
1917 variable._matched = true; |
|
1918 |
|
1919 // If the variable was ever expanded, there's a possibility it may |
|
1920 // contain some matched properties, so make sure they're visible |
|
1921 // ("expand downwards"). |
|
1922 if (variable._store.size) { |
|
1923 variable.expand(); |
|
1924 } |
|
1925 |
|
1926 // If the variable is contained in another Scope, Variable, or Property, |
|
1927 // the parent may not be a match, thus hidden. It should be visible |
|
1928 // ("expand upwards"). |
|
1929 while ((variable = variable.ownerView) && variable instanceof Scope) { |
|
1930 variable._matched = true; |
|
1931 variable.expand(); |
|
1932 } |
|
1933 } |
|
1934 |
|
1935 // Proceed with the search recursively inside this variable or property. |
|
1936 if (currentObject._store.size || currentObject.getter || currentObject.setter) { |
|
1937 currentObject._performSearch(aLowerCaseQuery); |
|
1938 } |
|
1939 } |
|
1940 }, |
|
1941 |
|
1942 /** |
|
1943 * Sets if this object instance is a matched or non-matched item. |
|
1944 * @param boolean aStatus |
|
1945 */ |
|
1946 set _matched(aStatus) { |
|
1947 if (this._isMatch == aStatus) { |
|
1948 return; |
|
1949 } |
|
1950 if (aStatus) { |
|
1951 this._isMatch = true; |
|
1952 this.target.removeAttribute("unmatched"); |
|
1953 } else { |
|
1954 this._isMatch = false; |
|
1955 this.target.setAttribute("unmatched", ""); |
|
1956 } |
|
1957 }, |
|
1958 |
|
1959 /** |
|
1960 * Find the first item in the tree of visible items in this item that matches |
|
1961 * the predicate. Searches in visual order (the order seen by the user). |
|
1962 * Tests itself, then descends into first the enumerable children and then |
|
1963 * the non-enumerable children (since they are presented in separate groups). |
|
1964 * |
|
1965 * @param function aPredicate |
|
1966 * A function that returns true when a match is found. |
|
1967 * @return Scope | Variable | Property |
|
1968 * The first visible scope, variable or property, or null if nothing |
|
1969 * is found. |
|
1970 */ |
|
1971 _findInVisibleItems: function(aPredicate) { |
|
1972 if (aPredicate(this)) { |
|
1973 return this; |
|
1974 } |
|
1975 |
|
1976 if (this._isExpanded) { |
|
1977 if (this._variablesView._enumVisible) { |
|
1978 for (let item of this._enumItems) { |
|
1979 let result = item._findInVisibleItems(aPredicate); |
|
1980 if (result) { |
|
1981 return result; |
|
1982 } |
|
1983 } |
|
1984 } |
|
1985 |
|
1986 if (this._variablesView._nonEnumVisible) { |
|
1987 for (let item of this._nonEnumItems) { |
|
1988 let result = item._findInVisibleItems(aPredicate); |
|
1989 if (result) { |
|
1990 return result; |
|
1991 } |
|
1992 } |
|
1993 } |
|
1994 } |
|
1995 |
|
1996 return null; |
|
1997 }, |
|
1998 |
|
1999 /** |
|
2000 * Find the last item in the tree of visible items in this item that matches |
|
2001 * the predicate. Searches in reverse visual order (opposite of the order |
|
2002 * seen by the user). Descends into first the non-enumerable children, then |
|
2003 * the enumerable children (since they are presented in separate groups), and |
|
2004 * finally tests itself. |
|
2005 * |
|
2006 * @param function aPredicate |
|
2007 * A function that returns true when a match is found. |
|
2008 * @return Scope | Variable | Property |
|
2009 * The last visible scope, variable or property, or null if nothing |
|
2010 * is found. |
|
2011 */ |
|
2012 _findInVisibleItemsReverse: function(aPredicate) { |
|
2013 if (this._isExpanded) { |
|
2014 if (this._variablesView._nonEnumVisible) { |
|
2015 for (let i = this._nonEnumItems.length - 1; i >= 0; i--) { |
|
2016 let item = this._nonEnumItems[i]; |
|
2017 let result = item._findInVisibleItemsReverse(aPredicate); |
|
2018 if (result) { |
|
2019 return result; |
|
2020 } |
|
2021 } |
|
2022 } |
|
2023 |
|
2024 if (this._variablesView._enumVisible) { |
|
2025 for (let i = this._enumItems.length - 1; i >= 0; i--) { |
|
2026 let item = this._enumItems[i]; |
|
2027 let result = item._findInVisibleItemsReverse(aPredicate); |
|
2028 if (result) { |
|
2029 return result; |
|
2030 } |
|
2031 } |
|
2032 } |
|
2033 } |
|
2034 |
|
2035 if (aPredicate(this)) { |
|
2036 return this; |
|
2037 } |
|
2038 |
|
2039 return null; |
|
2040 }, |
|
2041 |
|
2042 /** |
|
2043 * Gets top level variables view instance. |
|
2044 * @return VariablesView |
|
2045 */ |
|
2046 get _variablesView() this._topView || (this._topView = (function(self) { |
|
2047 let parentView = self.ownerView; |
|
2048 let topView; |
|
2049 |
|
2050 while (topView = parentView.ownerView) { |
|
2051 parentView = topView; |
|
2052 } |
|
2053 return parentView; |
|
2054 })(this)), |
|
2055 |
|
2056 /** |
|
2057 * Gets the parent node holding this scope. |
|
2058 * @return nsIDOMNode |
|
2059 */ |
|
2060 get parentNode() this.ownerView._list, |
|
2061 |
|
2062 /** |
|
2063 * Gets the owner document holding this scope. |
|
2064 * @return nsIHTMLDocument |
|
2065 */ |
|
2066 get document() this._document || (this._document = this.ownerView.document), |
|
2067 |
|
2068 /** |
|
2069 * Gets the default window holding this scope. |
|
2070 * @return nsIDOMWindow |
|
2071 */ |
|
2072 get window() this._window || (this._window = this.ownerView.window), |
|
2073 |
|
2074 _topView: null, |
|
2075 _document: null, |
|
2076 _window: null, |
|
2077 |
|
2078 ownerView: null, |
|
2079 eval: null, |
|
2080 switch: null, |
|
2081 delete: null, |
|
2082 new: null, |
|
2083 preventDisableOnChange: false, |
|
2084 preventDescriptorModifiers: false, |
|
2085 editing: false, |
|
2086 editableNameTooltip: "", |
|
2087 editableValueTooltip: "", |
|
2088 editButtonTooltip: "", |
|
2089 deleteButtonTooltip: "", |
|
2090 domNodeValueTooltip: "", |
|
2091 contextMenuId: "", |
|
2092 separatorStr: "", |
|
2093 |
|
2094 _store: null, |
|
2095 _enumItems: null, |
|
2096 _nonEnumItems: null, |
|
2097 _fetched: false, |
|
2098 _committed: false, |
|
2099 _isLocked: false, |
|
2100 _isExpanded: false, |
|
2101 _isContentVisible: true, |
|
2102 _isHeaderVisible: true, |
|
2103 _isArrowVisible: true, |
|
2104 _isMatch: true, |
|
2105 _idString: "", |
|
2106 _nameString: "", |
|
2107 _target: null, |
|
2108 _arrow: null, |
|
2109 _name: null, |
|
2110 _title: null, |
|
2111 _enum: null, |
|
2112 _nonenum: null, |
|
2113 }; |
|
2114 |
|
2115 // Creating maps and arrays thousands of times for variables or properties |
|
2116 // with a large number of children fills up a lot of memory. Make sure |
|
2117 // these are instantiated only if needed. |
|
2118 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_store", Map); |
|
2119 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_enumItems", Array); |
|
2120 DevToolsUtils.defineLazyPrototypeGetter(Scope.prototype, "_nonEnumItems", Array); |
|
2121 |
|
2122 // An ellipsis symbol (usually "…") used for localization. |
|
2123 XPCOMUtils.defineLazyGetter(Scope, "ellipsis", () => |
|
2124 Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data); |
|
2125 |
|
2126 /** |
|
2127 * A Variable is a Scope holding Property instances. |
|
2128 * Iterable via "for (let [name, property] of instance) { }". |
|
2129 * |
|
2130 * @param Scope aScope |
|
2131 * The scope to contain this variable. |
|
2132 * @param string aName |
|
2133 * The variable's name. |
|
2134 * @param object aDescriptor |
|
2135 * The variable's descriptor. |
|
2136 */ |
|
2137 function Variable(aScope, aName, aDescriptor) { |
|
2138 this._setTooltips = this._setTooltips.bind(this); |
|
2139 this._activateNameInput = this._activateNameInput.bind(this); |
|
2140 this._activateValueInput = this._activateValueInput.bind(this); |
|
2141 this.openNodeInInspector = this.openNodeInInspector.bind(this); |
|
2142 this.highlightDomNode = this.highlightDomNode.bind(this); |
|
2143 this.unhighlightDomNode = this.unhighlightDomNode.bind(this); |
|
2144 |
|
2145 // Treat safe getter descriptors as descriptors with a value. |
|
2146 if ("getterValue" in aDescriptor) { |
|
2147 aDescriptor.value = aDescriptor.getterValue; |
|
2148 delete aDescriptor.get; |
|
2149 delete aDescriptor.set; |
|
2150 } |
|
2151 |
|
2152 Scope.call(this, aScope, aName, this._initialDescriptor = aDescriptor); |
|
2153 this.setGrip(aDescriptor.value); |
|
2154 this._symbolicName = aName; |
|
2155 this._absoluteName = aScope.name + "[\"" + aName + "\"]"; |
|
2156 } |
|
2157 |
|
2158 Variable.prototype = Heritage.extend(Scope.prototype, { |
|
2159 /** |
|
2160 * Whether this Variable should be prefetched when it is remoted. |
|
2161 */ |
|
2162 get shouldPrefetch() { |
|
2163 return this.name == "window" || this.name == "this"; |
|
2164 }, |
|
2165 |
|
2166 /** |
|
2167 * Whether this Variable should paginate its contents. |
|
2168 */ |
|
2169 get allowPaginate() { |
|
2170 return this.name != "window" && this.name != "this"; |
|
2171 }, |
|
2172 |
|
2173 /** |
|
2174 * The class name applied to this variable's target element. |
|
2175 */ |
|
2176 targetClassName: "variables-view-variable variable-or-property", |
|
2177 |
|
2178 /** |
|
2179 * Create a new Property that is a child of Variable. |
|
2180 * |
|
2181 * @param string aName |
|
2182 * The name of the new Property. |
|
2183 * @param object aDescriptor |
|
2184 * The property's descriptor. |
|
2185 * @return Property |
|
2186 * The newly created child Property. |
|
2187 */ |
|
2188 _createChild: function(aName, aDescriptor) { |
|
2189 return new Property(this, aName, aDescriptor); |
|
2190 }, |
|
2191 |
|
2192 /** |
|
2193 * Remove this Variable from its parent and remove all children recursively. |
|
2194 */ |
|
2195 remove: function() { |
|
2196 if (this._linkedToInspector) { |
|
2197 this.unhighlightDomNode(); |
|
2198 this._valueLabel.removeEventListener("mouseover", this.highlightDomNode, false); |
|
2199 this._valueLabel.removeEventListener("mouseout", this.unhighlightDomNode, false); |
|
2200 this._openInspectorNode.removeEventListener("mousedown", this.openNodeInInspector, false); |
|
2201 } |
|
2202 |
|
2203 this.ownerView._store.delete(this._nameString); |
|
2204 this._variablesView._itemsByElement.delete(this._target); |
|
2205 this._variablesView._currHierarchy.delete(this._absoluteName); |
|
2206 |
|
2207 this._target.remove(); |
|
2208 |
|
2209 for (let property of this._store.values()) { |
|
2210 property.remove(); |
|
2211 } |
|
2212 }, |
|
2213 |
|
2214 /** |
|
2215 * Populates this variable to contain all the properties of an object. |
|
2216 * |
|
2217 * @param object aObject |
|
2218 * The raw object you want to display. |
|
2219 * @param object aOptions [optional] |
|
2220 * Additional options for adding the properties. Supported options: |
|
2221 * - sorted: true to sort all the properties before adding them |
|
2222 * - expanded: true to expand all the properties after adding them |
|
2223 */ |
|
2224 populate: function(aObject, aOptions = {}) { |
|
2225 // Retrieve the properties only once. |
|
2226 if (this._fetched) { |
|
2227 return; |
|
2228 } |
|
2229 this._fetched = true; |
|
2230 |
|
2231 let propertyNames = Object.getOwnPropertyNames(aObject); |
|
2232 let prototype = Object.getPrototypeOf(aObject); |
|
2233 |
|
2234 // Sort all of the properties before adding them, if preferred. |
|
2235 if (aOptions.sorted) { |
|
2236 propertyNames.sort(); |
|
2237 } |
|
2238 // Add all the variable properties. |
|
2239 for (let name of propertyNames) { |
|
2240 let descriptor = Object.getOwnPropertyDescriptor(aObject, name); |
|
2241 if (descriptor.get || descriptor.set) { |
|
2242 let prop = this._addRawNonValueProperty(name, descriptor); |
|
2243 if (aOptions.expanded) { |
|
2244 prop.expanded = true; |
|
2245 } |
|
2246 } else { |
|
2247 let prop = this._addRawValueProperty(name, descriptor, aObject[name]); |
|
2248 if (aOptions.expanded) { |
|
2249 prop.expanded = true; |
|
2250 } |
|
2251 } |
|
2252 } |
|
2253 // Add the variable's __proto__. |
|
2254 if (prototype) { |
|
2255 this._addRawValueProperty("__proto__", {}, prototype); |
|
2256 } |
|
2257 }, |
|
2258 |
|
2259 /** |
|
2260 * Populates a specific variable or property instance to contain all the |
|
2261 * properties of an object |
|
2262 * |
|
2263 * @param Variable | Property aVar |
|
2264 * The target variable to populate. |
|
2265 * @param object aObject [optional] |
|
2266 * The raw object you want to display. If unspecified, the object is |
|
2267 * assumed to be defined in a _sourceValue property on the target. |
|
2268 */ |
|
2269 _populateTarget: function(aVar, aObject = aVar._sourceValue) { |
|
2270 aVar.populate(aObject); |
|
2271 }, |
|
2272 |
|
2273 /** |
|
2274 * Adds a property for this variable based on a raw value descriptor. |
|
2275 * |
|
2276 * @param string aName |
|
2277 * The property's name. |
|
2278 * @param object aDescriptor |
|
2279 * Specifies the exact property descriptor as returned by a call to |
|
2280 * Object.getOwnPropertyDescriptor. |
|
2281 * @param object aValue |
|
2282 * The raw property value you want to display. |
|
2283 * @return Property |
|
2284 * The newly added property instance. |
|
2285 */ |
|
2286 _addRawValueProperty: function(aName, aDescriptor, aValue) { |
|
2287 let descriptor = Object.create(aDescriptor); |
|
2288 descriptor.value = VariablesView.getGrip(aValue); |
|
2289 |
|
2290 let propertyItem = this.addItem(aName, descriptor); |
|
2291 propertyItem._sourceValue = aValue; |
|
2292 |
|
2293 // Add an 'onexpand' callback for the property, lazily handling |
|
2294 // the addition of new child properties. |
|
2295 if (!VariablesView.isPrimitive(descriptor)) { |
|
2296 propertyItem.onexpand = this._populateTarget; |
|
2297 } |
|
2298 return propertyItem; |
|
2299 }, |
|
2300 |
|
2301 /** |
|
2302 * Adds a property for this variable based on a getter/setter descriptor. |
|
2303 * |
|
2304 * @param string aName |
|
2305 * The property's name. |
|
2306 * @param object aDescriptor |
|
2307 * Specifies the exact property descriptor as returned by a call to |
|
2308 * Object.getOwnPropertyDescriptor. |
|
2309 * @return Property |
|
2310 * The newly added property instance. |
|
2311 */ |
|
2312 _addRawNonValueProperty: function(aName, aDescriptor) { |
|
2313 let descriptor = Object.create(aDescriptor); |
|
2314 descriptor.get = VariablesView.getGrip(aDescriptor.get); |
|
2315 descriptor.set = VariablesView.getGrip(aDescriptor.set); |
|
2316 |
|
2317 return this.addItem(aName, descriptor); |
|
2318 }, |
|
2319 |
|
2320 /** |
|
2321 * Gets this variable's path to the topmost scope in the form of a string |
|
2322 * meant for use via eval() or a similar approach. |
|
2323 * For example, a symbolic name may look like "arguments['0']['foo']['bar']". |
|
2324 * @return string |
|
2325 */ |
|
2326 get symbolicName() this._symbolicName, |
|
2327 |
|
2328 /** |
|
2329 * Gets this variable's symbolic path to the topmost scope. |
|
2330 * @return array |
|
2331 * @see Variable._buildSymbolicPath |
|
2332 */ |
|
2333 get symbolicPath() { |
|
2334 if (this._symbolicPath) { |
|
2335 return this._symbolicPath; |
|
2336 } |
|
2337 this._symbolicPath = this._buildSymbolicPath(); |
|
2338 return this._symbolicPath; |
|
2339 }, |
|
2340 |
|
2341 /** |
|
2342 * Build this variable's path to the topmost scope in form of an array of |
|
2343 * strings, one for each segment of the path. |
|
2344 * For example, a symbolic path may look like ["0", "foo", "bar"]. |
|
2345 * @return array |
|
2346 */ |
|
2347 _buildSymbolicPath: function(path = []) { |
|
2348 if (this.name) { |
|
2349 path.unshift(this.name); |
|
2350 if (this.ownerView instanceof Variable) { |
|
2351 return this.ownerView._buildSymbolicPath(path); |
|
2352 } |
|
2353 } |
|
2354 return path; |
|
2355 }, |
|
2356 |
|
2357 /** |
|
2358 * Returns this variable's value from the descriptor if available. |
|
2359 * @return any |
|
2360 */ |
|
2361 get value() this._initialDescriptor.value, |
|
2362 |
|
2363 /** |
|
2364 * Returns this variable's getter from the descriptor if available. |
|
2365 * @return object |
|
2366 */ |
|
2367 get getter() this._initialDescriptor.get, |
|
2368 |
|
2369 /** |
|
2370 * Returns this variable's getter from the descriptor if available. |
|
2371 * @return object |
|
2372 */ |
|
2373 get setter() this._initialDescriptor.set, |
|
2374 |
|
2375 /** |
|
2376 * Sets the specific grip for this variable (applies the text content and |
|
2377 * class name to the value label). |
|
2378 * |
|
2379 * The grip should contain the value or the type & class, as defined in the |
|
2380 * remote debugger protocol. For convenience, undefined and null are |
|
2381 * both considered types. |
|
2382 * |
|
2383 * @param any aGrip |
|
2384 * Specifies the value and/or type & class of the variable. |
|
2385 * e.g. - 42 |
|
2386 * - true |
|
2387 * - "nasu" |
|
2388 * - { type: "undefined" } |
|
2389 * - { type: "null" } |
|
2390 * - { type: "object", class: "Object" } |
|
2391 */ |
|
2392 setGrip: function(aGrip) { |
|
2393 // Don't allow displaying grip information if there's no name available |
|
2394 // or the grip is malformed. |
|
2395 if (!this._nameString || aGrip === undefined || aGrip === null) { |
|
2396 return; |
|
2397 } |
|
2398 // Getters and setters should display grip information in sub-properties. |
|
2399 if (this.getter || this.setter) { |
|
2400 return; |
|
2401 } |
|
2402 |
|
2403 let prevGrip = this._valueGrip; |
|
2404 if (prevGrip) { |
|
2405 this._valueLabel.classList.remove(VariablesView.getClass(prevGrip)); |
|
2406 } |
|
2407 this._valueGrip = aGrip; |
|
2408 this._valueString = VariablesView.getString(aGrip, { |
|
2409 concise: true, |
|
2410 noEllipsis: true, |
|
2411 }); |
|
2412 this._valueClassName = VariablesView.getClass(aGrip); |
|
2413 |
|
2414 this._valueLabel.classList.add(this._valueClassName); |
|
2415 this._valueLabel.setAttribute("value", this._valueString); |
|
2416 this._separatorLabel.hidden = false; |
|
2417 |
|
2418 // DOMNodes get special treatment since they can be linked to the inspector |
|
2419 if (this._valueGrip.preview && this._valueGrip.preview.kind === "DOMNode") { |
|
2420 this._linkToInspector(); |
|
2421 } |
|
2422 }, |
|
2423 |
|
2424 /** |
|
2425 * Marks this variable as overridden. |
|
2426 * |
|
2427 * @param boolean aFlag |
|
2428 * Whether this variable is overridden or not. |
|
2429 */ |
|
2430 setOverridden: function(aFlag) { |
|
2431 if (aFlag) { |
|
2432 this._target.setAttribute("overridden", ""); |
|
2433 } else { |
|
2434 this._target.removeAttribute("overridden"); |
|
2435 } |
|
2436 }, |
|
2437 |
|
2438 /** |
|
2439 * Briefly flashes this variable. |
|
2440 * |
|
2441 * @param number aDuration [optional] |
|
2442 * An optional flash animation duration. |
|
2443 */ |
|
2444 flash: function(aDuration = ITEM_FLASH_DURATION) { |
|
2445 let fadeInDelay = this._variablesView.lazyEmptyDelay + 1; |
|
2446 let fadeOutDelay = fadeInDelay + aDuration; |
|
2447 |
|
2448 setNamedTimeout("vview-flash-in" + this._absoluteName, |
|
2449 fadeInDelay, () => this._target.setAttribute("changed", "")); |
|
2450 |
|
2451 setNamedTimeout("vview-flash-out" + this._absoluteName, |
|
2452 fadeOutDelay, () => this._target.removeAttribute("changed")); |
|
2453 }, |
|
2454 |
|
2455 /** |
|
2456 * Initializes this variable's id, view and binds event listeners. |
|
2457 * |
|
2458 * @param string aName |
|
2459 * The variable's name. |
|
2460 * @param object aDescriptor |
|
2461 * The variable's descriptor. |
|
2462 */ |
|
2463 _init: function(aName, aDescriptor) { |
|
2464 this._idString = generateId(this._nameString = aName); |
|
2465 this._displayScope(aName, this.targetClassName); |
|
2466 this._displayVariable(); |
|
2467 this._customizeVariable(); |
|
2468 this._prepareTooltips(); |
|
2469 this._setAttributes(); |
|
2470 this._addEventListeners(); |
|
2471 |
|
2472 if (this._initialDescriptor.enumerable || |
|
2473 this._nameString == "this" || |
|
2474 this._nameString == "<return>" || |
|
2475 this._nameString == "<exception>") { |
|
2476 this.ownerView._enum.appendChild(this._target); |
|
2477 this.ownerView._enumItems.push(this); |
|
2478 } else { |
|
2479 this.ownerView._nonenum.appendChild(this._target); |
|
2480 this.ownerView._nonEnumItems.push(this); |
|
2481 } |
|
2482 }, |
|
2483 |
|
2484 /** |
|
2485 * Creates the necessary nodes for this variable. |
|
2486 */ |
|
2487 _displayVariable: function() { |
|
2488 let document = this.document; |
|
2489 let descriptor = this._initialDescriptor; |
|
2490 |
|
2491 let separatorLabel = this._separatorLabel = document.createElement("label"); |
|
2492 separatorLabel.className = "plain separator"; |
|
2493 separatorLabel.setAttribute("value", this.separatorStr + " "); |
|
2494 |
|
2495 let valueLabel = this._valueLabel = document.createElement("label"); |
|
2496 valueLabel.className = "plain value"; |
|
2497 valueLabel.setAttribute("flex", "1"); |
|
2498 valueLabel.setAttribute("crop", "center"); |
|
2499 |
|
2500 this._title.appendChild(separatorLabel); |
|
2501 this._title.appendChild(valueLabel); |
|
2502 |
|
2503 if (VariablesView.isPrimitive(descriptor)) { |
|
2504 this.hideArrow(); |
|
2505 } |
|
2506 |
|
2507 // If no value will be displayed, we don't need the separator. |
|
2508 if (!descriptor.get && !descriptor.set && !("value" in descriptor)) { |
|
2509 separatorLabel.hidden = true; |
|
2510 } |
|
2511 |
|
2512 // If this is a getter/setter property, create two child pseudo-properties |
|
2513 // called "get" and "set" that display the corresponding functions. |
|
2514 if (descriptor.get || descriptor.set) { |
|
2515 separatorLabel.hidden = true; |
|
2516 valueLabel.hidden = true; |
|
2517 |
|
2518 // Changing getter/setter names is never allowed. |
|
2519 this.switch = null; |
|
2520 |
|
2521 // Getter/setter properties require special handling when it comes to |
|
2522 // evaluation and deletion. |
|
2523 if (this.ownerView.eval) { |
|
2524 this.delete = VariablesView.getterOrSetterDeleteCallback; |
|
2525 this.evaluationMacro = VariablesView.overrideValueEvalMacro; |
|
2526 } |
|
2527 // Deleting getters and setters individually is not allowed if no |
|
2528 // evaluation method is provided. |
|
2529 else { |
|
2530 this.delete = null; |
|
2531 this.evaluationMacro = null; |
|
2532 } |
|
2533 |
|
2534 let getter = this.addItem("get", { value: descriptor.get }); |
|
2535 let setter = this.addItem("set", { value: descriptor.set }); |
|
2536 getter.evaluationMacro = VariablesView.getterOrSetterEvalMacro; |
|
2537 setter.evaluationMacro = VariablesView.getterOrSetterEvalMacro; |
|
2538 |
|
2539 getter.hideArrow(); |
|
2540 setter.hideArrow(); |
|
2541 this.expand(); |
|
2542 } |
|
2543 }, |
|
2544 |
|
2545 /** |
|
2546 * Adds specific nodes for this variable based on custom flags. |
|
2547 */ |
|
2548 _customizeVariable: function() { |
|
2549 let ownerView = this.ownerView; |
|
2550 let descriptor = this._initialDescriptor; |
|
2551 |
|
2552 if (ownerView.eval && this.getter || this.setter) { |
|
2553 let editNode = this._editNode = this.document.createElement("toolbarbutton"); |
|
2554 editNode.className = "plain variables-view-edit"; |
|
2555 editNode.addEventListener("mousedown", this._onEdit.bind(this), false); |
|
2556 this._title.insertBefore(editNode, this._spacer); |
|
2557 } |
|
2558 |
|
2559 if (ownerView.delete) { |
|
2560 let deleteNode = this._deleteNode = this.document.createElement("toolbarbutton"); |
|
2561 deleteNode.className = "plain variables-view-delete"; |
|
2562 deleteNode.addEventListener("click", this._onDelete.bind(this), false); |
|
2563 this._title.appendChild(deleteNode); |
|
2564 } |
|
2565 |
|
2566 if (ownerView.new) { |
|
2567 let addPropertyNode = this._addPropertyNode = this.document.createElement("toolbarbutton"); |
|
2568 addPropertyNode.className = "plain variables-view-add-property"; |
|
2569 addPropertyNode.addEventListener("mousedown", this._onAddProperty.bind(this), false); |
|
2570 this._title.appendChild(addPropertyNode); |
|
2571 |
|
2572 // Can't add properties to primitive values, hide the node in those cases. |
|
2573 if (VariablesView.isPrimitive(descriptor)) { |
|
2574 addPropertyNode.setAttribute("invisible", ""); |
|
2575 } |
|
2576 } |
|
2577 |
|
2578 if (ownerView.contextMenuId) { |
|
2579 this._title.setAttribute("context", ownerView.contextMenuId); |
|
2580 } |
|
2581 |
|
2582 if (ownerView.preventDescriptorModifiers) { |
|
2583 return; |
|
2584 } |
|
2585 |
|
2586 if (!descriptor.writable && !ownerView.getter && !ownerView.setter) { |
|
2587 let nonWritableIcon = this.document.createElement("hbox"); |
|
2588 nonWritableIcon.className = "plain variable-or-property-non-writable-icon"; |
|
2589 nonWritableIcon.setAttribute("optional-visibility", ""); |
|
2590 this._title.appendChild(nonWritableIcon); |
|
2591 } |
|
2592 if (descriptor.value && typeof descriptor.value == "object") { |
|
2593 if (descriptor.value.frozen) { |
|
2594 let frozenLabel = this.document.createElement("label"); |
|
2595 frozenLabel.className = "plain variable-or-property-frozen-label"; |
|
2596 frozenLabel.setAttribute("optional-visibility", ""); |
|
2597 frozenLabel.setAttribute("value", "F"); |
|
2598 this._title.appendChild(frozenLabel); |
|
2599 } |
|
2600 if (descriptor.value.sealed) { |
|
2601 let sealedLabel = this.document.createElement("label"); |
|
2602 sealedLabel.className = "plain variable-or-property-sealed-label"; |
|
2603 sealedLabel.setAttribute("optional-visibility", ""); |
|
2604 sealedLabel.setAttribute("value", "S"); |
|
2605 this._title.appendChild(sealedLabel); |
|
2606 } |
|
2607 if (!descriptor.value.extensible) { |
|
2608 let nonExtensibleLabel = this.document.createElement("label"); |
|
2609 nonExtensibleLabel.className = "plain variable-or-property-non-extensible-label"; |
|
2610 nonExtensibleLabel.setAttribute("optional-visibility", ""); |
|
2611 nonExtensibleLabel.setAttribute("value", "N"); |
|
2612 this._title.appendChild(nonExtensibleLabel); |
|
2613 } |
|
2614 } |
|
2615 }, |
|
2616 |
|
2617 /** |
|
2618 * Prepares all tooltips for this variable. |
|
2619 */ |
|
2620 _prepareTooltips: function() { |
|
2621 this._target.addEventListener("mouseover", this._setTooltips, false); |
|
2622 }, |
|
2623 |
|
2624 /** |
|
2625 * Sets all tooltips for this variable. |
|
2626 */ |
|
2627 _setTooltips: function() { |
|
2628 this._target.removeEventListener("mouseover", this._setTooltips, false); |
|
2629 |
|
2630 let ownerView = this.ownerView; |
|
2631 if (ownerView.preventDescriptorModifiers) { |
|
2632 return; |
|
2633 } |
|
2634 |
|
2635 let tooltip = this.document.createElement("tooltip"); |
|
2636 tooltip.id = "tooltip-" + this._idString; |
|
2637 tooltip.setAttribute("orient", "horizontal"); |
|
2638 |
|
2639 let labels = [ |
|
2640 "configurable", "enumerable", "writable", |
|
2641 "frozen", "sealed", "extensible", "overridden", "WebIDL"]; |
|
2642 |
|
2643 for (let type of labels) { |
|
2644 let labelElement = this.document.createElement("label"); |
|
2645 labelElement.className = type; |
|
2646 labelElement.setAttribute("value", STR.GetStringFromName(type + "Tooltip")); |
|
2647 tooltip.appendChild(labelElement); |
|
2648 } |
|
2649 |
|
2650 this._target.appendChild(tooltip); |
|
2651 this._target.setAttribute("tooltip", tooltip.id); |
|
2652 |
|
2653 if (this._editNode && ownerView.eval) { |
|
2654 this._editNode.setAttribute("tooltiptext", ownerView.editButtonTooltip); |
|
2655 } |
|
2656 if (this._openInspectorNode && this._linkedToInspector) { |
|
2657 this._openInspectorNode.setAttribute("tooltiptext", this.ownerView.domNodeValueTooltip); |
|
2658 } |
|
2659 if (this._valueLabel && ownerView.eval) { |
|
2660 this._valueLabel.setAttribute("tooltiptext", ownerView.editableValueTooltip); |
|
2661 } |
|
2662 if (this._name && ownerView.switch) { |
|
2663 this._name.setAttribute("tooltiptext", ownerView.editableNameTooltip); |
|
2664 } |
|
2665 if (this._deleteNode && ownerView.delete) { |
|
2666 this._deleteNode.setAttribute("tooltiptext", ownerView.deleteButtonTooltip); |
|
2667 } |
|
2668 }, |
|
2669 |
|
2670 /** |
|
2671 * Get the parent variablesview toolbox, if any. |
|
2672 */ |
|
2673 get toolbox() { |
|
2674 return this._variablesView.toolbox; |
|
2675 }, |
|
2676 |
|
2677 /** |
|
2678 * Checks if this variable is a DOMNode and is part of a variablesview that |
|
2679 * has been linked to the toolbox, so that highlighting and jumping to the |
|
2680 * inspector can be done. |
|
2681 */ |
|
2682 _isLinkableToInspector: function() { |
|
2683 let isDomNode = this._valueGrip && this._valueGrip.preview.kind === "DOMNode"; |
|
2684 let hasBeenLinked = this._linkedToInspector; |
|
2685 let hasToolbox = !!this.toolbox; |
|
2686 |
|
2687 return isDomNode && !hasBeenLinked && hasToolbox; |
|
2688 }, |
|
2689 |
|
2690 /** |
|
2691 * If the variable is a DOMNode, and if a toolbox is set, then link it to the |
|
2692 * inspector (highlight on hover, and jump to markup-view on click) |
|
2693 */ |
|
2694 _linkToInspector: function() { |
|
2695 if (!this._isLinkableToInspector()) { |
|
2696 return; |
|
2697 } |
|
2698 |
|
2699 // Listen to value mouseover/click events to highlight and jump |
|
2700 this._valueLabel.addEventListener("mouseover", this.highlightDomNode, false); |
|
2701 this._valueLabel.addEventListener("mouseout", this.unhighlightDomNode, false); |
|
2702 |
|
2703 // Add a button to open the node in the inspector |
|
2704 this._openInspectorNode = this.document.createElement("toolbarbutton"); |
|
2705 this._openInspectorNode.className = "plain variables-view-open-inspector"; |
|
2706 this._openInspectorNode.addEventListener("mousedown", this.openNodeInInspector, false); |
|
2707 this._title.insertBefore(this._openInspectorNode, this._title.querySelector("toolbarbutton")); |
|
2708 |
|
2709 this._linkedToInspector = true; |
|
2710 }, |
|
2711 |
|
2712 /** |
|
2713 * In case this variable is a DOMNode and part of a variablesview that has been |
|
2714 * linked to the toolbox's inspector, then select the corresponding node in |
|
2715 * the inspector, and switch the inspector tool in the toolbox |
|
2716 * @return a promise that resolves when the node is selected and the inspector |
|
2717 * has been switched to and is ready |
|
2718 */ |
|
2719 openNodeInInspector: function(event) { |
|
2720 if (!this.toolbox) { |
|
2721 return promise.reject(new Error("Toolbox not available")); |
|
2722 } |
|
2723 |
|
2724 event && event.stopPropagation(); |
|
2725 |
|
2726 return Task.spawn(function*() { |
|
2727 yield this.toolbox.initInspector(); |
|
2728 |
|
2729 let nodeFront = this._nodeFront; |
|
2730 if (!nodeFront) { |
|
2731 nodeFront = yield this.toolbox.walker.getNodeActorFromObjectActor(this._valueGrip.actor); |
|
2732 } |
|
2733 |
|
2734 if (nodeFront) { |
|
2735 yield this.toolbox.selectTool("inspector"); |
|
2736 |
|
2737 let inspectorReady = promise.defer(); |
|
2738 this.toolbox.getPanel("inspector").once("inspector-updated", inspectorReady.resolve); |
|
2739 yield this.toolbox.selection.setNodeFront(nodeFront, "variables-view"); |
|
2740 yield inspectorReady.promise; |
|
2741 } |
|
2742 }.bind(this)); |
|
2743 }, |
|
2744 |
|
2745 /** |
|
2746 * In case this variable is a DOMNode and part of a variablesview that has been |
|
2747 * linked to the toolbox's inspector, then highlight the corresponding node |
|
2748 */ |
|
2749 highlightDomNode: function() { |
|
2750 if (this.toolbox) { |
|
2751 if (this._nodeFront) { |
|
2752 // If the nodeFront has been retrieved before, no need to ask the server |
|
2753 // again for it |
|
2754 this.toolbox.highlighterUtils.highlightNodeFront(this._nodeFront); |
|
2755 return; |
|
2756 } |
|
2757 |
|
2758 this.toolbox.highlighterUtils.highlightDomValueGrip(this._valueGrip).then(front => { |
|
2759 this._nodeFront = front; |
|
2760 }); |
|
2761 } |
|
2762 }, |
|
2763 |
|
2764 /** |
|
2765 * Unhighlight a previously highlit node |
|
2766 * @see highlightDomNode |
|
2767 */ |
|
2768 unhighlightDomNode: function() { |
|
2769 if (this.toolbox) { |
|
2770 this.toolbox.highlighterUtils.unhighlight(); |
|
2771 } |
|
2772 }, |
|
2773 |
|
2774 /** |
|
2775 * Sets a variable's configurable, enumerable and writable attributes, |
|
2776 * and specifies if it's a 'this', '<exception>', '<return>' or '__proto__' |
|
2777 * reference. |
|
2778 */ |
|
2779 _setAttributes: function() { |
|
2780 let ownerView = this.ownerView; |
|
2781 if (ownerView.preventDescriptorModifiers) { |
|
2782 return; |
|
2783 } |
|
2784 |
|
2785 let descriptor = this._initialDescriptor; |
|
2786 let target = this._target; |
|
2787 let name = this._nameString; |
|
2788 |
|
2789 if (ownerView.eval) { |
|
2790 target.setAttribute("editable", ""); |
|
2791 } |
|
2792 |
|
2793 if (!descriptor.configurable) { |
|
2794 target.setAttribute("non-configurable", ""); |
|
2795 } |
|
2796 if (!descriptor.enumerable) { |
|
2797 target.setAttribute("non-enumerable", ""); |
|
2798 } |
|
2799 if (!descriptor.writable && !ownerView.getter && !ownerView.setter) { |
|
2800 target.setAttribute("non-writable", ""); |
|
2801 } |
|
2802 |
|
2803 if (descriptor.value && typeof descriptor.value == "object") { |
|
2804 if (descriptor.value.frozen) { |
|
2805 target.setAttribute("frozen", ""); |
|
2806 } |
|
2807 if (descriptor.value.sealed) { |
|
2808 target.setAttribute("sealed", ""); |
|
2809 } |
|
2810 if (!descriptor.value.extensible) { |
|
2811 target.setAttribute("non-extensible", ""); |
|
2812 } |
|
2813 } |
|
2814 |
|
2815 if (descriptor && "getterValue" in descriptor) { |
|
2816 target.setAttribute("safe-getter", ""); |
|
2817 } |
|
2818 |
|
2819 if (name == "this") { |
|
2820 target.setAttribute("self", ""); |
|
2821 } |
|
2822 else if (name == "<exception>") { |
|
2823 target.setAttribute("exception", ""); |
|
2824 target.setAttribute("pseudo-item", ""); |
|
2825 } |
|
2826 else if (name == "<return>") { |
|
2827 target.setAttribute("return", ""); |
|
2828 target.setAttribute("pseudo-item", ""); |
|
2829 } |
|
2830 else if (name == "__proto__") { |
|
2831 target.setAttribute("proto", ""); |
|
2832 target.setAttribute("pseudo-item", ""); |
|
2833 } |
|
2834 |
|
2835 if (Object.keys(descriptor).length == 0) { |
|
2836 target.setAttribute("pseudo-item", ""); |
|
2837 } |
|
2838 }, |
|
2839 |
|
2840 /** |
|
2841 * Adds the necessary event listeners for this variable. |
|
2842 */ |
|
2843 _addEventListeners: function() { |
|
2844 this._name.addEventListener("dblclick", this._activateNameInput, false); |
|
2845 this._valueLabel.addEventListener("mousedown", this._activateValueInput, false); |
|
2846 this._title.addEventListener("mousedown", this._onClick, false); |
|
2847 }, |
|
2848 |
|
2849 /** |
|
2850 * Makes this variable's name editable. |
|
2851 */ |
|
2852 _activateNameInput: function(e) { |
|
2853 if (!this._variablesView.alignedValues) { |
|
2854 this._separatorLabel.hidden = true; |
|
2855 this._valueLabel.hidden = true; |
|
2856 } |
|
2857 |
|
2858 EditableName.create(this, { |
|
2859 onSave: aKey => { |
|
2860 if (!this._variablesView.preventDisableOnChange) { |
|
2861 this._disable(); |
|
2862 } |
|
2863 this.ownerView.switch(this, aKey); |
|
2864 }, |
|
2865 onCleanup: () => { |
|
2866 if (!this._variablesView.alignedValues) { |
|
2867 this._separatorLabel.hidden = false; |
|
2868 this._valueLabel.hidden = false; |
|
2869 } |
|
2870 } |
|
2871 }, e); |
|
2872 }, |
|
2873 |
|
2874 /** |
|
2875 * Makes this variable's value editable. |
|
2876 */ |
|
2877 _activateValueInput: function(e) { |
|
2878 EditableValue.create(this, { |
|
2879 onSave: aString => { |
|
2880 if (this._linkedToInspector) { |
|
2881 this.unhighlightDomNode(); |
|
2882 } |
|
2883 if (!this._variablesView.preventDisableOnChange) { |
|
2884 this._disable(); |
|
2885 } |
|
2886 this.ownerView.eval(this, aString); |
|
2887 } |
|
2888 }, e); |
|
2889 }, |
|
2890 |
|
2891 /** |
|
2892 * Disables this variable prior to a new name switch or value evaluation. |
|
2893 */ |
|
2894 _disable: function() { |
|
2895 // Prevent the variable from being collapsed or expanded. |
|
2896 this.hideArrow(); |
|
2897 |
|
2898 // Hide any nodes that may offer information about the variable. |
|
2899 for (let node of this._title.childNodes) { |
|
2900 node.hidden = node != this._arrow && node != this._name; |
|
2901 } |
|
2902 this._enum.hidden = true; |
|
2903 this._nonenum.hidden = true; |
|
2904 }, |
|
2905 |
|
2906 /** |
|
2907 * The current macro used to generate the string evaluated when performing |
|
2908 * a variable or property value change. |
|
2909 */ |
|
2910 evaluationMacro: VariablesView.simpleValueEvalMacro, |
|
2911 |
|
2912 /** |
|
2913 * The click listener for the edit button. |
|
2914 */ |
|
2915 _onEdit: function(e) { |
|
2916 if (e.button != 0) { |
|
2917 return; |
|
2918 } |
|
2919 |
|
2920 e.preventDefault(); |
|
2921 e.stopPropagation(); |
|
2922 this._activateValueInput(); |
|
2923 }, |
|
2924 |
|
2925 /** |
|
2926 * The click listener for the delete button. |
|
2927 */ |
|
2928 _onDelete: function(e) { |
|
2929 if ("button" in e && e.button != 0) { |
|
2930 return; |
|
2931 } |
|
2932 |
|
2933 e.preventDefault(); |
|
2934 e.stopPropagation(); |
|
2935 |
|
2936 if (this.ownerView.delete) { |
|
2937 if (!this.ownerView.delete(this)) { |
|
2938 this.hide(); |
|
2939 } |
|
2940 } |
|
2941 }, |
|
2942 |
|
2943 /** |
|
2944 * The click listener for the add property button. |
|
2945 */ |
|
2946 _onAddProperty: function(e) { |
|
2947 if ("button" in e && e.button != 0) { |
|
2948 return; |
|
2949 } |
|
2950 |
|
2951 e.preventDefault(); |
|
2952 e.stopPropagation(); |
|
2953 |
|
2954 this.expanded = true; |
|
2955 |
|
2956 let item = this.addItem(" ", { |
|
2957 value: undefined, |
|
2958 configurable: true, |
|
2959 enumerable: true, |
|
2960 writable: true |
|
2961 }, true); |
|
2962 |
|
2963 // Force showing the separator. |
|
2964 item._separatorLabel.hidden = false; |
|
2965 |
|
2966 EditableNameAndValue.create(item, { |
|
2967 onSave: ([aKey, aValue]) => { |
|
2968 if (!this._variablesView.preventDisableOnChange) { |
|
2969 this._disable(); |
|
2970 } |
|
2971 this.ownerView.new(this, aKey, aValue); |
|
2972 } |
|
2973 }, e); |
|
2974 }, |
|
2975 |
|
2976 _symbolicName: "", |
|
2977 _symbolicPath: null, |
|
2978 _absoluteName: "", |
|
2979 _initialDescriptor: null, |
|
2980 _separatorLabel: null, |
|
2981 _valueLabel: null, |
|
2982 _spacer: null, |
|
2983 _editNode: null, |
|
2984 _deleteNode: null, |
|
2985 _addPropertyNode: null, |
|
2986 _tooltip: null, |
|
2987 _valueGrip: null, |
|
2988 _valueString: "", |
|
2989 _valueClassName: "", |
|
2990 _prevExpandable: false, |
|
2991 _prevExpanded: false |
|
2992 }); |
|
2993 |
|
2994 /** |
|
2995 * A Property is a Variable holding additional child Property instances. |
|
2996 * Iterable via "for (let [name, property] of instance) { }". |
|
2997 * |
|
2998 * @param Variable aVar |
|
2999 * The variable to contain this property. |
|
3000 * @param string aName |
|
3001 * The property's name. |
|
3002 * @param object aDescriptor |
|
3003 * The property's descriptor. |
|
3004 */ |
|
3005 function Property(aVar, aName, aDescriptor) { |
|
3006 Variable.call(this, aVar, aName, aDescriptor); |
|
3007 this._symbolicName = aVar._symbolicName + "[\"" + aName + "\"]"; |
|
3008 this._absoluteName = aVar._absoluteName + "[\"" + aName + "\"]"; |
|
3009 } |
|
3010 |
|
3011 Property.prototype = Heritage.extend(Variable.prototype, { |
|
3012 /** |
|
3013 * The class name applied to this property's target element. |
|
3014 */ |
|
3015 targetClassName: "variables-view-property variable-or-property" |
|
3016 }); |
|
3017 |
|
3018 /** |
|
3019 * A generator-iterator over the VariablesView, Scopes, Variables and Properties. |
|
3020 */ |
|
3021 VariablesView.prototype["@@iterator"] = |
|
3022 Scope.prototype["@@iterator"] = |
|
3023 Variable.prototype["@@iterator"] = |
|
3024 Property.prototype["@@iterator"] = function*() { |
|
3025 yield* this._store; |
|
3026 }; |
|
3027 |
|
3028 /** |
|
3029 * Forget everything recorded about added scopes, variables or properties. |
|
3030 * @see VariablesView.commitHierarchy |
|
3031 */ |
|
3032 VariablesView.prototype.clearHierarchy = function() { |
|
3033 this._prevHierarchy.clear(); |
|
3034 this._currHierarchy.clear(); |
|
3035 }; |
|
3036 |
|
3037 /** |
|
3038 * Perform operations on all the VariablesView Scopes, Variables and Properties |
|
3039 * after you've added all the items you wanted. |
|
3040 * |
|
3041 * Calling this method is optional, and does the following: |
|
3042 * - styles the items overridden by other items in parent scopes |
|
3043 * - reopens the items which were previously expanded |
|
3044 * - flashes the items whose values changed |
|
3045 */ |
|
3046 VariablesView.prototype.commitHierarchy = function() { |
|
3047 for (let [, currItem] of this._currHierarchy) { |
|
3048 // Avoid performing expensive operations. |
|
3049 if (this.commitHierarchyIgnoredItems[currItem._nameString]) { |
|
3050 continue; |
|
3051 } |
|
3052 let overridden = this.isOverridden(currItem); |
|
3053 if (overridden) { |
|
3054 currItem.setOverridden(true); |
|
3055 } |
|
3056 let expanded = !currItem._committed && this.wasExpanded(currItem); |
|
3057 if (expanded) { |
|
3058 currItem.expand(); |
|
3059 } |
|
3060 let changed = !currItem._committed && this.hasChanged(currItem); |
|
3061 if (changed) { |
|
3062 currItem.flash(); |
|
3063 } |
|
3064 currItem._committed = true; |
|
3065 } |
|
3066 if (this.oncommit) { |
|
3067 this.oncommit(this); |
|
3068 } |
|
3069 }; |
|
3070 |
|
3071 // Some variables are likely to contain a very large number of properties. |
|
3072 // It would be a bad idea to re-expand them or perform expensive operations. |
|
3073 VariablesView.prototype.commitHierarchyIgnoredItems = Heritage.extend(null, { |
|
3074 "window": true, |
|
3075 "this": true |
|
3076 }); |
|
3077 |
|
3078 /** |
|
3079 * Checks if the an item was previously expanded, if it existed in a |
|
3080 * previous hierarchy. |
|
3081 * |
|
3082 * @param Scope | Variable | Property aItem |
|
3083 * The item to verify. |
|
3084 * @return boolean |
|
3085 * Whether the item was expanded. |
|
3086 */ |
|
3087 VariablesView.prototype.wasExpanded = function(aItem) { |
|
3088 if (!(aItem instanceof Scope)) { |
|
3089 return false; |
|
3090 } |
|
3091 let prevItem = this._prevHierarchy.get(aItem._absoluteName || aItem._nameString); |
|
3092 return prevItem ? prevItem._isExpanded : false; |
|
3093 }; |
|
3094 |
|
3095 /** |
|
3096 * Checks if the an item's displayed value (a representation of the grip) |
|
3097 * has changed, if it existed in a previous hierarchy. |
|
3098 * |
|
3099 * @param Variable | Property aItem |
|
3100 * The item to verify. |
|
3101 * @return boolean |
|
3102 * Whether the item has changed. |
|
3103 */ |
|
3104 VariablesView.prototype.hasChanged = function(aItem) { |
|
3105 // Only analyze Variables and Properties for displayed value changes. |
|
3106 // Scopes are just collections of Variables and Properties and |
|
3107 // don't have a "value", so they can't change. |
|
3108 if (!(aItem instanceof Variable)) { |
|
3109 return false; |
|
3110 } |
|
3111 let prevItem = this._prevHierarchy.get(aItem._absoluteName); |
|
3112 return prevItem ? prevItem._valueString != aItem._valueString : false; |
|
3113 }; |
|
3114 |
|
3115 /** |
|
3116 * Checks if the an item was previously expanded, if it existed in a |
|
3117 * previous hierarchy. |
|
3118 * |
|
3119 * @param Scope | Variable | Property aItem |
|
3120 * The item to verify. |
|
3121 * @return boolean |
|
3122 * Whether the item was expanded. |
|
3123 */ |
|
3124 VariablesView.prototype.isOverridden = function(aItem) { |
|
3125 // Only analyze Variables for being overridden in different Scopes. |
|
3126 if (!(aItem instanceof Variable) || aItem instanceof Property) { |
|
3127 return false; |
|
3128 } |
|
3129 let currVariableName = aItem._nameString; |
|
3130 let parentScopes = this.getParentScopesForVariableOrProperty(aItem); |
|
3131 |
|
3132 for (let otherScope of parentScopes) { |
|
3133 for (let [otherVariableName] of otherScope) { |
|
3134 if (otherVariableName == currVariableName) { |
|
3135 return true; |
|
3136 } |
|
3137 } |
|
3138 } |
|
3139 return false; |
|
3140 }; |
|
3141 |
|
3142 /** |
|
3143 * Returns true if the descriptor represents an undefined, null or |
|
3144 * primitive value. |
|
3145 * |
|
3146 * @param object aDescriptor |
|
3147 * The variable's descriptor. |
|
3148 */ |
|
3149 VariablesView.isPrimitive = function(aDescriptor) { |
|
3150 // For accessor property descriptors, the getter and setter need to be |
|
3151 // contained in 'get' and 'set' properties. |
|
3152 let getter = aDescriptor.get; |
|
3153 let setter = aDescriptor.set; |
|
3154 if (getter || setter) { |
|
3155 return false; |
|
3156 } |
|
3157 |
|
3158 // As described in the remote debugger protocol, the value grip |
|
3159 // must be contained in a 'value' property. |
|
3160 let grip = aDescriptor.value; |
|
3161 if (typeof grip != "object") { |
|
3162 return true; |
|
3163 } |
|
3164 |
|
3165 // For convenience, undefined, null, Infinity, -Infinity, NaN, -0, and long |
|
3166 // strings are considered types. |
|
3167 let type = grip.type; |
|
3168 if (type == "undefined" || |
|
3169 type == "null" || |
|
3170 type == "Infinity" || |
|
3171 type == "-Infinity" || |
|
3172 type == "NaN" || |
|
3173 type == "-0" || |
|
3174 type == "longString") { |
|
3175 return true; |
|
3176 } |
|
3177 |
|
3178 return false; |
|
3179 }; |
|
3180 |
|
3181 /** |
|
3182 * Returns true if the descriptor represents an undefined value. |
|
3183 * |
|
3184 * @param object aDescriptor |
|
3185 * The variable's descriptor. |
|
3186 */ |
|
3187 VariablesView.isUndefined = function(aDescriptor) { |
|
3188 // For accessor property descriptors, the getter and setter need to be |
|
3189 // contained in 'get' and 'set' properties. |
|
3190 let getter = aDescriptor.get; |
|
3191 let setter = aDescriptor.set; |
|
3192 if (typeof getter == "object" && getter.type == "undefined" && |
|
3193 typeof setter == "object" && setter.type == "undefined") { |
|
3194 return true; |
|
3195 } |
|
3196 |
|
3197 // As described in the remote debugger protocol, the value grip |
|
3198 // must be contained in a 'value' property. |
|
3199 let grip = aDescriptor.value; |
|
3200 if (typeof grip == "object" && grip.type == "undefined") { |
|
3201 return true; |
|
3202 } |
|
3203 |
|
3204 return false; |
|
3205 }; |
|
3206 |
|
3207 /** |
|
3208 * Returns true if the descriptor represents a falsy value. |
|
3209 * |
|
3210 * @param object aDescriptor |
|
3211 * The variable's descriptor. |
|
3212 */ |
|
3213 VariablesView.isFalsy = function(aDescriptor) { |
|
3214 // As described in the remote debugger protocol, the value grip |
|
3215 // must be contained in a 'value' property. |
|
3216 let grip = aDescriptor.value; |
|
3217 if (typeof grip != "object") { |
|
3218 return !grip; |
|
3219 } |
|
3220 |
|
3221 // For convenience, undefined, null, NaN, and -0 are all considered types. |
|
3222 let type = grip.type; |
|
3223 if (type == "undefined" || |
|
3224 type == "null" || |
|
3225 type == "NaN" || |
|
3226 type == "-0") { |
|
3227 return true; |
|
3228 } |
|
3229 |
|
3230 return false; |
|
3231 }; |
|
3232 |
|
3233 /** |
|
3234 * Returns true if the value is an instance of Variable or Property. |
|
3235 * |
|
3236 * @param any aValue |
|
3237 * The value to test. |
|
3238 */ |
|
3239 VariablesView.isVariable = function(aValue) { |
|
3240 return aValue instanceof Variable; |
|
3241 }; |
|
3242 |
|
3243 /** |
|
3244 * Returns a standard grip for a value. |
|
3245 * |
|
3246 * @param any aValue |
|
3247 * The raw value to get a grip for. |
|
3248 * @return any |
|
3249 * The value's grip. |
|
3250 */ |
|
3251 VariablesView.getGrip = function(aValue) { |
|
3252 switch (typeof aValue) { |
|
3253 case "boolean": |
|
3254 case "string": |
|
3255 return aValue; |
|
3256 case "number": |
|
3257 if (aValue === Infinity) { |
|
3258 return { type: "Infinity" }; |
|
3259 } else if (aValue === -Infinity) { |
|
3260 return { type: "-Infinity" }; |
|
3261 } else if (Number.isNaN(aValue)) { |
|
3262 return { type: "NaN" }; |
|
3263 } else if (1 / aValue === -Infinity) { |
|
3264 return { type: "-0" }; |
|
3265 } |
|
3266 return aValue; |
|
3267 case "undefined": |
|
3268 // document.all is also "undefined" |
|
3269 if (aValue === undefined) { |
|
3270 return { type: "undefined" }; |
|
3271 } |
|
3272 case "object": |
|
3273 if (aValue === null) { |
|
3274 return { type: "null" }; |
|
3275 } |
|
3276 case "function": |
|
3277 return { type: "object", |
|
3278 class: WebConsoleUtils.getObjectClassName(aValue) }; |
|
3279 default: |
|
3280 Cu.reportError("Failed to provide a grip for value of " + typeof value + |
|
3281 ": " + aValue); |
|
3282 return null; |
|
3283 } |
|
3284 }; |
|
3285 |
|
3286 /** |
|
3287 * Returns a custom formatted property string for a grip. |
|
3288 * |
|
3289 * @param any aGrip |
|
3290 * @see Variable.setGrip |
|
3291 * @param object aOptions |
|
3292 * Options: |
|
3293 * - concise: boolean that tells you want a concisely formatted string. |
|
3294 * - noStringQuotes: boolean that tells to not quote strings. |
|
3295 * - noEllipsis: boolean that tells to not add an ellipsis after the |
|
3296 * initial text of a longString. |
|
3297 * @return string |
|
3298 * The formatted property string. |
|
3299 */ |
|
3300 VariablesView.getString = function(aGrip, aOptions = {}) { |
|
3301 if (aGrip && typeof aGrip == "object") { |
|
3302 switch (aGrip.type) { |
|
3303 case "undefined": |
|
3304 case "null": |
|
3305 case "NaN": |
|
3306 case "Infinity": |
|
3307 case "-Infinity": |
|
3308 case "-0": |
|
3309 return aGrip.type; |
|
3310 default: |
|
3311 let stringifier = VariablesView.stringifiers.byType[aGrip.type]; |
|
3312 if (stringifier) { |
|
3313 let result = stringifier(aGrip, aOptions); |
|
3314 if (result != null) { |
|
3315 return result; |
|
3316 } |
|
3317 } |
|
3318 |
|
3319 if (aGrip.displayString) { |
|
3320 return VariablesView.getString(aGrip.displayString, aOptions); |
|
3321 } |
|
3322 |
|
3323 if (aGrip.type == "object" && aOptions.concise) { |
|
3324 return aGrip.class; |
|
3325 } |
|
3326 |
|
3327 return "[" + aGrip.type + " " + aGrip.class + "]"; |
|
3328 } |
|
3329 } |
|
3330 |
|
3331 switch (typeof aGrip) { |
|
3332 case "string": |
|
3333 return VariablesView.stringifiers.byType.string(aGrip, aOptions); |
|
3334 case "boolean": |
|
3335 return aGrip ? "true" : "false"; |
|
3336 case "number": |
|
3337 if (!aGrip && 1 / aGrip === -Infinity) { |
|
3338 return "-0"; |
|
3339 } |
|
3340 default: |
|
3341 return aGrip + ""; |
|
3342 } |
|
3343 }; |
|
3344 |
|
3345 /** |
|
3346 * The VariablesView stringifiers are used by VariablesView.getString(). These |
|
3347 * are organized by object type, object class and by object actor preview kind. |
|
3348 * Some objects share identical ways for previews, for example Arrays, Sets and |
|
3349 * NodeLists. |
|
3350 * |
|
3351 * Any stringifier function must return a string. If null is returned, * then |
|
3352 * the default stringifier will be used. When invoked, the stringifier is |
|
3353 * given the same two arguments as those given to VariablesView.getString(). |
|
3354 */ |
|
3355 VariablesView.stringifiers = {}; |
|
3356 |
|
3357 VariablesView.stringifiers.byType = { |
|
3358 string: function(aGrip, {noStringQuotes}) { |
|
3359 if (noStringQuotes) { |
|
3360 return aGrip; |
|
3361 } |
|
3362 return '"' + aGrip + '"'; |
|
3363 }, |
|
3364 |
|
3365 longString: function({initial}, {noStringQuotes, noEllipsis}) { |
|
3366 let ellipsis = noEllipsis ? "" : Scope.ellipsis; |
|
3367 if (noStringQuotes) { |
|
3368 return initial + ellipsis; |
|
3369 } |
|
3370 let result = '"' + initial + '"'; |
|
3371 if (!ellipsis) { |
|
3372 return result; |
|
3373 } |
|
3374 return result.substr(0, result.length - 1) + ellipsis + '"'; |
|
3375 }, |
|
3376 |
|
3377 object: function(aGrip, aOptions) { |
|
3378 let {preview} = aGrip; |
|
3379 let stringifier; |
|
3380 if (preview && preview.kind) { |
|
3381 stringifier = VariablesView.stringifiers.byObjectKind[preview.kind]; |
|
3382 } |
|
3383 if (!stringifier && aGrip.class) { |
|
3384 stringifier = VariablesView.stringifiers.byObjectClass[aGrip.class]; |
|
3385 } |
|
3386 if (stringifier) { |
|
3387 return stringifier(aGrip, aOptions); |
|
3388 } |
|
3389 return null; |
|
3390 }, |
|
3391 }; // VariablesView.stringifiers.byType |
|
3392 |
|
3393 VariablesView.stringifiers.byObjectClass = { |
|
3394 Function: function(aGrip, {concise}) { |
|
3395 // TODO: Bug 948484 - support arrow functions and ES6 generators |
|
3396 |
|
3397 let name = aGrip.userDisplayName || aGrip.displayName || aGrip.name || ""; |
|
3398 name = VariablesView.getString(name, { noStringQuotes: true }); |
|
3399 |
|
3400 // TODO: Bug 948489 - Support functions with destructured parameters and |
|
3401 // rest parameters |
|
3402 let params = aGrip.parameterNames || ""; |
|
3403 if (!concise) { |
|
3404 return "function " + name + "(" + params + ")"; |
|
3405 } |
|
3406 return (name || "function ") + "(" + params + ")"; |
|
3407 }, |
|
3408 |
|
3409 RegExp: function({displayString}) { |
|
3410 return VariablesView.getString(displayString, { noStringQuotes: true }); |
|
3411 }, |
|
3412 |
|
3413 Date: function({preview}) { |
|
3414 if (!preview || !("timestamp" in preview)) { |
|
3415 return null; |
|
3416 } |
|
3417 |
|
3418 if (typeof preview.timestamp != "number") { |
|
3419 return new Date(preview.timestamp).toString(); // invalid date |
|
3420 } |
|
3421 |
|
3422 return "Date " + new Date(preview.timestamp).toISOString(); |
|
3423 }, |
|
3424 |
|
3425 String: function({displayString}) { |
|
3426 if (displayString === undefined) { |
|
3427 return null; |
|
3428 } |
|
3429 return VariablesView.getString(displayString); |
|
3430 }, |
|
3431 |
|
3432 Number: function({preview}) { |
|
3433 if (preview === undefined) { |
|
3434 return null; |
|
3435 } |
|
3436 return VariablesView.getString(preview.value); |
|
3437 }, |
|
3438 }; // VariablesView.stringifiers.byObjectClass |
|
3439 |
|
3440 VariablesView.stringifiers.byObjectClass.Boolean = |
|
3441 VariablesView.stringifiers.byObjectClass.Number; |
|
3442 |
|
3443 VariablesView.stringifiers.byObjectKind = { |
|
3444 ArrayLike: function(aGrip, {concise}) { |
|
3445 let {preview} = aGrip; |
|
3446 if (concise) { |
|
3447 return aGrip.class + "[" + preview.length + "]"; |
|
3448 } |
|
3449 |
|
3450 if (!preview.items) { |
|
3451 return null; |
|
3452 } |
|
3453 |
|
3454 let shown = 0, result = [], lastHole = null; |
|
3455 for (let item of preview.items) { |
|
3456 if (item === null) { |
|
3457 if (lastHole !== null) { |
|
3458 result[lastHole] += ","; |
|
3459 } else { |
|
3460 result.push(""); |
|
3461 } |
|
3462 lastHole = result.length - 1; |
|
3463 } else { |
|
3464 lastHole = null; |
|
3465 result.push(VariablesView.getString(item, { concise: true })); |
|
3466 } |
|
3467 shown++; |
|
3468 } |
|
3469 |
|
3470 if (shown < preview.length) { |
|
3471 let n = preview.length - shown; |
|
3472 result.push(VariablesView.stringifiers._getNMoreString(n)); |
|
3473 } else if (lastHole !== null) { |
|
3474 // make sure we have the right number of commas... |
|
3475 result[lastHole] += ","; |
|
3476 } |
|
3477 |
|
3478 let prefix = aGrip.class == "Array" ? "" : aGrip.class + " "; |
|
3479 return prefix + "[" + result.join(", ") + "]"; |
|
3480 }, |
|
3481 |
|
3482 MapLike: function(aGrip, {concise}) { |
|
3483 let {preview} = aGrip; |
|
3484 if (concise || !preview.entries) { |
|
3485 let size = typeof preview.size == "number" ? |
|
3486 "[" + preview.size + "]" : ""; |
|
3487 return aGrip.class + size; |
|
3488 } |
|
3489 |
|
3490 let entries = []; |
|
3491 for (let [key, value] of preview.entries) { |
|
3492 let keyString = VariablesView.getString(key, { |
|
3493 concise: true, |
|
3494 noStringQuotes: true, |
|
3495 }); |
|
3496 let valueString = VariablesView.getString(value, { concise: true }); |
|
3497 entries.push(keyString + ": " + valueString); |
|
3498 } |
|
3499 |
|
3500 if (typeof preview.size == "number" && preview.size > entries.length) { |
|
3501 let n = preview.size - entries.length; |
|
3502 entries.push(VariablesView.stringifiers._getNMoreString(n)); |
|
3503 } |
|
3504 |
|
3505 return aGrip.class + " {" + entries.join(", ") + "}"; |
|
3506 }, |
|
3507 |
|
3508 ObjectWithText: function(aGrip, {concise}) { |
|
3509 if (concise) { |
|
3510 return aGrip.class; |
|
3511 } |
|
3512 |
|
3513 return aGrip.class + " " + VariablesView.getString(aGrip.preview.text); |
|
3514 }, |
|
3515 |
|
3516 ObjectWithURL: function(aGrip, {concise}) { |
|
3517 let result = aGrip.class; |
|
3518 let url = aGrip.preview.url; |
|
3519 if (!VariablesView.isFalsy({ value: url })) { |
|
3520 result += " \u2192 " + WebConsoleUtils.abbreviateSourceURL(url, |
|
3521 { onlyCropQuery: !concise }); |
|
3522 } |
|
3523 return result; |
|
3524 }, |
|
3525 |
|
3526 // Stringifier for any kind of object. |
|
3527 Object: function(aGrip, {concise}) { |
|
3528 if (concise) { |
|
3529 return aGrip.class; |
|
3530 } |
|
3531 |
|
3532 let {preview} = aGrip; |
|
3533 let props = []; |
|
3534 for (let key of Object.keys(preview.ownProperties || {})) { |
|
3535 let value = preview.ownProperties[key]; |
|
3536 let valueString = ""; |
|
3537 if (value.get) { |
|
3538 valueString = "Getter"; |
|
3539 } else if (value.set) { |
|
3540 valueString = "Setter"; |
|
3541 } else { |
|
3542 valueString = VariablesView.getString(value.value, { concise: true }); |
|
3543 } |
|
3544 props.push(key + ": " + valueString); |
|
3545 } |
|
3546 |
|
3547 for (let key of Object.keys(preview.safeGetterValues || {})) { |
|
3548 let value = preview.safeGetterValues[key]; |
|
3549 let valueString = VariablesView.getString(value.getterValue, |
|
3550 { concise: true }); |
|
3551 props.push(key + ": " + valueString); |
|
3552 } |
|
3553 |
|
3554 if (!props.length) { |
|
3555 return null; |
|
3556 } |
|
3557 |
|
3558 if (preview.ownPropertiesLength) { |
|
3559 let previewLength = Object.keys(preview.ownProperties).length; |
|
3560 let diff = preview.ownPropertiesLength - previewLength; |
|
3561 if (diff > 0) { |
|
3562 props.push(VariablesView.stringifiers._getNMoreString(diff)); |
|
3563 } |
|
3564 } |
|
3565 |
|
3566 let prefix = aGrip.class != "Object" ? aGrip.class + " " : ""; |
|
3567 return prefix + "{" + props.join(", ") + "}"; |
|
3568 }, // Object |
|
3569 |
|
3570 Error: function(aGrip, {concise}) { |
|
3571 let {preview} = aGrip; |
|
3572 let name = VariablesView.getString(preview.name, { noStringQuotes: true }); |
|
3573 if (concise) { |
|
3574 return name || aGrip.class; |
|
3575 } |
|
3576 |
|
3577 let msg = name + ": " + |
|
3578 VariablesView.getString(preview.message, { noStringQuotes: true }); |
|
3579 |
|
3580 if (!VariablesView.isFalsy({ value: preview.stack })) { |
|
3581 msg += "\n" + STR.GetStringFromName("variablesViewErrorStacktrace") + |
|
3582 "\n" + preview.stack; |
|
3583 } |
|
3584 |
|
3585 return msg; |
|
3586 }, |
|
3587 |
|
3588 DOMException: function(aGrip, {concise}) { |
|
3589 let {preview} = aGrip; |
|
3590 if (concise) { |
|
3591 return preview.name || aGrip.class; |
|
3592 } |
|
3593 |
|
3594 let msg = aGrip.class + " [" + preview.name + ": " + |
|
3595 VariablesView.getString(preview.message) + "\n" + |
|
3596 "code: " + preview.code + "\n" + |
|
3597 "nsresult: 0x" + (+preview.result).toString(16); |
|
3598 |
|
3599 if (preview.filename) { |
|
3600 msg += "\nlocation: " + preview.filename; |
|
3601 if (preview.lineNumber) { |
|
3602 msg += ":" + preview.lineNumber; |
|
3603 } |
|
3604 } |
|
3605 |
|
3606 return msg + "]"; |
|
3607 }, |
|
3608 |
|
3609 DOMEvent: function(aGrip, {concise}) { |
|
3610 let {preview} = aGrip; |
|
3611 if (!preview.type) { |
|
3612 return null; |
|
3613 } |
|
3614 |
|
3615 if (concise) { |
|
3616 return aGrip.class + " " + preview.type; |
|
3617 } |
|
3618 |
|
3619 let result = preview.type; |
|
3620 |
|
3621 if (preview.eventKind == "key" && preview.modifiers && |
|
3622 preview.modifiers.length) { |
|
3623 result += " " + preview.modifiers.join("-"); |
|
3624 } |
|
3625 |
|
3626 let props = []; |
|
3627 if (preview.target) { |
|
3628 let target = VariablesView.getString(preview.target, { concise: true }); |
|
3629 props.push("target: " + target); |
|
3630 } |
|
3631 |
|
3632 for (let prop in preview.properties) { |
|
3633 let value = preview.properties[prop]; |
|
3634 props.push(prop + ": " + VariablesView.getString(value, { concise: true })); |
|
3635 } |
|
3636 |
|
3637 return result + " {" + props.join(", ") + "}"; |
|
3638 }, // DOMEvent |
|
3639 |
|
3640 DOMNode: function(aGrip, {concise}) { |
|
3641 let {preview} = aGrip; |
|
3642 |
|
3643 switch (preview.nodeType) { |
|
3644 case Ci.nsIDOMNode.DOCUMENT_NODE: { |
|
3645 let location = WebConsoleUtils.abbreviateSourceURL(preview.location, |
|
3646 { onlyCropQuery: !concise }); |
|
3647 return aGrip.class + " \u2192 " + location; |
|
3648 } |
|
3649 |
|
3650 case Ci.nsIDOMNode.ATTRIBUTE_NODE: { |
|
3651 let value = VariablesView.getString(preview.value, { noStringQuotes: true }); |
|
3652 return preview.nodeName + '="' + escapeHTML(value) + '"'; |
|
3653 } |
|
3654 |
|
3655 case Ci.nsIDOMNode.TEXT_NODE: |
|
3656 return preview.nodeName + " " + |
|
3657 VariablesView.getString(preview.textContent); |
|
3658 |
|
3659 case Ci.nsIDOMNode.COMMENT_NODE: { |
|
3660 let comment = VariablesView.getString(preview.textContent, |
|
3661 { noStringQuotes: true }); |
|
3662 return "<!--" + comment + "-->"; |
|
3663 } |
|
3664 |
|
3665 case Ci.nsIDOMNode.DOCUMENT_FRAGMENT_NODE: { |
|
3666 if (concise || !preview.childNodes) { |
|
3667 return aGrip.class + "[" + preview.childNodesLength + "]"; |
|
3668 } |
|
3669 let nodes = []; |
|
3670 for (let node of preview.childNodes) { |
|
3671 nodes.push(VariablesView.getString(node)); |
|
3672 } |
|
3673 if (nodes.length < preview.childNodesLength) { |
|
3674 let n = preview.childNodesLength - nodes.length; |
|
3675 nodes.push(VariablesView.stringifiers._getNMoreString(n)); |
|
3676 } |
|
3677 return aGrip.class + " [" + nodes.join(", ") + "]"; |
|
3678 } |
|
3679 |
|
3680 case Ci.nsIDOMNode.ELEMENT_NODE: { |
|
3681 let attrs = preview.attributes; |
|
3682 if (!concise) { |
|
3683 let n = 0, result = "<" + preview.nodeName; |
|
3684 for (let name in attrs) { |
|
3685 let value = VariablesView.getString(attrs[name], |
|
3686 { noStringQuotes: true }); |
|
3687 result += " " + name + '="' + escapeHTML(value) + '"'; |
|
3688 n++; |
|
3689 } |
|
3690 if (preview.attributesLength > n) { |
|
3691 result += " " + Scope.ellipsis; |
|
3692 } |
|
3693 return result + ">"; |
|
3694 } |
|
3695 |
|
3696 let result = "<" + preview.nodeName; |
|
3697 if (attrs.id) { |
|
3698 result += "#" + attrs.id; |
|
3699 } |
|
3700 return result + ">"; |
|
3701 } |
|
3702 |
|
3703 default: |
|
3704 return null; |
|
3705 } |
|
3706 }, // DOMNode |
|
3707 }; // VariablesView.stringifiers.byObjectKind |
|
3708 |
|
3709 |
|
3710 /** |
|
3711 * Get the "N more…" formatted string, given an N. This is used for displaying |
|
3712 * how many elements are not displayed in an object preview (eg. an array). |
|
3713 * |
|
3714 * @private |
|
3715 * @param number aNumber |
|
3716 * @return string |
|
3717 */ |
|
3718 VariablesView.stringifiers._getNMoreString = function(aNumber) { |
|
3719 let str = STR.GetStringFromName("variablesViewMoreObjects"); |
|
3720 return PluralForm.get(aNumber, str).replace("#1", aNumber); |
|
3721 }; |
|
3722 |
|
3723 /** |
|
3724 * Returns a custom class style for a grip. |
|
3725 * |
|
3726 * @param any aGrip |
|
3727 * @see Variable.setGrip |
|
3728 * @return string |
|
3729 * The custom class style. |
|
3730 */ |
|
3731 VariablesView.getClass = function(aGrip) { |
|
3732 if (aGrip && typeof aGrip == "object") { |
|
3733 if (aGrip.preview) { |
|
3734 switch (aGrip.preview.kind) { |
|
3735 case "DOMNode": |
|
3736 return "token-domnode"; |
|
3737 } |
|
3738 } |
|
3739 |
|
3740 switch (aGrip.type) { |
|
3741 case "undefined": |
|
3742 return "token-undefined"; |
|
3743 case "null": |
|
3744 return "token-null"; |
|
3745 case "Infinity": |
|
3746 case "-Infinity": |
|
3747 case "NaN": |
|
3748 case "-0": |
|
3749 return "token-number"; |
|
3750 case "longString": |
|
3751 return "token-string"; |
|
3752 } |
|
3753 } |
|
3754 switch (typeof aGrip) { |
|
3755 case "string": |
|
3756 return "token-string"; |
|
3757 case "boolean": |
|
3758 return "token-boolean"; |
|
3759 case "number": |
|
3760 return "token-number"; |
|
3761 default: |
|
3762 return "token-other"; |
|
3763 } |
|
3764 }; |
|
3765 |
|
3766 /** |
|
3767 * A monotonically-increasing counter, that guarantees the uniqueness of scope, |
|
3768 * variables and properties ids. |
|
3769 * |
|
3770 * @param string aName |
|
3771 * An optional string to prefix the id with. |
|
3772 * @return number |
|
3773 * A unique id. |
|
3774 */ |
|
3775 let generateId = (function() { |
|
3776 let count = 0; |
|
3777 return function(aName = "") { |
|
3778 return aName.toLowerCase().trim().replace(/\s+/g, "-") + (++count); |
|
3779 }; |
|
3780 })(); |
|
3781 |
|
3782 /** |
|
3783 * Escape some HTML special characters. We do not need full HTML serialization |
|
3784 * here, we just want to make strings safe to display in HTML attributes, for |
|
3785 * the stringifiers. |
|
3786 * |
|
3787 * @param string aString |
|
3788 * @return string |
|
3789 */ |
|
3790 function escapeHTML(aString) { |
|
3791 return aString.replace(/&/g, "&") |
|
3792 .replace(/"/g, """) |
|
3793 .replace(/</g, "<") |
|
3794 .replace(/>/g, ">"); |
|
3795 } |
|
3796 |
|
3797 |
|
3798 /** |
|
3799 * An Editable encapsulates the UI of an edit box that overlays a label, |
|
3800 * allowing the user to edit the value. |
|
3801 * |
|
3802 * @param Variable aVariable |
|
3803 * The Variable or Property to make editable. |
|
3804 * @param object aOptions |
|
3805 * - onSave |
|
3806 * The callback to call with the value when editing is complete. |
|
3807 * - onCleanup |
|
3808 * The callback to call when the editable is removed for any reason. |
|
3809 */ |
|
3810 function Editable(aVariable, aOptions) { |
|
3811 this._variable = aVariable; |
|
3812 this._onSave = aOptions.onSave; |
|
3813 this._onCleanup = aOptions.onCleanup; |
|
3814 } |
|
3815 |
|
3816 Editable.create = function(aVariable, aOptions, aEvent) { |
|
3817 let editable = new this(aVariable, aOptions); |
|
3818 editable.activate(aEvent); |
|
3819 return editable; |
|
3820 }; |
|
3821 |
|
3822 Editable.prototype = { |
|
3823 /** |
|
3824 * The class name for targeting this Editable type's label element. Overridden |
|
3825 * by inheriting classes. |
|
3826 */ |
|
3827 className: null, |
|
3828 |
|
3829 /** |
|
3830 * Boolean indicating whether this Editable should activate. Overridden by |
|
3831 * inheriting classes. |
|
3832 */ |
|
3833 shouldActivate: null, |
|
3834 |
|
3835 /** |
|
3836 * The label element for this Editable. Overridden by inheriting classes. |
|
3837 */ |
|
3838 label: null, |
|
3839 |
|
3840 /** |
|
3841 * Activate this editable by replacing the input box it overlays and |
|
3842 * initialize the handlers. |
|
3843 * |
|
3844 * @param Event e [optional] |
|
3845 * Optionally, the Event object that was used to activate the Editable. |
|
3846 */ |
|
3847 activate: function(e) { |
|
3848 if (!this.shouldActivate) { |
|
3849 this._onCleanup && this._onCleanup(); |
|
3850 return; |
|
3851 } |
|
3852 |
|
3853 let { label } = this; |
|
3854 let initialString = label.getAttribute("value"); |
|
3855 |
|
3856 if (e) { |
|
3857 e.preventDefault(); |
|
3858 e.stopPropagation(); |
|
3859 } |
|
3860 |
|
3861 // Create a texbox input element which will be shown in the current |
|
3862 // element's specified label location. |
|
3863 let input = this._input = this._variable.document.createElement("textbox"); |
|
3864 input.className = "plain " + this.className; |
|
3865 input.setAttribute("value", initialString); |
|
3866 input.setAttribute("flex", "1"); |
|
3867 |
|
3868 // Replace the specified label with a textbox input element. |
|
3869 label.parentNode.replaceChild(input, label); |
|
3870 this._variable._variablesView.boxObject.ensureElementIsVisible(input); |
|
3871 input.select(); |
|
3872 |
|
3873 // When the value is a string (displayed as "value"), then we probably want |
|
3874 // to change it to another string in the textbox, so to avoid typing the "" |
|
3875 // again, tackle with the selection bounds just a bit. |
|
3876 if (initialString.match(/^".+"$/)) { |
|
3877 input.selectionEnd--; |
|
3878 input.selectionStart++; |
|
3879 } |
|
3880 |
|
3881 this._onKeypress = this._onKeypress.bind(this); |
|
3882 this._onBlur = this._onBlur.bind(this); |
|
3883 input.addEventListener("keypress", this._onKeypress); |
|
3884 input.addEventListener("blur", this._onBlur); |
|
3885 |
|
3886 this._prevExpandable = this._variable.twisty; |
|
3887 this._prevExpanded = this._variable.expanded; |
|
3888 this._variable.collapse(); |
|
3889 this._variable.hideArrow(); |
|
3890 this._variable.locked = true; |
|
3891 this._variable.editing = true; |
|
3892 }, |
|
3893 |
|
3894 /** |
|
3895 * Remove the input box and restore the Variable or Property to its previous |
|
3896 * state. |
|
3897 */ |
|
3898 deactivate: function() { |
|
3899 this._input.removeEventListener("keypress", this._onKeypress); |
|
3900 this._input.removeEventListener("blur", this.deactivate); |
|
3901 this._input.parentNode.replaceChild(this.label, this._input); |
|
3902 this._input = null; |
|
3903 |
|
3904 let { boxObject } = this._variable._variablesView; |
|
3905 boxObject.scrollBy(-this._variable._target, 0); |
|
3906 this._variable.locked = false; |
|
3907 this._variable.twisty = this._prevExpandable; |
|
3908 this._variable.expanded = this._prevExpanded; |
|
3909 this._variable.editing = false; |
|
3910 this._onCleanup && this._onCleanup(); |
|
3911 }, |
|
3912 |
|
3913 /** |
|
3914 * Save the current value and deactivate the Editable. |
|
3915 */ |
|
3916 _save: function() { |
|
3917 let initial = this.label.getAttribute("value"); |
|
3918 let current = this._input.value.trim(); |
|
3919 this.deactivate(); |
|
3920 if (initial != current) { |
|
3921 this._onSave(current); |
|
3922 } |
|
3923 }, |
|
3924 |
|
3925 /** |
|
3926 * Called when tab is pressed, allowing subclasses to link different |
|
3927 * behavior to tabbing if desired. |
|
3928 */ |
|
3929 _next: function() { |
|
3930 this._save(); |
|
3931 }, |
|
3932 |
|
3933 /** |
|
3934 * Called when escape is pressed, indicating a cancelling of editing without |
|
3935 * saving. |
|
3936 */ |
|
3937 _reset: function() { |
|
3938 this.deactivate(); |
|
3939 this._variable.focus(); |
|
3940 }, |
|
3941 |
|
3942 /** |
|
3943 * Event handler for when the input loses focus. |
|
3944 */ |
|
3945 _onBlur: function() { |
|
3946 this.deactivate(); |
|
3947 }, |
|
3948 |
|
3949 /** |
|
3950 * Event handler for when the input receives a key press. |
|
3951 */ |
|
3952 _onKeypress: function(e) { |
|
3953 e.stopPropagation(); |
|
3954 |
|
3955 switch (e.keyCode) { |
|
3956 case e.DOM_VK_TAB: |
|
3957 this._next(); |
|
3958 break; |
|
3959 case e.DOM_VK_RETURN: |
|
3960 this._save(); |
|
3961 break; |
|
3962 case e.DOM_VK_ESCAPE: |
|
3963 this._reset(); |
|
3964 break; |
|
3965 } |
|
3966 }, |
|
3967 }; |
|
3968 |
|
3969 |
|
3970 /** |
|
3971 * An Editable specific to editing the name of a Variable or Property. |
|
3972 */ |
|
3973 function EditableName(aVariable, aOptions) { |
|
3974 Editable.call(this, aVariable, aOptions); |
|
3975 } |
|
3976 |
|
3977 EditableName.create = Editable.create; |
|
3978 |
|
3979 EditableName.prototype = Heritage.extend(Editable.prototype, { |
|
3980 className: "element-name-input", |
|
3981 |
|
3982 get label() { |
|
3983 return this._variable._name; |
|
3984 }, |
|
3985 |
|
3986 get shouldActivate() { |
|
3987 return !!this._variable.ownerView.switch; |
|
3988 }, |
|
3989 }); |
|
3990 |
|
3991 |
|
3992 /** |
|
3993 * An Editable specific to editing the value of a Variable or Property. |
|
3994 */ |
|
3995 function EditableValue(aVariable, aOptions) { |
|
3996 Editable.call(this, aVariable, aOptions); |
|
3997 } |
|
3998 |
|
3999 EditableValue.create = Editable.create; |
|
4000 |
|
4001 EditableValue.prototype = Heritage.extend(Editable.prototype, { |
|
4002 className: "element-value-input", |
|
4003 |
|
4004 get label() { |
|
4005 return this._variable._valueLabel; |
|
4006 }, |
|
4007 |
|
4008 get shouldActivate() { |
|
4009 return !!this._variable.ownerView.eval; |
|
4010 }, |
|
4011 }); |
|
4012 |
|
4013 |
|
4014 /** |
|
4015 * An Editable specific to editing the key and value of a new property. |
|
4016 */ |
|
4017 function EditableNameAndValue(aVariable, aOptions) { |
|
4018 EditableName.call(this, aVariable, aOptions); |
|
4019 } |
|
4020 |
|
4021 EditableNameAndValue.create = Editable.create; |
|
4022 |
|
4023 EditableNameAndValue.prototype = Heritage.extend(EditableName.prototype, { |
|
4024 _reset: function(e) { |
|
4025 // Hide the Variable or Property if the user presses escape. |
|
4026 this._variable.remove(); |
|
4027 this.deactivate(); |
|
4028 }, |
|
4029 |
|
4030 _next: function(e) { |
|
4031 // Override _next so as to set both key and value at the same time. |
|
4032 let key = this._input.value; |
|
4033 this.label.setAttribute("value", key); |
|
4034 |
|
4035 let valueEditable = EditableValue.create(this._variable, { |
|
4036 onSave: aValue => { |
|
4037 this._onSave([key, aValue]); |
|
4038 } |
|
4039 }); |
|
4040 valueEditable._reset = () => { |
|
4041 this._variable.remove(); |
|
4042 valueEditable.deactivate(); |
|
4043 }; |
|
4044 }, |
|
4045 |
|
4046 _save: function(e) { |
|
4047 // Both _save and _next activate the value edit box. |
|
4048 this._next(e); |
|
4049 } |
|
4050 }); |