|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 var kMaxChunkDuration = 30; // ms |
|
8 |
|
9 function escapeHTML(html) { |
|
10 var pre = document.createElementNS("http://www.w3.org/1999/xhtml", "pre"); |
|
11 var text = document.createTextNode(html); |
|
12 pre.appendChild(text); |
|
13 return pre.innerHTML; |
|
14 } |
|
15 |
|
16 RegExp.escape = function(text) { |
|
17 return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); |
|
18 } |
|
19 |
|
20 var requestAnimationFrame = window.webkitRequestAnimationFrame || |
|
21 window.mozRequestAnimationFrame || |
|
22 window.oRequestAnimationFrame || |
|
23 window.msRequestAnimationFrame || |
|
24 function(callback, element) { |
|
25 return window.setTimeout(callback, 1000 / 60); |
|
26 }; |
|
27 |
|
28 var cancelAnimationFrame = window.webkitCancelAnimationFrame || |
|
29 window.mozCancelAnimationFrame || |
|
30 window.oCancelAnimationFrame || |
|
31 window.msCancelAnimationFrame || |
|
32 function(req) { |
|
33 window.clearTimeout(req); |
|
34 }; |
|
35 |
|
36 function TreeView() { |
|
37 this._eventListeners = {}; |
|
38 this._pendingActions = []; |
|
39 this._pendingActionsProcessingCallback = null; |
|
40 |
|
41 this._container = document.createElement("div"); |
|
42 this._container.className = "treeViewContainer"; |
|
43 this._container.setAttribute("tabindex", "0"); // make it focusable |
|
44 |
|
45 this._header = document.createElement("ul"); |
|
46 this._header.className = "treeHeader"; |
|
47 this._container.appendChild(this._header); |
|
48 |
|
49 this._verticalScrollbox = document.createElement("div"); |
|
50 this._verticalScrollbox.className = "treeViewVerticalScrollbox"; |
|
51 this._container.appendChild(this._verticalScrollbox); |
|
52 |
|
53 this._leftColumnBackground = document.createElement("div"); |
|
54 this._leftColumnBackground.className = "leftColumnBackground"; |
|
55 this._verticalScrollbox.appendChild(this._leftColumnBackground); |
|
56 |
|
57 this._horizontalScrollbox = document.createElement("div"); |
|
58 this._horizontalScrollbox.className = "treeViewHorizontalScrollbox"; |
|
59 this._verticalScrollbox.appendChild(this._horizontalScrollbox); |
|
60 |
|
61 this._styleElement = document.createElement("style"); |
|
62 this._styleElement.setAttribute("type", "text/css"); |
|
63 this._container.appendChild(this._styleElement); |
|
64 |
|
65 this._contextMenu = document.createElement("menu"); |
|
66 this._contextMenu.setAttribute("type", "context"); |
|
67 this._contextMenu.id = "contextMenuForTreeView" + TreeView.instanceCounter++; |
|
68 this._container.appendChild(this._contextMenu); |
|
69 |
|
70 this._busyCover = document.createElement("div"); |
|
71 this._busyCover.className = "busyCover"; |
|
72 this._container.appendChild(this._busyCover); |
|
73 this._abortToggleAll = false; |
|
74 this.initSelection = true; |
|
75 |
|
76 var self = this; |
|
77 this._container.onkeydown = function (e) { |
|
78 self._onkeypress(e); |
|
79 }; |
|
80 this._container.onkeypress = function (e) { |
|
81 // on key down gives us '8' and mapping shift+8='*' may not be portable. |
|
82 if (String.fromCharCode(e.charCode) == '*') |
|
83 self._onkeypress(e); |
|
84 }; |
|
85 this._container.onclick = function (e) { |
|
86 self._onclick(e); |
|
87 }; |
|
88 this._verticalScrollbox.addEventListener("contextmenu", function(event) { |
|
89 self._populateContextMenu(event); |
|
90 }, true); |
|
91 this._setUpScrolling(); |
|
92 }; |
|
93 TreeView.instanceCounter = 0; |
|
94 |
|
95 TreeView.prototype = { |
|
96 getContainer: function TreeView_getContainer() { |
|
97 return this._container; |
|
98 }, |
|
99 setColumns: function TreeView_setColumns(columns) { |
|
100 this._header.innerHTML = ""; |
|
101 for (var i = 0; i < columns.length; i++) { |
|
102 var li = document.createElement("li"); |
|
103 li.className = "treeColumnHeader treeColumnHeader" + i; |
|
104 li.id = columns[i].name + "Header"; |
|
105 li.textContent = columns[i].title; |
|
106 this._header.appendChild(li); |
|
107 } |
|
108 }, |
|
109 dataIsOutdated: function TreeView_dataIsOutdated() { |
|
110 this._busyCover.classList.add("busy"); |
|
111 }, |
|
112 display: function TreeView_display(data, resources, filterByName) { |
|
113 this._busyCover.classList.remove("busy"); |
|
114 this._filterByName = filterByName; |
|
115 this._resources = resources; |
|
116 this._addResourceIconStyles(); |
|
117 this._filterByNameReg = null; // lazy init |
|
118 if (this._filterByName === "") |
|
119 this._filterByName = null; |
|
120 this._horizontalScrollbox.innerHTML = ""; |
|
121 this._horizontalScrollbox.data = data[0].getData(); |
|
122 if (this._pendingActionsProcessingCallback) { |
|
123 cancelAnimationFrame(this._pendingActionsProcessingCallback); |
|
124 this._pendingActionsProcessingCallback = 0; |
|
125 } |
|
126 this._pendingActions = []; |
|
127 |
|
128 this._pendingActions.push({ |
|
129 parentElement: this._horizontalScrollbox, |
|
130 parentNode: null, |
|
131 data: data[0].getData() |
|
132 }); |
|
133 this._processPendingActionsChunk(); |
|
134 changeFocus(this._container); |
|
135 }, |
|
136 // Provide a snapshot of the reverse selection to restore with 'invert callback' |
|
137 getReverseSelectionSnapshot: function TreeView__getReverseSelectionSnapshot(isJavascriptOnly) { |
|
138 var snapshot = []; |
|
139 |
|
140 if (!this._selectedNode) { |
|
141 return snapshot; |
|
142 } |
|
143 |
|
144 var curr = this._selectedNode.data; |
|
145 |
|
146 while(curr) { |
|
147 if (isJavascriptOnly && curr.isJSFrame || !isJavascriptOnly) { |
|
148 snapshot.push(curr.name); |
|
149 //dump(JSON.stringify(curr.name) + "\n"); |
|
150 } |
|
151 if (curr.treeChildren && curr.treeChildren.length >= 1) { |
|
152 curr = curr.treeChildren[0].getData(); |
|
153 } else { |
|
154 break; |
|
155 } |
|
156 } |
|
157 |
|
158 return snapshot.reverse(); |
|
159 }, |
|
160 // Provide a snapshot of the current selection to restore |
|
161 getSelectionSnapshot: function TreeView__getSelectionSnapshot(isJavascriptOnly) { |
|
162 var snapshot = []; |
|
163 var curr = this._selectedNode; |
|
164 |
|
165 while(curr) { |
|
166 if (isJavascriptOnly && curr.data.isJSFrame || !isJavascriptOnly) { |
|
167 snapshot.push(curr.data.name); |
|
168 //dump(JSON.stringify(curr.data.name) + "\n"); |
|
169 } |
|
170 curr = curr.treeParent; |
|
171 } |
|
172 |
|
173 return snapshot.reverse(); |
|
174 }, |
|
175 setSelection: function TreeView_setSelection(frames) { |
|
176 this.restoreSelectionSnapshot(frames, false); |
|
177 }, |
|
178 // Take a selection snapshot and restore the selection |
|
179 restoreSelectionSnapshot: function TreeView_restoreSelectionSnapshot(snapshot, allowNonContiguous) { |
|
180 var currNode = this._horizontalScrollbox.firstChild; |
|
181 if (currNode.data.name == snapshot[0] || snapshot[0] == "(total)") { |
|
182 snapshot.shift(); |
|
183 } |
|
184 //dump("len: " + snapshot.length + "\n"); |
|
185 next_level: while (currNode && snapshot.length > 0) { |
|
186 this._toggle(currNode, false, true); |
|
187 this._syncProcessPendingActionProcessing(); |
|
188 for (var i = 0; i < currNode.treeChildren.length; i++) { |
|
189 if (currNode.treeChildren[i].data.name == snapshot[0]) { |
|
190 snapshot.shift(); |
|
191 this._toggle(currNode, false, true); |
|
192 currNode = currNode.treeChildren[i]; |
|
193 continue next_level; |
|
194 } |
|
195 } |
|
196 if (allowNonContiguous) { |
|
197 // We need to do a Breadth-first search to find a match |
|
198 var pendingSearch = [currNode.data]; |
|
199 while (pendingSearch.length > 0) { |
|
200 var node = pendingSearch.shift(); |
|
201 if (!node.treeChildren) |
|
202 continue; |
|
203 for (var i = 0; i < node.treeChildren.length; i++) { |
|
204 var childNode = node.treeChildren[i].getData(); |
|
205 if (childNode.name == snapshot[0]) { |
|
206 //dump("found: " + childNode.name + "\n"); |
|
207 snapshot.shift(); |
|
208 var nodesToToggle = [childNode]; |
|
209 while (nodesToToggle[0].name != currNode.data.name) { |
|
210 nodesToToggle.splice(0, 0, nodesToToggle[0].parent); |
|
211 } |
|
212 var lastToggle = currNode; |
|
213 for (var j = 0; j < nodesToToggle.length; j++) { |
|
214 for (var k = 0; k < lastToggle.treeChildren.length; k++) { |
|
215 if (lastToggle.treeChildren[k].data.name == nodesToToggle[j].name) { |
|
216 //dump("Expend: " + nodesToToggle[j].name + "\n"); |
|
217 this._toggle(lastToggle.treeChildren[k], false, true); |
|
218 lastToggle = lastToggle.treeChildren[k]; |
|
219 this._syncProcessPendingActionProcessing(); |
|
220 } |
|
221 } |
|
222 } |
|
223 currNode = lastToggle; |
|
224 continue next_level; |
|
225 } |
|
226 //dump("pending: " + childNode.name + "\n"); |
|
227 pendingSearch.push(childNode); |
|
228 } |
|
229 } |
|
230 } |
|
231 break; // Didn't find child node matching |
|
232 } |
|
233 |
|
234 if (currNode == this._horizontalScrollbox) { |
|
235 PROFILERERROR("Failed to restore selection, could not find root.\n"); |
|
236 return; |
|
237 } |
|
238 |
|
239 this._toggle(currNode, true, true); |
|
240 this._select(currNode); |
|
241 }, |
|
242 _processPendingActionsChunk: function TreeView__processPendingActionsChunk(isSync) { |
|
243 this._pendingActionsProcessingCallback = 0; |
|
244 |
|
245 var startTime = Date.now(); |
|
246 var endTime = startTime + kMaxChunkDuration; |
|
247 while ((isSync == true || Date.now() < endTime) && this._pendingActions.length > 0) { |
|
248 this._processOneAction(this._pendingActions.shift()); |
|
249 } |
|
250 this._scrollHeightChanged(); |
|
251 |
|
252 this._schedulePendingActionProcessing(); |
|
253 }, |
|
254 _schedulePendingActionProcessing: function TreeView__schedulePendingActionProcessing() { |
|
255 if (!this._pendingActionsProcessingCallback && this._pendingActions.length > 0) { |
|
256 var self = this; |
|
257 this._pendingActionsProcessingCallback = requestAnimationFrame(function () { |
|
258 self._processPendingActionsChunk(); |
|
259 }); |
|
260 } |
|
261 }, |
|
262 _syncProcessPendingActionProcessing: function TreeView__syncProcessPendingActionProcessing() { |
|
263 this._processPendingActionsChunk(true); |
|
264 }, |
|
265 _processOneAction: function TreeView__processOneAction(action) { |
|
266 var li = this._createTree(action.parentElement, action.parentNode, action.data); |
|
267 if ("allChildrenCollapsedValue" in action) { |
|
268 if (this._abortToggleAll) |
|
269 return; |
|
270 this._toggleAll(li, action.allChildrenCollapsedValue, true); |
|
271 } |
|
272 }, |
|
273 addEventListener: function TreeView_addEventListener(eventName, callbackFunction) { |
|
274 if (!(eventName in this._eventListeners)) |
|
275 this._eventListeners[eventName] = []; |
|
276 if (this._eventListeners[eventName].indexOf(callbackFunction) != -1) |
|
277 return; |
|
278 this._eventListeners[eventName].push(callbackFunction); |
|
279 }, |
|
280 removeEventListener: function TreeView_removeEventListener(eventName, callbackFunction) { |
|
281 if (!(eventName in this._eventListeners)) |
|
282 return; |
|
283 var index = this._eventListeners[eventName].indexOf(callbackFunction); |
|
284 if (index == -1) |
|
285 return; |
|
286 this._eventListeners[eventName].splice(index, 1); |
|
287 }, |
|
288 _fireEvent: function TreeView__fireEvent(eventName, eventObject) { |
|
289 if (!(eventName in this._eventListeners)) |
|
290 return; |
|
291 this._eventListeners[eventName].forEach(function (callbackFunction) { |
|
292 callbackFunction(eventObject); |
|
293 }); |
|
294 }, |
|
295 _setUpScrolling: function TreeView__setUpScrolling() { |
|
296 var waitingForPaint = false; |
|
297 var accumulatedDeltaX = 0; |
|
298 var accumulatedDeltaY = 0; |
|
299 var self = this; |
|
300 function scrollListener(e) { |
|
301 if (!waitingForPaint) { |
|
302 requestAnimationFrame(function () { |
|
303 self._horizontalScrollbox.scrollLeft += accumulatedDeltaX; |
|
304 self._verticalScrollbox.scrollTop += accumulatedDeltaY; |
|
305 accumulatedDeltaX = 0; |
|
306 accumulatedDeltaY = 0; |
|
307 waitingForPaint = false; |
|
308 }); |
|
309 waitingForPaint = true; |
|
310 } |
|
311 if (e.axis == e.HORIZONTAL_AXIS) { |
|
312 accumulatedDeltaX += e.detail; |
|
313 } else { |
|
314 accumulatedDeltaY += e.detail; |
|
315 } |
|
316 e.preventDefault(); |
|
317 } |
|
318 this._verticalScrollbox.addEventListener("MozMousePixelScroll", scrollListener, false); |
|
319 this._verticalScrollbox.cleanUp = function () { |
|
320 self._verticalScrollbox.removeEventListener("MozMousePixelScroll", scrollListener, false); |
|
321 }; |
|
322 }, |
|
323 _scrollHeightChanged: function TreeView__scrollHeightChanged() { |
|
324 if (!this._pendingScrollHeightChanged) { |
|
325 var self = this; |
|
326 this._pendingScrollHeightChanged = setTimeout(function() { |
|
327 self._leftColumnBackground.style.height = self._horizontalScrollbox.getBoundingClientRect().height + 'px'; |
|
328 self._pendingScrollHeightChanged = null; |
|
329 }, 0); |
|
330 } |
|
331 }, |
|
332 _createTree: function TreeView__createTree(parentElement, parentNode, data) { |
|
333 var div = document.createElement("div"); |
|
334 div.className = "treeViewNode collapsed"; |
|
335 var hasChildren = ("children" in data) && (data.children.length > 0); |
|
336 if (!hasChildren) |
|
337 div.classList.add("leaf"); |
|
338 var treeLine = document.createElement("div"); |
|
339 treeLine.className = "treeLine"; |
|
340 treeLine.innerHTML = this._HTMLForFunction(data); |
|
341 div.depth = parentNode ? parentNode.depth + 1 : 0; |
|
342 div.style.marginLeft = div.depth + "em"; |
|
343 // When this item is toggled we will expand its children |
|
344 div.pendingExpand = []; |
|
345 div.treeLine = treeLine; |
|
346 div.data = data; |
|
347 // Useful for debugging |
|
348 //this.uniqueID = this.uniqueID || 0; |
|
349 //div.id = "Node" + this.uniqueID++; |
|
350 div.appendChild(treeLine); |
|
351 div.treeChildren = []; |
|
352 div.treeParent = parentNode; |
|
353 if (hasChildren) { |
|
354 for (var i = 0; i < data.children.length; ++i) { |
|
355 div.pendingExpand.push({parentElement: this._horizontalScrollbox, parentNode: div, data: data.children[i].getData() }); |
|
356 } |
|
357 } |
|
358 if (parentNode) { |
|
359 parentNode.treeChildren.push(div); |
|
360 } |
|
361 if (parentNode != null) { |
|
362 var nextTo; |
|
363 if (parentNode.treeChildren.length > 1) { |
|
364 nextTo = parentNode.treeChildren[parentNode.treeChildren.length-2].nextSibling; |
|
365 } else { |
|
366 nextTo = parentNode.nextSibling; |
|
367 } |
|
368 parentElement.insertBefore(div, nextTo); |
|
369 } else { |
|
370 parentElement.appendChild(div); |
|
371 } |
|
372 return div; |
|
373 }, |
|
374 _addResourceIconStyles: function TreeView__addResourceIconStyles() { |
|
375 var styles = []; |
|
376 for (var resourceName in this._resources) { |
|
377 var resource = this._resources[resourceName]; |
|
378 if (resource.icon) { |
|
379 styles.push('.resourceIcon[data-resource="' + resourceName + '"] { background-image: url("' + resource.icon + '"); }'); |
|
380 } |
|
381 } |
|
382 this._styleElement.textContent = styles.join("\n"); |
|
383 }, |
|
384 _populateContextMenu: function TreeView__populateContextMenu(event) { |
|
385 this._verticalScrollbox.setAttribute("contextmenu", ""); |
|
386 |
|
387 var target = event.target; |
|
388 if (target.classList.contains("expandCollapseButton") || |
|
389 target.classList.contains("focusCallstackButton")) |
|
390 return; |
|
391 |
|
392 var li = this._getParentTreeViewNode(target); |
|
393 if (!li) |
|
394 return; |
|
395 |
|
396 this._select(li); |
|
397 |
|
398 this._contextMenu.innerHTML = ""; |
|
399 |
|
400 var self = this; |
|
401 this._contextMenuForFunction(li.data).forEach(function (menuItem) { |
|
402 var menuItemNode = document.createElement("menuitem"); |
|
403 menuItemNode.onclick = (function (menuItem) { |
|
404 return function() { |
|
405 self._contextMenuClick(li.data, menuItem); |
|
406 }; |
|
407 })(menuItem); |
|
408 menuItemNode.label = menuItem; |
|
409 self._contextMenu.appendChild(menuItemNode); |
|
410 }); |
|
411 |
|
412 this._verticalScrollbox.setAttribute("contextmenu", this._contextMenu.id); |
|
413 }, |
|
414 _contextMenuClick: function TreeView__contextMenuClick(node, menuItem) { |
|
415 this._fireEvent("contextMenuClick", { node: node, menuItem: menuItem }); |
|
416 }, |
|
417 _contextMenuForFunction: function TreeView__contextMenuForFunction(node) { |
|
418 // TODO move me outside tree.js |
|
419 var menu = []; |
|
420 if (node.library && ( |
|
421 node.library.toLowerCase() == "lib_xul" || |
|
422 node.library.toLowerCase() == "lib_xul.dll" |
|
423 )) { |
|
424 menu.push("View Source"); |
|
425 } |
|
426 if (node.isJSFrame && node.scriptLocation) { |
|
427 menu.push("View JS Source"); |
|
428 } |
|
429 menu.push("Focus Frame"); |
|
430 menu.push("Focus Callstack"); |
|
431 menu.push("Google Search"); |
|
432 menu.push("Plugin View: Pie"); |
|
433 menu.push("Plugin View: Tree"); |
|
434 return menu; |
|
435 }, |
|
436 _HTMLForFunction: function TreeView__HTMLForFunction(node) { |
|
437 var nodeName = escapeHTML(node.name); |
|
438 var resource = this._resources[node.library] || {}; |
|
439 var libName = escapeHTML(resource.name || ""); |
|
440 if (this._filterByName) { |
|
441 if (!this._filterByNameReg) { |
|
442 this._filterByName = RegExp.escape(this._filterByName); |
|
443 this._filterByNameReg = new RegExp("(" + this._filterByName + ")","gi"); |
|
444 } |
|
445 nodeName = nodeName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>"); |
|
446 libName = libName.replace(this._filterByNameReg, "<a style='color:red;'>$1</a>"); |
|
447 } |
|
448 var samplePercentage; |
|
449 if (isNaN(node.ratio)) { |
|
450 samplePercentage = ""; |
|
451 } else { |
|
452 samplePercentage = (100 * node.ratio).toFixed(1) + "%"; |
|
453 } |
|
454 return '<input type="button" value="Expand / Collapse" class="expandCollapseButton" tabindex="-1"> ' + |
|
455 '<span class="sampleCount">' + node.counter + '</span> ' + |
|
456 '<span class="samplePercentage">' + samplePercentage + '</span> ' + |
|
457 '<span class="selfSampleCount">' + node.selfCounter + '</span> ' + |
|
458 '<span class="resourceIcon" data-resource="' + node.library + '"></span> ' + |
|
459 '<span class="functionName">' + nodeName + '</span>' + |
|
460 '<span class="libraryName">' + libName + '</span>' + |
|
461 ((nodeName === '(total)' || gHideSourceLinks) ? '' : |
|
462 '<input type="button" value="Focus Callstack" title="Focus Callstack" class="focusCallstackButton" tabindex="-1">'); |
|
463 }, |
|
464 _resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) { |
|
465 while (div.pendingExpand != null && div.pendingExpand.length > 0) { |
|
466 var pendingExpand = div.pendingExpand.shift(); |
|
467 pendingExpand.allChildrenCollapsedValue = childrenCollapsedValue; |
|
468 this._pendingActions.push(pendingExpand); |
|
469 this._schedulePendingActionProcessing(); |
|
470 } |
|
471 }, |
|
472 _showChild: function TreeView__showChild(div, isVisible) { |
|
473 for (var i = 0; i < div.treeChildren.length; i++) { |
|
474 div.treeChildren[i].style.display = isVisible?"":"none"; |
|
475 if (!isVisible) { |
|
476 div.treeChildren[i].classList.add("collapsed"); |
|
477 this._showChild(div.treeChildren[i], isVisible); |
|
478 } |
|
479 } |
|
480 }, |
|
481 _toggle: function TreeView__toggle(div, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) { |
|
482 var currentCollapsedValue = this._isCollapsed(div); |
|
483 if (newCollapsedValue === undefined) |
|
484 newCollapsedValue = !currentCollapsedValue; |
|
485 if (newCollapsedValue) { |
|
486 div.classList.add("collapsed"); |
|
487 this._showChild(div, false); |
|
488 } else { |
|
489 this._resolveChildren(div, true); |
|
490 div.classList.remove("collapsed"); |
|
491 this._showChild(div, true); |
|
492 } |
|
493 if (!suppressScrollHeightNotification) |
|
494 this._scrollHeightChanged(); |
|
495 }, |
|
496 _toggleAll: function TreeView__toggleAll(subtreeRoot, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) { |
|
497 |
|
498 // Reset abort |
|
499 this._abortToggleAll = false; |
|
500 |
|
501 // Expands / collapses all child nodes, too. |
|
502 |
|
503 if (newCollapsedValue === undefined) |
|
504 newCollapsedValue = !this._isCollapsed(subtreeRoot); |
|
505 if (!newCollapsedValue) { |
|
506 // expanding |
|
507 this._resolveChildren(subtreeRoot, newCollapsedValue); |
|
508 } |
|
509 this._toggle(subtreeRoot, newCollapsedValue, true); |
|
510 for (var i = 0; i < subtreeRoot.treeChildren.length; ++i) { |
|
511 this._toggleAll(subtreeRoot.treeChildren[i], newCollapsedValue, true); |
|
512 } |
|
513 if (!suppressScrollHeightNotification) |
|
514 this._scrollHeightChanged(); |
|
515 }, |
|
516 _getParent: function TreeView__getParent(div) { |
|
517 return div.treeParent; |
|
518 }, |
|
519 _getFirstChild: function TreeView__getFirstChild(div) { |
|
520 if (this._isCollapsed(div)) |
|
521 return null; |
|
522 var child = div.treeChildren[0]; |
|
523 return child; |
|
524 }, |
|
525 _getLastChild: function TreeView__getLastChild(div) { |
|
526 if (this._isCollapsed(div)) |
|
527 return div; |
|
528 var lastChild = div.treeChildren[div.treeChildren.length-1]; |
|
529 if (lastChild == null) |
|
530 return div; |
|
531 return this._getLastChild(lastChild); |
|
532 }, |
|
533 _getPrevSib: function TreeView__getPevSib(div) { |
|
534 if (div.treeParent == null) |
|
535 return null; |
|
536 var nodeIndex = div.treeParent.treeChildren.indexOf(div); |
|
537 if (nodeIndex == 0) |
|
538 return null; |
|
539 return div.treeParent.treeChildren[nodeIndex-1]; |
|
540 }, |
|
541 _getNextSib: function TreeView__getNextSib(div) { |
|
542 if (div.treeParent == null) |
|
543 return null; |
|
544 var nodeIndex = div.treeParent.treeChildren.indexOf(div); |
|
545 if (nodeIndex == div.treeParent.treeChildren.length - 1) |
|
546 return this._getNextSib(div.treeParent); |
|
547 return div.treeParent.treeChildren[nodeIndex+1]; |
|
548 }, |
|
549 _scheduleScrollIntoView: function TreeView__scheduleScrollIntoView(element, maxImportantWidth) { |
|
550 // Schedule this on the animation frame otherwise we may run this more then once per frames |
|
551 // causing more work then needed. |
|
552 var self = this; |
|
553 if (self._pendingAnimationFrame != null) { |
|
554 return; |
|
555 } |
|
556 self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() { |
|
557 cancelAnimationFrame(self._pendingAnimationFrame); |
|
558 self._pendingAnimationFrame = null; |
|
559 self._scrollIntoView(element, maxImportantWidth); |
|
560 }); |
|
561 }, |
|
562 _scrollIntoView: function TreeView__scrollIntoView(element, maxImportantWidth) { |
|
563 // Make sure that element is inside the visible part of our scrollbox by |
|
564 // adjusting the scroll positions. If element is wider or |
|
565 // higher than the scroll port, the left and top edges are prioritized over |
|
566 // the right and bottom edges. |
|
567 // If maxImportantWidth is set, parts of the beyond this widths are |
|
568 // considered as not important; they'll not be moved into view. |
|
569 |
|
570 if (maxImportantWidth === undefined) |
|
571 maxImportantWidth = Infinity; |
|
572 |
|
573 var visibleRect = { |
|
574 left: this._horizontalScrollbox.getBoundingClientRect().left + 150, // TODO: un-hardcode 150 |
|
575 top: this._verticalScrollbox.getBoundingClientRect().top, |
|
576 right: this._horizontalScrollbox.getBoundingClientRect().right, |
|
577 bottom: this._verticalScrollbox.getBoundingClientRect().bottom |
|
578 } |
|
579 var r = element.getBoundingClientRect(); |
|
580 var right = Math.min(r.right, r.left + maxImportantWidth); |
|
581 var leftCutoff = visibleRect.left - r.left; |
|
582 var rightCutoff = right - visibleRect.right; |
|
583 var topCutoff = visibleRect.top - r.top; |
|
584 var bottomCutoff = r.bottom - visibleRect.bottom; |
|
585 if (leftCutoff > 0) |
|
586 this._horizontalScrollbox.scrollLeft -= leftCutoff; |
|
587 else if (rightCutoff > 0) |
|
588 this._horizontalScrollbox.scrollLeft += Math.min(rightCutoff, -leftCutoff); |
|
589 if (topCutoff > 0) |
|
590 this._verticalScrollbox.scrollTop -= topCutoff; |
|
591 else if (bottomCutoff > 0) |
|
592 this._verticalScrollbox.scrollTop += Math.min(bottomCutoff, -topCutoff); |
|
593 }, |
|
594 _select: function TreeView__select(li) { |
|
595 if (this._selectedNode != null) { |
|
596 this._selectedNode.treeLine.classList.remove("selected"); |
|
597 this._selectedNode = null; |
|
598 } |
|
599 if (li) { |
|
600 li.treeLine.classList.add("selected"); |
|
601 this._selectedNode = li; |
|
602 var functionName = li.treeLine.querySelector(".functionName"); |
|
603 this._scheduleScrollIntoView(functionName, 400); |
|
604 this._fireEvent("select", li.data); |
|
605 } |
|
606 updateDocumentURL(); |
|
607 }, |
|
608 _isCollapsed: function TreeView__isCollapsed(div) { |
|
609 return div.classList.contains("collapsed"); |
|
610 }, |
|
611 _getParentTreeViewNode: function TreeView__getParentTreeViewNode(node) { |
|
612 while (node) { |
|
613 if (node.nodeType != node.ELEMENT_NODE) |
|
614 break; |
|
615 if (node.classList.contains("treeViewNode")) |
|
616 return node; |
|
617 node = node.parentNode; |
|
618 } |
|
619 return null; |
|
620 }, |
|
621 _onclick: function TreeView__onclick(event) { |
|
622 var target = event.target; |
|
623 var node = this._getParentTreeViewNode(target); |
|
624 if (!node) |
|
625 return; |
|
626 if (target.classList.contains("expandCollapseButton")) { |
|
627 if (event.altKey) |
|
628 this._toggleAll(node); |
|
629 else |
|
630 this._toggle(node); |
|
631 } else if (target.classList.contains("focusCallstackButton")) { |
|
632 this._fireEvent("focusCallstackButtonClicked", node.data); |
|
633 } else { |
|
634 this._select(node); |
|
635 if (event.detail == 2) // dblclick |
|
636 this._toggle(node); |
|
637 } |
|
638 }, |
|
639 _onkeypress: function TreeView__onkeypress(event) { |
|
640 if (event.ctrlKey || event.altKey || event.metaKey) |
|
641 return; |
|
642 |
|
643 this._abortToggleAll = true; |
|
644 |
|
645 var selected = this._selectedNode; |
|
646 if (event.keyCode < 37 || event.keyCode > 40) { |
|
647 if (event.keyCode != 0 || |
|
648 String.fromCharCode(event.charCode) != '*') { |
|
649 return; |
|
650 } |
|
651 } |
|
652 event.stopPropagation(); |
|
653 event.preventDefault(); |
|
654 if (!selected) |
|
655 return; |
|
656 if (event.keyCode == 37) { // KEY_LEFT |
|
657 var isCollapsed = this._isCollapsed(selected); |
|
658 if (!isCollapsed) { |
|
659 this._toggle(selected); |
|
660 } else { |
|
661 var parent = this._getParent(selected); |
|
662 if (parent != null) { |
|
663 this._select(parent); |
|
664 } |
|
665 } |
|
666 } else if (event.keyCode == 38) { // KEY_UP |
|
667 var prevSib = this._getPrevSib(selected); |
|
668 var parent = this._getParent(selected); |
|
669 if (prevSib != null) { |
|
670 this._select(this._getLastChild(prevSib)); |
|
671 } else if (parent != null) { |
|
672 this._select(parent); |
|
673 } |
|
674 } else if (event.keyCode == 39) { // KEY_RIGHT |
|
675 var isCollapsed = this._isCollapsed(selected); |
|
676 if (isCollapsed) { |
|
677 this._toggle(selected); |
|
678 this._syncProcessPendingActionProcessing(); |
|
679 } else { |
|
680 // Do KEY_DOWN |
|
681 var nextSib = this._getNextSib(selected); |
|
682 var child = this._getFirstChild(selected); |
|
683 if (child != null) { |
|
684 this._select(child); |
|
685 } else if (nextSib) { |
|
686 this._select(nextSib); |
|
687 } |
|
688 } |
|
689 } else if (event.keyCode == 40) { // KEY_DOWN |
|
690 var nextSib = this._getNextSib(selected); |
|
691 var child = this._getFirstChild(selected); |
|
692 if (child != null) { |
|
693 this._select(child); |
|
694 } else if (nextSib) { |
|
695 this._select(nextSib); |
|
696 } |
|
697 } else if (String.fromCharCode(event.charCode) == '*') { |
|
698 this._toggleAll(selected); |
|
699 } |
|
700 }, |
|
701 }; |
|
702 |