michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: var kMaxChunkDuration = 30; // ms michael@0: michael@0: function escapeHTML(html) { michael@0: var pre = document.createElementNS("http://www.w3.org/1999/xhtml", "pre"); michael@0: var text = document.createTextNode(html); michael@0: pre.appendChild(text); michael@0: return pre.innerHTML; michael@0: } michael@0: michael@0: RegExp.escape = function(text) { michael@0: return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); michael@0: } michael@0: michael@0: var requestAnimationFrame = window.webkitRequestAnimationFrame || michael@0: window.mozRequestAnimationFrame || michael@0: window.oRequestAnimationFrame || michael@0: window.msRequestAnimationFrame || michael@0: function(callback, element) { michael@0: return window.setTimeout(callback, 1000 / 60); michael@0: }; michael@0: michael@0: var cancelAnimationFrame = window.webkitCancelAnimationFrame || michael@0: window.mozCancelAnimationFrame || michael@0: window.oCancelAnimationFrame || michael@0: window.msCancelAnimationFrame || michael@0: function(req) { michael@0: window.clearTimeout(req); michael@0: }; michael@0: michael@0: function TreeView() { michael@0: this._eventListeners = {}; michael@0: this._pendingActions = []; michael@0: this._pendingActionsProcessingCallback = null; michael@0: michael@0: this._container = document.createElement("div"); michael@0: this._container.className = "treeViewContainer"; michael@0: this._container.setAttribute("tabindex", "0"); // make it focusable michael@0: michael@0: this._header = document.createElement("ul"); michael@0: this._header.className = "treeHeader"; michael@0: this._container.appendChild(this._header); michael@0: michael@0: this._verticalScrollbox = document.createElement("div"); michael@0: this._verticalScrollbox.className = "treeViewVerticalScrollbox"; michael@0: this._container.appendChild(this._verticalScrollbox); michael@0: michael@0: this._leftColumnBackground = document.createElement("div"); michael@0: this._leftColumnBackground.className = "leftColumnBackground"; michael@0: this._verticalScrollbox.appendChild(this._leftColumnBackground); michael@0: michael@0: this._horizontalScrollbox = document.createElement("div"); michael@0: this._horizontalScrollbox.className = "treeViewHorizontalScrollbox"; michael@0: this._verticalScrollbox.appendChild(this._horizontalScrollbox); michael@0: michael@0: this._styleElement = document.createElement("style"); michael@0: this._styleElement.setAttribute("type", "text/css"); michael@0: this._container.appendChild(this._styleElement); michael@0: michael@0: this._contextMenu = document.createElement("menu"); michael@0: this._contextMenu.setAttribute("type", "context"); michael@0: this._contextMenu.id = "contextMenuForTreeView" + TreeView.instanceCounter++; michael@0: this._container.appendChild(this._contextMenu); michael@0: michael@0: this._busyCover = document.createElement("div"); michael@0: this._busyCover.className = "busyCover"; michael@0: this._container.appendChild(this._busyCover); michael@0: this._abortToggleAll = false; michael@0: this.initSelection = true; michael@0: michael@0: var self = this; michael@0: this._container.onkeydown = function (e) { michael@0: self._onkeypress(e); michael@0: }; michael@0: this._container.onkeypress = function (e) { michael@0: // on key down gives us '8' and mapping shift+8='*' may not be portable. michael@0: if (String.fromCharCode(e.charCode) == '*') michael@0: self._onkeypress(e); michael@0: }; michael@0: this._container.onclick = function (e) { michael@0: self._onclick(e); michael@0: }; michael@0: this._verticalScrollbox.addEventListener("contextmenu", function(event) { michael@0: self._populateContextMenu(event); michael@0: }, true); michael@0: this._setUpScrolling(); michael@0: }; michael@0: TreeView.instanceCounter = 0; michael@0: michael@0: TreeView.prototype = { michael@0: getContainer: function TreeView_getContainer() { michael@0: return this._container; michael@0: }, michael@0: setColumns: function TreeView_setColumns(columns) { michael@0: this._header.innerHTML = ""; michael@0: for (var i = 0; i < columns.length; i++) { michael@0: var li = document.createElement("li"); michael@0: li.className = "treeColumnHeader treeColumnHeader" + i; michael@0: li.id = columns[i].name + "Header"; michael@0: li.textContent = columns[i].title; michael@0: this._header.appendChild(li); michael@0: } michael@0: }, michael@0: dataIsOutdated: function TreeView_dataIsOutdated() { michael@0: this._busyCover.classList.add("busy"); michael@0: }, michael@0: display: function TreeView_display(data, resources, filterByName) { michael@0: this._busyCover.classList.remove("busy"); michael@0: this._filterByName = filterByName; michael@0: this._resources = resources; michael@0: this._addResourceIconStyles(); michael@0: this._filterByNameReg = null; // lazy init michael@0: if (this._filterByName === "") michael@0: this._filterByName = null; michael@0: this._horizontalScrollbox.innerHTML = ""; michael@0: this._horizontalScrollbox.data = data[0].getData(); michael@0: if (this._pendingActionsProcessingCallback) { michael@0: cancelAnimationFrame(this._pendingActionsProcessingCallback); michael@0: this._pendingActionsProcessingCallback = 0; michael@0: } michael@0: this._pendingActions = []; michael@0: michael@0: this._pendingActions.push({ michael@0: parentElement: this._horizontalScrollbox, michael@0: parentNode: null, michael@0: data: data[0].getData() michael@0: }); michael@0: this._processPendingActionsChunk(); michael@0: changeFocus(this._container); michael@0: }, michael@0: // Provide a snapshot of the reverse selection to restore with 'invert callback' michael@0: getReverseSelectionSnapshot: function TreeView__getReverseSelectionSnapshot(isJavascriptOnly) { michael@0: var snapshot = []; michael@0: michael@0: if (!this._selectedNode) { michael@0: return snapshot; michael@0: } michael@0: michael@0: var curr = this._selectedNode.data; michael@0: michael@0: while(curr) { michael@0: if (isJavascriptOnly && curr.isJSFrame || !isJavascriptOnly) { michael@0: snapshot.push(curr.name); michael@0: //dump(JSON.stringify(curr.name) + "\n"); michael@0: } michael@0: if (curr.treeChildren && curr.treeChildren.length >= 1) { michael@0: curr = curr.treeChildren[0].getData(); michael@0: } else { michael@0: break; michael@0: } michael@0: } michael@0: michael@0: return snapshot.reverse(); michael@0: }, michael@0: // Provide a snapshot of the current selection to restore michael@0: getSelectionSnapshot: function TreeView__getSelectionSnapshot(isJavascriptOnly) { michael@0: var snapshot = []; michael@0: var curr = this._selectedNode; michael@0: michael@0: while(curr) { michael@0: if (isJavascriptOnly && curr.data.isJSFrame || !isJavascriptOnly) { michael@0: snapshot.push(curr.data.name); michael@0: //dump(JSON.stringify(curr.data.name) + "\n"); michael@0: } michael@0: curr = curr.treeParent; michael@0: } michael@0: michael@0: return snapshot.reverse(); michael@0: }, michael@0: setSelection: function TreeView_setSelection(frames) { michael@0: this.restoreSelectionSnapshot(frames, false); michael@0: }, michael@0: // Take a selection snapshot and restore the selection michael@0: restoreSelectionSnapshot: function TreeView_restoreSelectionSnapshot(snapshot, allowNonContiguous) { michael@0: var currNode = this._horizontalScrollbox.firstChild; michael@0: if (currNode.data.name == snapshot[0] || snapshot[0] == "(total)") { michael@0: snapshot.shift(); michael@0: } michael@0: //dump("len: " + snapshot.length + "\n"); michael@0: next_level: while (currNode && snapshot.length > 0) { michael@0: this._toggle(currNode, false, true); michael@0: this._syncProcessPendingActionProcessing(); michael@0: for (var i = 0; i < currNode.treeChildren.length; i++) { michael@0: if (currNode.treeChildren[i].data.name == snapshot[0]) { michael@0: snapshot.shift(); michael@0: this._toggle(currNode, false, true); michael@0: currNode = currNode.treeChildren[i]; michael@0: continue next_level; michael@0: } michael@0: } michael@0: if (allowNonContiguous) { michael@0: // We need to do a Breadth-first search to find a match michael@0: var pendingSearch = [currNode.data]; michael@0: while (pendingSearch.length > 0) { michael@0: var node = pendingSearch.shift(); michael@0: if (!node.treeChildren) michael@0: continue; michael@0: for (var i = 0; i < node.treeChildren.length; i++) { michael@0: var childNode = node.treeChildren[i].getData(); michael@0: if (childNode.name == snapshot[0]) { michael@0: //dump("found: " + childNode.name + "\n"); michael@0: snapshot.shift(); michael@0: var nodesToToggle = [childNode]; michael@0: while (nodesToToggle[0].name != currNode.data.name) { michael@0: nodesToToggle.splice(0, 0, nodesToToggle[0].parent); michael@0: } michael@0: var lastToggle = currNode; michael@0: for (var j = 0; j < nodesToToggle.length; j++) { michael@0: for (var k = 0; k < lastToggle.treeChildren.length; k++) { michael@0: if (lastToggle.treeChildren[k].data.name == nodesToToggle[j].name) { michael@0: //dump("Expend: " + nodesToToggle[j].name + "\n"); michael@0: this._toggle(lastToggle.treeChildren[k], false, true); michael@0: lastToggle = lastToggle.treeChildren[k]; michael@0: this._syncProcessPendingActionProcessing(); michael@0: } michael@0: } michael@0: } michael@0: currNode = lastToggle; michael@0: continue next_level; michael@0: } michael@0: //dump("pending: " + childNode.name + "\n"); michael@0: pendingSearch.push(childNode); michael@0: } michael@0: } michael@0: } michael@0: break; // Didn't find child node matching michael@0: } michael@0: michael@0: if (currNode == this._horizontalScrollbox) { michael@0: PROFILERERROR("Failed to restore selection, could not find root.\n"); michael@0: return; michael@0: } michael@0: michael@0: this._toggle(currNode, true, true); michael@0: this._select(currNode); michael@0: }, michael@0: _processPendingActionsChunk: function TreeView__processPendingActionsChunk(isSync) { michael@0: this._pendingActionsProcessingCallback = 0; michael@0: michael@0: var startTime = Date.now(); michael@0: var endTime = startTime + kMaxChunkDuration; michael@0: while ((isSync == true || Date.now() < endTime) && this._pendingActions.length > 0) { michael@0: this._processOneAction(this._pendingActions.shift()); michael@0: } michael@0: this._scrollHeightChanged(); michael@0: michael@0: this._schedulePendingActionProcessing(); michael@0: }, michael@0: _schedulePendingActionProcessing: function TreeView__schedulePendingActionProcessing() { michael@0: if (!this._pendingActionsProcessingCallback && this._pendingActions.length > 0) { michael@0: var self = this; michael@0: this._pendingActionsProcessingCallback = requestAnimationFrame(function () { michael@0: self._processPendingActionsChunk(); michael@0: }); michael@0: } michael@0: }, michael@0: _syncProcessPendingActionProcessing: function TreeView__syncProcessPendingActionProcessing() { michael@0: this._processPendingActionsChunk(true); michael@0: }, michael@0: _processOneAction: function TreeView__processOneAction(action) { michael@0: var li = this._createTree(action.parentElement, action.parentNode, action.data); michael@0: if ("allChildrenCollapsedValue" in action) { michael@0: if (this._abortToggleAll) michael@0: return; michael@0: this._toggleAll(li, action.allChildrenCollapsedValue, true); michael@0: } michael@0: }, michael@0: addEventListener: function TreeView_addEventListener(eventName, callbackFunction) { michael@0: if (!(eventName in this._eventListeners)) michael@0: this._eventListeners[eventName] = []; michael@0: if (this._eventListeners[eventName].indexOf(callbackFunction) != -1) michael@0: return; michael@0: this._eventListeners[eventName].push(callbackFunction); michael@0: }, michael@0: removeEventListener: function TreeView_removeEventListener(eventName, callbackFunction) { michael@0: if (!(eventName in this._eventListeners)) michael@0: return; michael@0: var index = this._eventListeners[eventName].indexOf(callbackFunction); michael@0: if (index == -1) michael@0: return; michael@0: this._eventListeners[eventName].splice(index, 1); michael@0: }, michael@0: _fireEvent: function TreeView__fireEvent(eventName, eventObject) { michael@0: if (!(eventName in this._eventListeners)) michael@0: return; michael@0: this._eventListeners[eventName].forEach(function (callbackFunction) { michael@0: callbackFunction(eventObject); michael@0: }); michael@0: }, michael@0: _setUpScrolling: function TreeView__setUpScrolling() { michael@0: var waitingForPaint = false; michael@0: var accumulatedDeltaX = 0; michael@0: var accumulatedDeltaY = 0; michael@0: var self = this; michael@0: function scrollListener(e) { michael@0: if (!waitingForPaint) { michael@0: requestAnimationFrame(function () { michael@0: self._horizontalScrollbox.scrollLeft += accumulatedDeltaX; michael@0: self._verticalScrollbox.scrollTop += accumulatedDeltaY; michael@0: accumulatedDeltaX = 0; michael@0: accumulatedDeltaY = 0; michael@0: waitingForPaint = false; michael@0: }); michael@0: waitingForPaint = true; michael@0: } michael@0: if (e.axis == e.HORIZONTAL_AXIS) { michael@0: accumulatedDeltaX += e.detail; michael@0: } else { michael@0: accumulatedDeltaY += e.detail; michael@0: } michael@0: e.preventDefault(); michael@0: } michael@0: this._verticalScrollbox.addEventListener("MozMousePixelScroll", scrollListener, false); michael@0: this._verticalScrollbox.cleanUp = function () { michael@0: self._verticalScrollbox.removeEventListener("MozMousePixelScroll", scrollListener, false); michael@0: }; michael@0: }, michael@0: _scrollHeightChanged: function TreeView__scrollHeightChanged() { michael@0: if (!this._pendingScrollHeightChanged) { michael@0: var self = this; michael@0: this._pendingScrollHeightChanged = setTimeout(function() { michael@0: self._leftColumnBackground.style.height = self._horizontalScrollbox.getBoundingClientRect().height + 'px'; michael@0: self._pendingScrollHeightChanged = null; michael@0: }, 0); michael@0: } michael@0: }, michael@0: _createTree: function TreeView__createTree(parentElement, parentNode, data) { michael@0: var div = document.createElement("div"); michael@0: div.className = "treeViewNode collapsed"; michael@0: var hasChildren = ("children" in data) && (data.children.length > 0); michael@0: if (!hasChildren) michael@0: div.classList.add("leaf"); michael@0: var treeLine = document.createElement("div"); michael@0: treeLine.className = "treeLine"; michael@0: treeLine.innerHTML = this._HTMLForFunction(data); michael@0: div.depth = parentNode ? parentNode.depth + 1 : 0; michael@0: div.style.marginLeft = div.depth + "em"; michael@0: // When this item is toggled we will expand its children michael@0: div.pendingExpand = []; michael@0: div.treeLine = treeLine; michael@0: div.data = data; michael@0: // Useful for debugging michael@0: //this.uniqueID = this.uniqueID || 0; michael@0: //div.id = "Node" + this.uniqueID++; michael@0: div.appendChild(treeLine); michael@0: div.treeChildren = []; michael@0: div.treeParent = parentNode; michael@0: if (hasChildren) { michael@0: for (var i = 0; i < data.children.length; ++i) { michael@0: div.pendingExpand.push({parentElement: this._horizontalScrollbox, parentNode: div, data: data.children[i].getData() }); michael@0: } michael@0: } michael@0: if (parentNode) { michael@0: parentNode.treeChildren.push(div); michael@0: } michael@0: if (parentNode != null) { michael@0: var nextTo; michael@0: if (parentNode.treeChildren.length > 1) { michael@0: nextTo = parentNode.treeChildren[parentNode.treeChildren.length-2].nextSibling; michael@0: } else { michael@0: nextTo = parentNode.nextSibling; michael@0: } michael@0: parentElement.insertBefore(div, nextTo); michael@0: } else { michael@0: parentElement.appendChild(div); michael@0: } michael@0: return div; michael@0: }, michael@0: _addResourceIconStyles: function TreeView__addResourceIconStyles() { michael@0: var styles = []; michael@0: for (var resourceName in this._resources) { michael@0: var resource = this._resources[resourceName]; michael@0: if (resource.icon) { michael@0: styles.push('.resourceIcon[data-resource="' + resourceName + '"] { background-image: url("' + resource.icon + '"); }'); michael@0: } michael@0: } michael@0: this._styleElement.textContent = styles.join("\n"); michael@0: }, michael@0: _populateContextMenu: function TreeView__populateContextMenu(event) { michael@0: this._verticalScrollbox.setAttribute("contextmenu", ""); michael@0: michael@0: var target = event.target; michael@0: if (target.classList.contains("expandCollapseButton") || michael@0: target.classList.contains("focusCallstackButton")) michael@0: return; michael@0: michael@0: var li = this._getParentTreeViewNode(target); michael@0: if (!li) michael@0: return; michael@0: michael@0: this._select(li); michael@0: michael@0: this._contextMenu.innerHTML = ""; michael@0: michael@0: var self = this; michael@0: this._contextMenuForFunction(li.data).forEach(function (menuItem) { michael@0: var menuItemNode = document.createElement("menuitem"); michael@0: menuItemNode.onclick = (function (menuItem) { michael@0: return function() { michael@0: self._contextMenuClick(li.data, menuItem); michael@0: }; michael@0: })(menuItem); michael@0: menuItemNode.label = menuItem; michael@0: self._contextMenu.appendChild(menuItemNode); michael@0: }); michael@0: michael@0: this._verticalScrollbox.setAttribute("contextmenu", this._contextMenu.id); michael@0: }, michael@0: _contextMenuClick: function TreeView__contextMenuClick(node, menuItem) { michael@0: this._fireEvent("contextMenuClick", { node: node, menuItem: menuItem }); michael@0: }, michael@0: _contextMenuForFunction: function TreeView__contextMenuForFunction(node) { michael@0: // TODO move me outside tree.js michael@0: var menu = []; michael@0: if (node.library && ( michael@0: node.library.toLowerCase() == "lib_xul" || michael@0: node.library.toLowerCase() == "lib_xul.dll" michael@0: )) { michael@0: menu.push("View Source"); michael@0: } michael@0: if (node.isJSFrame && node.scriptLocation) { michael@0: menu.push("View JS Source"); michael@0: } michael@0: menu.push("Focus Frame"); michael@0: menu.push("Focus Callstack"); michael@0: menu.push("Google Search"); michael@0: menu.push("Plugin View: Pie"); michael@0: menu.push("Plugin View: Tree"); michael@0: return menu; michael@0: }, michael@0: _HTMLForFunction: function TreeView__HTMLForFunction(node) { michael@0: var nodeName = escapeHTML(node.name); michael@0: var resource = this._resources[node.library] || {}; michael@0: var libName = escapeHTML(resource.name || ""); michael@0: if (this._filterByName) { michael@0: if (!this._filterByNameReg) { michael@0: this._filterByName = RegExp.escape(this._filterByName); michael@0: this._filterByNameReg = new RegExp("(" + this._filterByName + ")","gi"); michael@0: } michael@0: nodeName = nodeName.replace(this._filterByNameReg, "$1"); michael@0: libName = libName.replace(this._filterByNameReg, "$1"); michael@0: } michael@0: var samplePercentage; michael@0: if (isNaN(node.ratio)) { michael@0: samplePercentage = ""; michael@0: } else { michael@0: samplePercentage = (100 * node.ratio).toFixed(1) + "%"; michael@0: } michael@0: return ' ' + michael@0: '' + node.counter + ' ' + michael@0: '' + samplePercentage + ' ' + michael@0: '' + node.selfCounter + ' ' + michael@0: ' ' + michael@0: '' + nodeName + '' + michael@0: '' + libName + '' + michael@0: ((nodeName === '(total)' || gHideSourceLinks) ? '' : michael@0: ''); michael@0: }, michael@0: _resolveChildren: function TreeView__resolveChildren(div, childrenCollapsedValue) { michael@0: while (div.pendingExpand != null && div.pendingExpand.length > 0) { michael@0: var pendingExpand = div.pendingExpand.shift(); michael@0: pendingExpand.allChildrenCollapsedValue = childrenCollapsedValue; michael@0: this._pendingActions.push(pendingExpand); michael@0: this._schedulePendingActionProcessing(); michael@0: } michael@0: }, michael@0: _showChild: function TreeView__showChild(div, isVisible) { michael@0: for (var i = 0; i < div.treeChildren.length; i++) { michael@0: div.treeChildren[i].style.display = isVisible?"":"none"; michael@0: if (!isVisible) { michael@0: div.treeChildren[i].classList.add("collapsed"); michael@0: this._showChild(div.treeChildren[i], isVisible); michael@0: } michael@0: } michael@0: }, michael@0: _toggle: function TreeView__toggle(div, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) { michael@0: var currentCollapsedValue = this._isCollapsed(div); michael@0: if (newCollapsedValue === undefined) michael@0: newCollapsedValue = !currentCollapsedValue; michael@0: if (newCollapsedValue) { michael@0: div.classList.add("collapsed"); michael@0: this._showChild(div, false); michael@0: } else { michael@0: this._resolveChildren(div, true); michael@0: div.classList.remove("collapsed"); michael@0: this._showChild(div, true); michael@0: } michael@0: if (!suppressScrollHeightNotification) michael@0: this._scrollHeightChanged(); michael@0: }, michael@0: _toggleAll: function TreeView__toggleAll(subtreeRoot, /* optional */ newCollapsedValue, /* optional */ suppressScrollHeightNotification) { michael@0: michael@0: // Reset abort michael@0: this._abortToggleAll = false; michael@0: michael@0: // Expands / collapses all child nodes, too. michael@0: michael@0: if (newCollapsedValue === undefined) michael@0: newCollapsedValue = !this._isCollapsed(subtreeRoot); michael@0: if (!newCollapsedValue) { michael@0: // expanding michael@0: this._resolveChildren(subtreeRoot, newCollapsedValue); michael@0: } michael@0: this._toggle(subtreeRoot, newCollapsedValue, true); michael@0: for (var i = 0; i < subtreeRoot.treeChildren.length; ++i) { michael@0: this._toggleAll(subtreeRoot.treeChildren[i], newCollapsedValue, true); michael@0: } michael@0: if (!suppressScrollHeightNotification) michael@0: this._scrollHeightChanged(); michael@0: }, michael@0: _getParent: function TreeView__getParent(div) { michael@0: return div.treeParent; michael@0: }, michael@0: _getFirstChild: function TreeView__getFirstChild(div) { michael@0: if (this._isCollapsed(div)) michael@0: return null; michael@0: var child = div.treeChildren[0]; michael@0: return child; michael@0: }, michael@0: _getLastChild: function TreeView__getLastChild(div) { michael@0: if (this._isCollapsed(div)) michael@0: return div; michael@0: var lastChild = div.treeChildren[div.treeChildren.length-1]; michael@0: if (lastChild == null) michael@0: return div; michael@0: return this._getLastChild(lastChild); michael@0: }, michael@0: _getPrevSib: function TreeView__getPevSib(div) { michael@0: if (div.treeParent == null) michael@0: return null; michael@0: var nodeIndex = div.treeParent.treeChildren.indexOf(div); michael@0: if (nodeIndex == 0) michael@0: return null; michael@0: return div.treeParent.treeChildren[nodeIndex-1]; michael@0: }, michael@0: _getNextSib: function TreeView__getNextSib(div) { michael@0: if (div.treeParent == null) michael@0: return null; michael@0: var nodeIndex = div.treeParent.treeChildren.indexOf(div); michael@0: if (nodeIndex == div.treeParent.treeChildren.length - 1) michael@0: return this._getNextSib(div.treeParent); michael@0: return div.treeParent.treeChildren[nodeIndex+1]; michael@0: }, michael@0: _scheduleScrollIntoView: function TreeView__scheduleScrollIntoView(element, maxImportantWidth) { michael@0: // Schedule this on the animation frame otherwise we may run this more then once per frames michael@0: // causing more work then needed. michael@0: var self = this; michael@0: if (self._pendingAnimationFrame != null) { michael@0: return; michael@0: } michael@0: self._pendingAnimationFrame = requestAnimationFrame(function anim_frame() { michael@0: cancelAnimationFrame(self._pendingAnimationFrame); michael@0: self._pendingAnimationFrame = null; michael@0: self._scrollIntoView(element, maxImportantWidth); michael@0: }); michael@0: }, michael@0: _scrollIntoView: function TreeView__scrollIntoView(element, maxImportantWidth) { michael@0: // Make sure that element is inside the visible part of our scrollbox by michael@0: // adjusting the scroll positions. If element is wider or michael@0: // higher than the scroll port, the left and top edges are prioritized over michael@0: // the right and bottom edges. michael@0: // If maxImportantWidth is set, parts of the beyond this widths are michael@0: // considered as not important; they'll not be moved into view. michael@0: michael@0: if (maxImportantWidth === undefined) michael@0: maxImportantWidth = Infinity; michael@0: michael@0: var visibleRect = { michael@0: left: this._horizontalScrollbox.getBoundingClientRect().left + 150, // TODO: un-hardcode 150 michael@0: top: this._verticalScrollbox.getBoundingClientRect().top, michael@0: right: this._horizontalScrollbox.getBoundingClientRect().right, michael@0: bottom: this._verticalScrollbox.getBoundingClientRect().bottom michael@0: } michael@0: var r = element.getBoundingClientRect(); michael@0: var right = Math.min(r.right, r.left + maxImportantWidth); michael@0: var leftCutoff = visibleRect.left - r.left; michael@0: var rightCutoff = right - visibleRect.right; michael@0: var topCutoff = visibleRect.top - r.top; michael@0: var bottomCutoff = r.bottom - visibleRect.bottom; michael@0: if (leftCutoff > 0) michael@0: this._horizontalScrollbox.scrollLeft -= leftCutoff; michael@0: else if (rightCutoff > 0) michael@0: this._horizontalScrollbox.scrollLeft += Math.min(rightCutoff, -leftCutoff); michael@0: if (topCutoff > 0) michael@0: this._verticalScrollbox.scrollTop -= topCutoff; michael@0: else if (bottomCutoff > 0) michael@0: this._verticalScrollbox.scrollTop += Math.min(bottomCutoff, -topCutoff); michael@0: }, michael@0: _select: function TreeView__select(li) { michael@0: if (this._selectedNode != null) { michael@0: this._selectedNode.treeLine.classList.remove("selected"); michael@0: this._selectedNode = null; michael@0: } michael@0: if (li) { michael@0: li.treeLine.classList.add("selected"); michael@0: this._selectedNode = li; michael@0: var functionName = li.treeLine.querySelector(".functionName"); michael@0: this._scheduleScrollIntoView(functionName, 400); michael@0: this._fireEvent("select", li.data); michael@0: } michael@0: updateDocumentURL(); michael@0: }, michael@0: _isCollapsed: function TreeView__isCollapsed(div) { michael@0: return div.classList.contains("collapsed"); michael@0: }, michael@0: _getParentTreeViewNode: function TreeView__getParentTreeViewNode(node) { michael@0: while (node) { michael@0: if (node.nodeType != node.ELEMENT_NODE) michael@0: break; michael@0: if (node.classList.contains("treeViewNode")) michael@0: return node; michael@0: node = node.parentNode; michael@0: } michael@0: return null; michael@0: }, michael@0: _onclick: function TreeView__onclick(event) { michael@0: var target = event.target; michael@0: var node = this._getParentTreeViewNode(target); michael@0: if (!node) michael@0: return; michael@0: if (target.classList.contains("expandCollapseButton")) { michael@0: if (event.altKey) michael@0: this._toggleAll(node); michael@0: else michael@0: this._toggle(node); michael@0: } else if (target.classList.contains("focusCallstackButton")) { michael@0: this._fireEvent("focusCallstackButtonClicked", node.data); michael@0: } else { michael@0: this._select(node); michael@0: if (event.detail == 2) // dblclick michael@0: this._toggle(node); michael@0: } michael@0: }, michael@0: _onkeypress: function TreeView__onkeypress(event) { michael@0: if (event.ctrlKey || event.altKey || event.metaKey) michael@0: return; michael@0: michael@0: this._abortToggleAll = true; michael@0: michael@0: var selected = this._selectedNode; michael@0: if (event.keyCode < 37 || event.keyCode > 40) { michael@0: if (event.keyCode != 0 || michael@0: String.fromCharCode(event.charCode) != '*') { michael@0: return; michael@0: } michael@0: } michael@0: event.stopPropagation(); michael@0: event.preventDefault(); michael@0: if (!selected) michael@0: return; michael@0: if (event.keyCode == 37) { // KEY_LEFT michael@0: var isCollapsed = this._isCollapsed(selected); michael@0: if (!isCollapsed) { michael@0: this._toggle(selected); michael@0: } else { michael@0: var parent = this._getParent(selected); michael@0: if (parent != null) { michael@0: this._select(parent); michael@0: } michael@0: } michael@0: } else if (event.keyCode == 38) { // KEY_UP michael@0: var prevSib = this._getPrevSib(selected); michael@0: var parent = this._getParent(selected); michael@0: if (prevSib != null) { michael@0: this._select(this._getLastChild(prevSib)); michael@0: } else if (parent != null) { michael@0: this._select(parent); michael@0: } michael@0: } else if (event.keyCode == 39) { // KEY_RIGHT michael@0: var isCollapsed = this._isCollapsed(selected); michael@0: if (isCollapsed) { michael@0: this._toggle(selected); michael@0: this._syncProcessPendingActionProcessing(); michael@0: } else { michael@0: // Do KEY_DOWN michael@0: var nextSib = this._getNextSib(selected); michael@0: var child = this._getFirstChild(selected); michael@0: if (child != null) { michael@0: this._select(child); michael@0: } else if (nextSib) { michael@0: this._select(nextSib); michael@0: } michael@0: } michael@0: } else if (event.keyCode == 40) { // KEY_DOWN michael@0: var nextSib = this._getNextSib(selected); michael@0: var child = this._getFirstChild(selected); michael@0: if (child != null) { michael@0: this._select(child); michael@0: } else if (nextSib) { michael@0: this._select(nextSib); michael@0: } michael@0: } else if (String.fromCharCode(event.charCode) == '*') { michael@0: this._toggleAll(selected); michael@0: } michael@0: }, michael@0: }; michael@0: