| /* | |
| * Copyright (C) 2012 Google Inc. All rights reserved. | |
| * | |
| * Redistribution and use in source and binary forms, with or without | |
| * modification, are permitted provided that the following conditions are | |
| * met: | |
| * | |
| * 1. Redistributions of source code must retain the above copyright | |
| * notice, this list of conditions and the following disclaimer. | |
| * | |
| * 2. Redistributions in binary form must reproduce the above | |
| * copyright notice, this list of conditions and the following disclaimer | |
| * in the documentation and/or other materials provided with the | |
| * distribution. | |
| * | |
| * THIS SOFTWARE IS PROVIDED BY GOOGLE INC. AND ITS CONTRIBUTORS | |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GOOGLE INC. | |
| * OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| */ | |
| /** | |
| * @implements {SDK.TargetManager.Observer} | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorView = class extends UI.VBox { | |
| constructor() { | |
| super(true); | |
| this.registerRequiredCSS('sources/navigatorView.css'); | |
| /** @type {?UI.Widget} */ | |
| this._placeholder = null; | |
| this._scriptsTree = new UI.TreeOutlineInShadow(); | |
| this._scriptsTree.registerRequiredCSS('sources/navigatorTree.css'); | |
| this._scriptsTree.setComparator(Sources.NavigatorView._treeElementsCompare); | |
| this.contentElement.appendChild(this._scriptsTree.element); | |
| this.setDefaultFocusedElement(this._scriptsTree.element); | |
| /** @type {!Platform.Multimap<!Workspace.UISourceCode, !Sources.NavigatorUISourceCodeTreeNode>} */ | |
| this._uiSourceCodeNodes = new Platform.Multimap(); | |
| /** @type {!Map.<string, !Sources.NavigatorFolderTreeNode>} */ | |
| this._subfolderNodes = new Map(); | |
| this._rootNode = new Sources.NavigatorRootTreeNode(this); | |
| this._rootNode.populate(); | |
| /** @type {!Map.<!SDK.ResourceTreeFrame, !Sources.NavigatorGroupTreeNode>} */ | |
| this._frameNodes = new Map(); | |
| this.contentElement.addEventListener('contextmenu', this.handleContextMenu.bind(this), false); | |
| UI.shortcutRegistry.addShortcutListener( | |
| this.contentElement, 'sources.rename', this._renameShortcut.bind(this), true); | |
| this._navigatorGroupByFolderSetting = Common.moduleSetting('navigatorGroupByFolder'); | |
| this._navigatorGroupByFolderSetting.addChangeListener(this._groupingChanged.bind(this)); | |
| this._initGrouping(); | |
| Persistence.persistence.addEventListener( | |
| Persistence.Persistence.Events.BindingCreated, this._onBindingChanged, this); | |
| Persistence.persistence.addEventListener( | |
| Persistence.Persistence.Events.BindingRemoved, this._onBindingChanged, this); | |
| SDK.targetManager.addEventListener(SDK.TargetManager.Events.NameChanged, this._targetNameChanged, this); | |
| SDK.targetManager.observeTargets(this); | |
| this._resetWorkspace(Workspace.workspace); | |
| this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); | |
| Bindings.networkProjectManager.addEventListener( | |
| Bindings.NetworkProjectManager.Events.FrameAttributionAdded, this._frameAttributionAdded, this); | |
| Bindings.networkProjectManager.addEventListener( | |
| Bindings.NetworkProjectManager.Events.FrameAttributionRemoved, this._frameAttributionRemoved, this); | |
| } | |
| /** | |
| * @param {!UI.TreeElement} treeElement | |
| */ | |
| static _treeElementOrder(treeElement) { | |
| if (treeElement._boostOrder) { | |
| return 0; | |
| } | |
| if (!Sources.NavigatorView._typeOrders) { | |
| const weights = {}; | |
| const types = Sources.NavigatorView.Types; | |
| weights[types.Root] = 1; | |
| weights[types.Domain] = 10; | |
| weights[types.FileSystemFolder] = 1; | |
| weights[types.NetworkFolder] = 1; | |
| weights[types.SourceMapFolder] = 2; | |
| weights[types.File] = 10; | |
| weights[types.Frame] = 70; | |
| weights[types.Worker] = 90; | |
| weights[types.FileSystem] = 100; | |
| Sources.NavigatorView._typeOrders = weights; | |
| } | |
| let order = Sources.NavigatorView._typeOrders[treeElement._nodeType]; | |
| if (treeElement._uiSourceCode) { | |
| const contentType = treeElement._uiSourceCode.contentType(); | |
| if (contentType.isDocument()) { | |
| order += 3; | |
| } else if (contentType.isScript()) { | |
| order += 5; | |
| } else if (contentType.isStyleSheet()) { | |
| order += 10; | |
| } else { | |
| order += 15; | |
| } | |
| } | |
| return order; | |
| } | |
| /** | |
| * @param {!UI.ContextMenu} contextMenu | |
| * @param {string=} path | |
| */ | |
| static appendSearchItem(contextMenu, path) { | |
| function searchPath() { | |
| Sources.SearchSourcesView.openSearch(`file:${path.trim()}`); | |
| } | |
| let searchLabel = Common.UIString('Search in folder'); | |
| if (!path || !path.trim()) { | |
| path = '*'; | |
| searchLabel = Common.UIString('Search in all files'); | |
| } | |
| contextMenu.viewSection().appendItem(searchLabel, searchPath); | |
| } | |
| /** | |
| * @param {!UI.TreeElement} treeElement1 | |
| * @param {!UI.TreeElement} treeElement2 | |
| * @return {number} | |
| */ | |
| static _treeElementsCompare(treeElement1, treeElement2) { | |
| const typeWeight1 = Sources.NavigatorView._treeElementOrder(treeElement1); | |
| const typeWeight2 = Sources.NavigatorView._treeElementOrder(treeElement2); | |
| if (typeWeight1 > typeWeight2) { | |
| return 1; | |
| } | |
| if (typeWeight1 < typeWeight2) { | |
| return -1; | |
| } | |
| return treeElement1.titleAsText().compareTo(treeElement2.titleAsText()); | |
| } | |
| /** | |
| * @param {!UI.Widget} placeholder | |
| */ | |
| setPlaceholder(placeholder) { | |
| console.assert(!this._placeholder, 'A placeholder widget was already set'); | |
| this._placeholder = placeholder; | |
| placeholder.show(this.contentElement, this.contentElement.firstChild); | |
| updateVisibility.call(this); | |
| this._scriptsTree.addEventListener(UI.TreeOutline.Events.ElementAttached, updateVisibility.bind(this)); | |
| this._scriptsTree.addEventListener(UI.TreeOutline.Events.ElementsDetached, updateVisibility.bind(this)); | |
| /** | |
| * @this {!Sources.NavigatorView} | |
| */ | |
| function updateVisibility() { | |
| const showTree = this._scriptsTree.firstChild(); | |
| if (showTree) { | |
| placeholder.hideWidget(); | |
| } else { | |
| placeholder.showWidget(); | |
| } | |
| this._scriptsTree.element.classList.toggle('hidden', !showTree); | |
| } | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _onBindingChanged(event) { | |
| const binding = /** @type {!Persistence.PersistenceBinding} */ (event.data); | |
| // Update UISourceCode titles. | |
| const networkNodes = this._uiSourceCodeNodes.get(binding.network); | |
| for (const networkNode of networkNodes) { | |
| networkNode.updateTitle(); | |
| } | |
| const fileSystemNodes = this._uiSourceCodeNodes.get(binding.fileSystem); | |
| for (const fileSystemNode of fileSystemNodes) { | |
| fileSystemNode.updateTitle(); | |
| } | |
| // Update folder titles. | |
| const pathTokens = Persistence.FileSystemWorkspaceBinding.relativePath(binding.fileSystem); | |
| let folderPath = ''; | |
| for (let i = 0; i < pathTokens.length - 1; ++i) { | |
| folderPath += pathTokens[i]; | |
| const folderId = | |
| this._folderNodeId(binding.fileSystem.project(), null, null, binding.fileSystem.origin(), folderPath); | |
| const folderNode = this._subfolderNodes.get(folderId); | |
| if (folderNode) { | |
| folderNode.updateTitle(); | |
| } | |
| folderPath += '/'; | |
| } | |
| // Update fileSystem root title. | |
| const fileSystemRoot = this._rootNode.child(binding.fileSystem.project().id()); | |
| if (fileSystemRoot) { | |
| fileSystemRoot.updateTitle(); | |
| } | |
| } | |
| /** | |
| * @override | |
| */ | |
| focus() { | |
| this._scriptsTree.focus(); | |
| } | |
| /** | |
| * @param {!Workspace.Workspace} workspace | |
| */ | |
| _resetWorkspace(workspace) { | |
| this._workspace = workspace; | |
| this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._uiSourceCodeAdded, this); | |
| this._workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeRemoved, this._uiSourceCodeRemoved, this); | |
| this._workspace.addEventListener(Workspace.Workspace.Events.ProjectAdded, event => { | |
| const project = /** @type {!Workspace.Project} */ (event.data); | |
| this._projectAdded(project); | |
| if (project.type() === Workspace.projectTypes.FileSystem) { | |
| this._computeUniqueFileSystemProjectNames(); | |
| } | |
| }); | |
| this._workspace.addEventListener(Workspace.Workspace.Events.ProjectRemoved, event => { | |
| const project = /** @type {!Workspace.Project} */ (event.data); | |
| this._removeProject(project); | |
| if (project.type() === Workspace.projectTypes.FileSystem) { | |
| this._computeUniqueFileSystemProjectNames(); | |
| } | |
| }); | |
| this._workspace.projects().forEach(this._projectAdded.bind(this)); | |
| this._computeUniqueFileSystemProjectNames(); | |
| } | |
| /** | |
| * @return {!Workspace.Workspace} | |
| * @protected | |
| */ | |
| workspace() { | |
| return this._workspace; | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @return {boolean} | |
| */ | |
| acceptProject(project) { | |
| return !project.isServiceProject(); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _frameAttributionAdded(event) { | |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); | |
| if (!this._acceptsUISourceCode(uiSourceCode)) { | |
| return; | |
| } | |
| const addedFrame = /** @type {?SDK.ResourceTreeFrame} */ (event.data.frame); | |
| // This event does not happen for UISourceCodes without initial attribution. | |
| this._addUISourceCodeNode(uiSourceCode, addedFrame); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _frameAttributionRemoved(event) { | |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data.uiSourceCode); | |
| if (!this._acceptsUISourceCode(uiSourceCode)) { | |
| return; | |
| } | |
| const removedFrame = /** @type {?SDK.ResourceTreeFrame} */ (event.data.frame); | |
| const node = Array.from(this._uiSourceCodeNodes.get(uiSourceCode)).find(node => node.frame() === removedFrame); | |
| this._removeUISourceCodeNode(node); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @return {boolean} | |
| */ | |
| _acceptsUISourceCode(uiSourceCode) { | |
| return this.acceptProject(uiSourceCode.project()); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| */ | |
| _addUISourceCode(uiSourceCode) { | |
| if (!this._acceptsUISourceCode(uiSourceCode)) { | |
| return; | |
| } | |
| const frames = Bindings.NetworkProject.framesForUISourceCode(uiSourceCode); | |
| if (frames.length) { | |
| for (const frame of frames) { | |
| this._addUISourceCodeNode(uiSourceCode, frame); | |
| } | |
| } else { | |
| this._addUISourceCodeNode(uiSourceCode, null); | |
| } | |
| this.uiSourceCodeAdded(uiSourceCode); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {?SDK.ResourceTreeFrame} frame | |
| */ | |
| _addUISourceCodeNode(uiSourceCode, frame) { | |
| const isFromSourceMap = uiSourceCode.contentType().isFromSourceMap(); | |
| let path; | |
| if (uiSourceCode.project().type() === Workspace.projectTypes.FileSystem) { | |
| path = Persistence.FileSystemWorkspaceBinding.relativePath(uiSourceCode).slice(0, -1); | |
| } else { | |
| path = Common.ParsedURL.extractPath(uiSourceCode.url()).split('/').slice(1, -1); | |
| } | |
| const project = uiSourceCode.project(); | |
| const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode); | |
| const folderNode = | |
| this._folderNode(uiSourceCode, project, target, frame, uiSourceCode.origin(), path, isFromSourceMap); | |
| const uiSourceCodeNode = new Sources.NavigatorUISourceCodeTreeNode(this, uiSourceCode, frame); | |
| folderNode.appendChild(uiSourceCodeNode); | |
| this._uiSourceCodeNodes.set(uiSourceCode, uiSourceCodeNode); | |
| this._selectDefaultTreeNode(); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| */ | |
| uiSourceCodeAdded(uiSourceCode) { | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _uiSourceCodeAdded(event) { | |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); | |
| this._addUISourceCode(uiSourceCode); | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _uiSourceCodeRemoved(event) { | |
| const uiSourceCode = /** @type {!Workspace.UISourceCode} */ (event.data); | |
| this._removeUISourceCode(uiSourceCode); | |
| } | |
| /** | |
| * @protected | |
| * @param {!Workspace.Project} project | |
| */ | |
| tryAddProject(project) { | |
| this._projectAdded(project); | |
| project.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| */ | |
| _projectAdded(project) { | |
| if (!this.acceptProject(project) || project.type() !== Workspace.projectTypes.FileSystem || | |
| Snippets.isSnippetsProject(project) || this._rootNode.child(project.id())) { | |
| return; | |
| } | |
| this._rootNode.appendChild(new Sources.NavigatorGroupTreeNode( | |
| this, project, project.id(), Sources.NavigatorView.Types.FileSystem, project.displayName())); | |
| this._selectDefaultTreeNode(); | |
| } | |
| // TODO(einbinder) remove this code after crbug.com/964075 is fixed | |
| _selectDefaultTreeNode() { | |
| const children = this._rootNode.children(); | |
| if (children.length && !this._scriptsTree.selectedTreeElement) { | |
| children[0].treeNode().select(true /* omitFocus */, false /* selectedByUser */); | |
| } | |
| } | |
| _computeUniqueFileSystemProjectNames() { | |
| const fileSystemProjects = this._workspace.projectsForType(Workspace.projectTypes.FileSystem); | |
| if (!fileSystemProjects.length) { | |
| return; | |
| } | |
| const encoder = new Persistence.PathEncoder(); | |
| const reversedPaths = fileSystemProjects.map(project => { | |
| const fileSystem = /** @type {!Persistence.FileSystemWorkspaceBinding.FileSystem} */ (project); | |
| return encoder.encode(fileSystem.fileSystemPath()).reverse(); | |
| }); | |
| const reversedIndex = new Common.Trie(); | |
| for (const reversedPath of reversedPaths) { | |
| reversedIndex.add(reversedPath); | |
| } | |
| for (let i = 0; i < fileSystemProjects.length; ++i) { | |
| const reversedPath = reversedPaths[i]; | |
| const project = fileSystemProjects[i]; | |
| reversedIndex.remove(reversedPath); | |
| const commonPrefix = reversedIndex.longestPrefix(reversedPath, false /* fullWordOnly */); | |
| reversedIndex.add(reversedPath); | |
| const path = encoder.decode(reversedPath.substring(0, commonPrefix.length + 1).reverse()); | |
| const fileSystemNode = this._rootNode.child(project.id()); | |
| if (fileSystemNode) { | |
| fileSystemNode.setTitle(path); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| */ | |
| _removeProject(project) { | |
| const uiSourceCodes = project.uiSourceCodes(); | |
| for (let i = 0; i < uiSourceCodes.length; ++i) { | |
| this._removeUISourceCode(uiSourceCodes[i]); | |
| } | |
| if (project.type() !== Workspace.projectTypes.FileSystem) { | |
| return; | |
| } | |
| const fileSystemNode = this._rootNode.child(project.id()); | |
| if (!fileSystemNode) { | |
| return; | |
| } | |
| this._rootNode.removeChild(fileSystemNode); | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @param {?SDK.Target} target | |
| * @param {?SDK.ResourceTreeFrame} frame | |
| * @param {string} projectOrigin | |
| * @param {string} path | |
| * @return {string} | |
| */ | |
| _folderNodeId(project, target, frame, projectOrigin, path) { | |
| const targetId = target ? target.id() : ''; | |
| const projectId = project.type() === Workspace.projectTypes.FileSystem ? project.id() : ''; | |
| const frameId = this._groupByFrame && frame ? frame.id : ''; | |
| return targetId + ':' + projectId + ':' + frameId + ':' + projectOrigin + ':' + path; | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {!Workspace.Project} project | |
| * @param {?SDK.Target} target | |
| * @param {?SDK.ResourceTreeFrame} frame | |
| * @param {string} projectOrigin | |
| * @param {!Array<string>} path | |
| * @param {boolean} fromSourceMap | |
| * @return {!Sources.NavigatorTreeNode} | |
| */ | |
| _folderNode(uiSourceCode, project, target, frame, projectOrigin, path, fromSourceMap) { | |
| if (Snippets.isSnippetsUISourceCode(uiSourceCode)) { | |
| return this._rootNode; | |
| } | |
| if (target && !this._groupByFolder && !fromSourceMap) { | |
| return this._domainNode(uiSourceCode, project, target, frame, projectOrigin); | |
| } | |
| const folderPath = path.join('/'); | |
| const folderId = this._folderNodeId(project, target, frame, projectOrigin, folderPath); | |
| let folderNode = this._subfolderNodes.get(folderId); | |
| if (folderNode) { | |
| return folderNode; | |
| } | |
| if (!path.length) { | |
| if (target) { | |
| return this._domainNode(uiSourceCode, project, target, frame, projectOrigin); | |
| } | |
| return /** @type {!Sources.NavigatorTreeNode} */ (this._rootNode.child(project.id())); | |
| } | |
| const parentNode = | |
| this._folderNode(uiSourceCode, project, target, frame, projectOrigin, path.slice(0, -1), fromSourceMap); | |
| let type = fromSourceMap ? Sources.NavigatorView.Types.SourceMapFolder : Sources.NavigatorView.Types.NetworkFolder; | |
| if (project.type() === Workspace.projectTypes.FileSystem) { | |
| type = Sources.NavigatorView.Types.FileSystemFolder; | |
| } | |
| const name = path[path.length - 1]; | |
| folderNode = new Sources.NavigatorFolderTreeNode(this, project, folderId, type, folderPath, name); | |
| this._subfolderNodes.set(folderId, folderNode); | |
| parentNode.appendChild(folderNode); | |
| return folderNode; | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {!Workspace.Project} project | |
| * @param {!SDK.Target} target | |
| * @param {?SDK.ResourceTreeFrame} frame | |
| * @param {string} projectOrigin | |
| * @return {!Sources.NavigatorTreeNode} | |
| */ | |
| _domainNode(uiSourceCode, project, target, frame, projectOrigin) { | |
| const frameNode = this._frameNode(project, target, frame); | |
| if (!this._groupByDomain) { | |
| return frameNode; | |
| } | |
| let domainNode = frameNode.child(projectOrigin); | |
| if (domainNode) { | |
| return domainNode; | |
| } | |
| domainNode = new Sources.NavigatorGroupTreeNode( | |
| this, project, projectOrigin, Sources.NavigatorView.Types.Domain, | |
| this._computeProjectDisplayName(target, projectOrigin)); | |
| if (frame && projectOrigin === Common.ParsedURL.extractOrigin(frame.url)) { | |
| domainNode.treeNode()._boostOrder = true; | |
| } | |
| frameNode.appendChild(domainNode); | |
| return domainNode; | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @param {!SDK.Target} target | |
| * @param {?SDK.ResourceTreeFrame} frame | |
| * @return {!Sources.NavigatorTreeNode} | |
| */ | |
| _frameNode(project, target, frame) { | |
| if (!this._groupByFrame || !frame) { | |
| return this._targetNode(project, target); | |
| } | |
| let frameNode = this._frameNodes.get(frame); | |
| if (frameNode) { | |
| return frameNode; | |
| } | |
| frameNode = new Sources.NavigatorGroupTreeNode( | |
| this, project, target.id() + ':' + frame.id, Sources.NavigatorView.Types.Frame, frame.displayName()); | |
| frameNode.setHoverCallback(hoverCallback); | |
| this._frameNodes.set(frame, frameNode); | |
| const parentFrame = frame.parentFrame || frame.crossTargetParentFrame(); | |
| this._frameNode(project, parentFrame ? parentFrame.resourceTreeModel().target() : target, parentFrame) | |
| .appendChild(frameNode); | |
| if (!parentFrame) { | |
| frameNode.treeNode()._boostOrder = true; | |
| frameNode.treeNode().expand(); | |
| } | |
| /** | |
| * @param {boolean} hovered | |
| */ | |
| function hoverCallback(hovered) { | |
| if (hovered) { | |
| const overlayModel = target.model(SDK.OverlayModel); | |
| if (overlayModel) { | |
| overlayModel.highlightFrame(frame.id); | |
| } | |
| } else { | |
| SDK.OverlayModel.hideDOMNodeHighlight(); | |
| } | |
| } | |
| return frameNode; | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @param {!SDK.Target} target | |
| * @return {!Sources.NavigatorTreeNode} | |
| */ | |
| _targetNode(project, target) { | |
| if (target === SDK.targetManager.mainTarget()) { | |
| return this._rootNode; | |
| } | |
| let targetNode = this._rootNode.child('target:' + target.id()); | |
| if (!targetNode) { | |
| targetNode = new Sources.NavigatorGroupTreeNode( | |
| this, project, 'target:' + target.id(), | |
| target.type() === SDK.Target.Type.Frame ? Sources.NavigatorView.Types.Frame : | |
| Sources.NavigatorView.Types.Worker, | |
| target.name()); | |
| this._rootNode.appendChild(targetNode); | |
| } | |
| return targetNode; | |
| } | |
| /** | |
| * @param {!SDK.Target} target | |
| * @param {string} projectOrigin | |
| * @return {string} | |
| */ | |
| _computeProjectDisplayName(target, projectOrigin) { | |
| const runtimeModel = target.model(SDK.RuntimeModel); | |
| const executionContexts = runtimeModel ? runtimeModel.executionContexts() : []; | |
| for (const context of executionContexts) { | |
| if (context.name && context.origin && projectOrigin.startsWith(context.origin)) { | |
| return context.name; | |
| } | |
| } | |
| if (!projectOrigin) { | |
| return Common.UIString('(no domain)'); | |
| } | |
| const parsedURL = new Common.ParsedURL(projectOrigin); | |
| const prettyURL = parsedURL.isValid ? parsedURL.host + (parsedURL.port ? (':' + parsedURL.port) : '') : ''; | |
| return (prettyURL || projectOrigin); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {boolean=} select | |
| * @return {?Sources.NavigatorUISourceCodeTreeNode} | |
| */ | |
| revealUISourceCode(uiSourceCode, select) { | |
| const nodes = this._uiSourceCodeNodes.get(uiSourceCode); | |
| const node = nodes.firstValue(); | |
| if (!node) { | |
| return null; | |
| } | |
| if (this._scriptsTree.selectedTreeElement) { | |
| this._scriptsTree.selectedTreeElement.deselect(); | |
| } | |
| this._lastSelectedUISourceCode = uiSourceCode; | |
| // TODO(dgozman): figure out revealing multiple. | |
| node.reveal(select); | |
| return node; | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {boolean} focusSource | |
| */ | |
| _sourceSelected(uiSourceCode, focusSource) { | |
| this._lastSelectedUISourceCode = uiSourceCode; | |
| Common.Revealer.reveal(uiSourceCode, !focusSource); | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| */ | |
| _removeUISourceCode(uiSourceCode) { | |
| const nodes = this._uiSourceCodeNodes.get(uiSourceCode); | |
| for (const node of nodes) { | |
| this._removeUISourceCodeNode(node); | |
| } | |
| } | |
| /** | |
| * @param {!Sources.NavigatorUISourceCodeTreeNode} node | |
| */ | |
| _removeUISourceCodeNode(node) { | |
| const uiSourceCode = node.uiSourceCode(); | |
| this._uiSourceCodeNodes.delete(uiSourceCode, node); | |
| const project = uiSourceCode.project(); | |
| const target = Bindings.NetworkProject.targetForUISourceCode(uiSourceCode); | |
| const frame = node.frame(); | |
| let parentNode = node.parent; | |
| parentNode.removeChild(node); | |
| node = parentNode; | |
| while (node) { | |
| parentNode = node.parent; | |
| if (!parentNode || !node.isEmpty()) { | |
| break; | |
| } | |
| if (parentNode === this._rootNode && project.type() === Workspace.projectTypes.FileSystem) { | |
| break; | |
| } | |
| if (!(node instanceof Sources.NavigatorGroupTreeNode || node instanceof Sources.NavigatorFolderTreeNode)) { | |
| break; | |
| } | |
| if (node._type === Sources.NavigatorView.Types.Frame) { | |
| this._discardFrame(/** @type {!SDK.ResourceTreeFrame} */ (frame)); | |
| break; | |
| } | |
| const folderId = this._folderNodeId(project, target, frame, uiSourceCode.origin(), node._folderPath); | |
| this._subfolderNodes.delete(folderId); | |
| parentNode.removeChild(node); | |
| node = parentNode; | |
| } | |
| } | |
| reset() { | |
| for (const node of this._uiSourceCodeNodes.valuesArray()) { | |
| node.dispose(); | |
| } | |
| this._scriptsTree.removeChildren(); | |
| this._uiSourceCodeNodes.clear(); | |
| this._subfolderNodes.clear(); | |
| this._frameNodes.clear(); | |
| this._rootNode.reset(); | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| handleContextMenu(event) { | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| _renameShortcut() { | |
| const node = this._scriptsTree.selectedTreeElement && this._scriptsTree.selectedTreeElement._node; | |
| if (!node || !node._uiSourceCode || !node._uiSourceCode.canRename()) { | |
| return false; | |
| } | |
| this.rename(node, false); | |
| return true; | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @param {string} path | |
| * @param {!Workspace.UISourceCode=} uiSourceCode | |
| */ | |
| _handleContextMenuCreate(project, path, uiSourceCode) { | |
| if (uiSourceCode) { | |
| const relativePath = Persistence.FileSystemWorkspaceBinding.relativePath(uiSourceCode); | |
| relativePath.pop(); | |
| path = relativePath.join('/'); | |
| } | |
| this.create(project, path, uiSourceCode); | |
| } | |
| /** | |
| * @param {!Sources.NavigatorUISourceCodeTreeNode} node | |
| */ | |
| _handleContextMenuRename(node) { | |
| this.rename(node, false); | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @param {string} path | |
| */ | |
| _handleContextMenuExclude(project, path) { | |
| const shouldExclude = window.confirm(Common.UIString('Are you sure you want to exclude this folder?')); | |
| if (shouldExclude) { | |
| UI.startBatchUpdate(); | |
| project.excludeFolder(Persistence.FileSystemWorkspaceBinding.completeURL(project, path)); | |
| UI.endBatchUpdate(); | |
| } | |
| } | |
| /** | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| */ | |
| _handleContextMenuDelete(uiSourceCode) { | |
| const shouldDelete = window.confirm(Common.UIString('Are you sure you want to delete this file?')); | |
| if (shouldDelete) { | |
| uiSourceCode.project().deleteFile(uiSourceCode); | |
| } | |
| } | |
| /** | |
| * @param {!Event} event | |
| * @param {!Sources.NavigatorUISourceCodeTreeNode} node | |
| */ | |
| handleFileContextMenu(event, node) { | |
| const uiSourceCode = node.uiSourceCode(); | |
| const contextMenu = new UI.ContextMenu(event); | |
| contextMenu.appendApplicableItems(uiSourceCode); | |
| const project = uiSourceCode.project(); | |
| if (project.type() === Workspace.projectTypes.FileSystem) { | |
| contextMenu.editSection().appendItem( | |
| Common.UIString('Rename\u2026'), this._handleContextMenuRename.bind(this, node)); | |
| contextMenu.editSection().appendItem( | |
| Common.UIString('Make a copy\u2026'), this._handleContextMenuCreate.bind(this, project, '', uiSourceCode)); | |
| contextMenu.editSection().appendItem( | |
| Common.UIString('Delete'), this._handleContextMenuDelete.bind(this, uiSourceCode)); | |
| } | |
| contextMenu.show(); | |
| } | |
| /** | |
| * @param {!Event} event | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| handleFolderContextMenu(event, node) { | |
| const path = node._folderPath || ''; | |
| const project = node._project; | |
| const contextMenu = new UI.ContextMenu(event); | |
| if (project.type() === Workspace.projectTypes.FileSystem) { | |
| Sources.NavigatorView.appendSearchItem(contextMenu, path); | |
| const folderPath = Common.ParsedURL.urlToPlatformPath( | |
| Persistence.FileSystemWorkspaceBinding.completeURL(project, path), Host.isWin()); | |
| contextMenu.revealSection().appendItem( | |
| Common.UIString('Open folder'), () => Host.InspectorFrontendHost.showItemInFolder(folderPath)); | |
| if (project.canCreateFile()) { | |
| contextMenu.defaultSection().appendItem( | |
| Common.UIString('New file'), this._handleContextMenuCreate.bind(this, project, path)); | |
| } | |
| } | |
| if (project.canExcludeFolder(path)) { | |
| contextMenu.defaultSection().appendItem( | |
| Common.UIString('Exclude folder'), this._handleContextMenuExclude.bind(this, project, path)); | |
| } | |
| function removeFolder() { | |
| const shouldRemove = window.confirm(Common.UIString('Are you sure you want to remove this folder?')); | |
| if (shouldRemove) { | |
| project.remove(); | |
| } | |
| } | |
| if (project.type() === Workspace.projectTypes.FileSystem) { | |
| contextMenu.defaultSection().appendAction('sources.add-folder-to-workspace', undefined, true); | |
| if (node instanceof Sources.NavigatorGroupTreeNode) { | |
| contextMenu.defaultSection().appendItem(Common.UIString('Remove folder from workspace'), removeFolder); | |
| } | |
| } | |
| contextMenu.show(); | |
| } | |
| /** | |
| * @param {!Sources.NavigatorUISourceCodeTreeNode} node | |
| * @param {boolean} creatingNewUISourceCode | |
| * @protected | |
| */ | |
| rename(node, creatingNewUISourceCode) { | |
| const uiSourceCode = node.uiSourceCode(); | |
| node.rename(callback.bind(this)); | |
| /** | |
| * @this {Sources.NavigatorView} | |
| * @param {boolean} committed | |
| */ | |
| function callback(committed) { | |
| if (!creatingNewUISourceCode) { | |
| return; | |
| } | |
| if (!committed) { | |
| uiSourceCode.remove(); | |
| } else if (node._treeElement.listItemElement.hasFocus()) { | |
| this._sourceSelected(uiSourceCode, true); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {!Workspace.Project} project | |
| * @param {string} path | |
| * @param {!Workspace.UISourceCode=} uiSourceCodeToCopy | |
| */ | |
| async create(project, path, uiSourceCodeToCopy) { | |
| let content = ''; | |
| if (uiSourceCodeToCopy) { | |
| content = (await uiSourceCodeToCopy.requestContent()).content || ''; | |
| } | |
| const uiSourceCode = await project.createFile(path, null, content); | |
| if (!uiSourceCode) { | |
| return; | |
| } | |
| this._sourceSelected(uiSourceCode, false); | |
| const node = this.revealUISourceCode(uiSourceCode, true); | |
| if (node) { | |
| this.rename(node, true); | |
| } | |
| } | |
| _groupingChanged() { | |
| this.reset(); | |
| this._initGrouping(); | |
| this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); | |
| } | |
| _initGrouping() { | |
| this._groupByFrame = true; | |
| this._groupByDomain = this._navigatorGroupByFolderSetting.get(); | |
| this._groupByFolder = this._groupByDomain; | |
| } | |
| _resetForTest() { | |
| this.reset(); | |
| this._workspace.uiSourceCodes().forEach(this._addUISourceCode.bind(this)); | |
| } | |
| /** | |
| * @param {!SDK.ResourceTreeFrame} frame | |
| */ | |
| _discardFrame(frame) { | |
| const node = this._frameNodes.get(frame); | |
| if (!node) { | |
| return; | |
| } | |
| if (node.parent) { | |
| node.parent.removeChild(node); | |
| } | |
| this._frameNodes.delete(frame); | |
| for (const child of frame.childFrames) { | |
| this._discardFrame(child); | |
| } | |
| } | |
| /** | |
| * @override | |
| * @param {!SDK.Target} target | |
| */ | |
| targetAdded(target) { | |
| } | |
| /** | |
| * @override | |
| * @param {!SDK.Target} target | |
| */ | |
| targetRemoved(target) { | |
| const targetNode = this._rootNode.child('target:' + target.id()); | |
| if (targetNode) { | |
| this._rootNode.removeChild(targetNode); | |
| } | |
| } | |
| /** | |
| * @param {!Common.Event} event | |
| */ | |
| _targetNameChanged(event) { | |
| const target = /** @type {!SDK.Target} */ (event.data); | |
| const targetNode = this._rootNode.child('target:' + target.id()); | |
| if (targetNode) { | |
| targetNode.setTitle(target.name()); | |
| } | |
| } | |
| }; | |
| Sources.NavigatorView.Types = { | |
| Domain: 'domain', | |
| File: 'file', | |
| FileSystem: 'fs', | |
| FileSystemFolder: 'fs-folder', | |
| Frame: 'frame', | |
| NetworkFolder: 'nw-folder', | |
| Root: 'root', | |
| SourceMapFolder: 'sm-folder', | |
| Worker: 'worker' | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorFolderTreeElement = class extends UI.TreeElement { | |
| /** | |
| * @param {!Sources.NavigatorView} navigatorView | |
| * @param {string} type | |
| * @param {string} title | |
| * @param {function(boolean)=} hoverCallback | |
| */ | |
| constructor(navigatorView, type, title, hoverCallback) { | |
| super('', true); | |
| this.listItemElement.classList.add('navigator-' + type + '-tree-item', 'navigator-folder-tree-item'); | |
| this._nodeType = type; | |
| this.title = title; | |
| this.tooltip = title; | |
| this._navigatorView = navigatorView; | |
| this._hoverCallback = hoverCallback; | |
| let iconType = 'largeicon-navigator-folder'; | |
| if (type === Sources.NavigatorView.Types.Domain) { | |
| iconType = 'largeicon-navigator-domain'; | |
| } else if (type === Sources.NavigatorView.Types.Frame) { | |
| iconType = 'largeicon-navigator-frame'; | |
| } else if (type === Sources.NavigatorView.Types.Worker) { | |
| iconType = 'largeicon-navigator-worker'; | |
| } | |
| this.setLeadingIcons([UI.Icon.create(iconType, 'icon')]); | |
| } | |
| /** | |
| * @override | |
| * @returns {!Promise} | |
| */ | |
| async onpopulate() { | |
| this._node.populate(); | |
| } | |
| /** | |
| * @override | |
| */ | |
| onattach() { | |
| this.collapse(); | |
| this._node.onattach(); | |
| this.listItemElement.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), false); | |
| this.listItemElement.addEventListener('mousemove', this._mouseMove.bind(this), false); | |
| this.listItemElement.addEventListener('mouseleave', this._mouseLeave.bind(this), false); | |
| } | |
| /** | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| setNode(node) { | |
| this._node = node; | |
| const paths = []; | |
| while (node && !node.isRoot()) { | |
| paths.push(node._title); | |
| node = node.parent; | |
| } | |
| paths.reverse(); | |
| this.tooltip = paths.join('/'); | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _handleContextMenuEvent(event) { | |
| if (!this._node) { | |
| return; | |
| } | |
| this.select(); | |
| this._navigatorView.handleFolderContextMenu(event, this._node); | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _mouseMove(event) { | |
| if (this._hovered || !this._hoverCallback) { | |
| return; | |
| } | |
| this._hovered = true; | |
| this._hoverCallback(true); | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _mouseLeave(event) { | |
| if (!this._hoverCallback) { | |
| return; | |
| } | |
| this._hovered = false; | |
| this._hoverCallback(false); | |
| } | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorSourceTreeElement = class extends UI.TreeElement { | |
| /** | |
| * @param {!Sources.NavigatorView} navigatorView | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {string} title | |
| * @param {!Sources.NavigatorUISourceCodeTreeNode} node | |
| */ | |
| constructor(navigatorView, uiSourceCode, title, node) { | |
| super('', false); | |
| this._nodeType = Sources.NavigatorView.Types.File; | |
| this._node = node; | |
| this.title = title; | |
| this.listItemElement.classList.add( | |
| 'navigator-' + uiSourceCode.contentType().name() + '-tree-item', 'navigator-file-tree-item'); | |
| this.tooltip = uiSourceCode.url(); | |
| this._navigatorView = navigatorView; | |
| this._uiSourceCode = uiSourceCode; | |
| this.updateIcon(); | |
| } | |
| updateIcon() { | |
| const binding = Persistence.persistence.binding(this._uiSourceCode); | |
| if (binding) { | |
| const container = createElementWithClass('span', 'icon-stack'); | |
| let iconType = 'largeicon-navigator-file-sync'; | |
| if (Snippets.isSnippetsUISourceCode(binding.fileSystem)) { | |
| iconType = 'largeicon-navigator-snippet'; | |
| } | |
| const icon = UI.Icon.create(iconType, 'icon'); | |
| const badge = UI.Icon.create('badge-navigator-file-sync', 'icon-badge'); | |
| // TODO(allada) This does not play well with dark theme. Add an actual icon and use it. | |
| if (Persistence.networkPersistenceManager.project() === binding.fileSystem.project()) { | |
| badge.style.filter = 'hue-rotate(160deg)'; | |
| } | |
| container.appendChild(icon); | |
| container.appendChild(badge); | |
| container.title = Persistence.PersistenceUtils.tooltipForUISourceCode(this._uiSourceCode); | |
| this.setLeadingIcons([container]); | |
| } else { | |
| let iconType = 'largeicon-navigator-file'; | |
| if (Snippets.isSnippetsUISourceCode(this._uiSourceCode)) { | |
| iconType = 'largeicon-navigator-snippet'; | |
| } | |
| const defaultIcon = UI.Icon.create(iconType, 'icon'); | |
| this.setLeadingIcons([defaultIcon]); | |
| } | |
| } | |
| /** | |
| * @return {!Workspace.UISourceCode} | |
| */ | |
| get uiSourceCode() { | |
| return this._uiSourceCode; | |
| } | |
| /** | |
| * @override | |
| */ | |
| onattach() { | |
| this.listItemElement.draggable = true; | |
| this.listItemElement.addEventListener('click', this._onclick.bind(this), false); | |
| this.listItemElement.addEventListener('contextmenu', this._handleContextMenuEvent.bind(this), false); | |
| this.listItemElement.addEventListener('dragstart', this._ondragstart.bind(this), false); | |
| } | |
| _shouldRenameOnMouseDown() { | |
| if (!this._uiSourceCode.canRename()) { | |
| return false; | |
| } | |
| const isSelected = this === this.treeOutline.selectedTreeElement; | |
| return isSelected && this.treeOutline.element.hasFocus() && !UI.isBeingEdited(this.treeOutline.element); | |
| } | |
| /** | |
| * @override | |
| */ | |
| selectOnMouseDown(event) { | |
| if (event.which !== 1 || !this._shouldRenameOnMouseDown()) { | |
| super.selectOnMouseDown(event); | |
| return; | |
| } | |
| setTimeout(rename.bind(this), 300); | |
| /** | |
| * @this {Sources.NavigatorSourceTreeElement} | |
| */ | |
| function rename() { | |
| if (this._shouldRenameOnMouseDown()) { | |
| this._navigatorView.rename(this._node, false); | |
| } | |
| } | |
| } | |
| /** | |
| * @param {!DragEvent} event | |
| */ | |
| _ondragstart(event) { | |
| event.dataTransfer.setData('text/plain', this._uiSourceCode.url()); | |
| event.dataTransfer.effectAllowed = 'copy'; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| onspace() { | |
| this._navigatorView._sourceSelected(this.uiSourceCode, true); | |
| return true; | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _onclick(event) { | |
| this._navigatorView._sourceSelected(this.uiSourceCode, false); | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| ondblclick(event) { | |
| const middleClick = event.button === 1; | |
| this._navigatorView._sourceSelected(this.uiSourceCode, !middleClick); | |
| return false; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| onenter() { | |
| this._navigatorView._sourceSelected(this.uiSourceCode, true); | |
| return true; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| ondelete() { | |
| return true; | |
| } | |
| /** | |
| * @param {!Event} event | |
| */ | |
| _handleContextMenuEvent(event) { | |
| this.select(); | |
| this._navigatorView.handleFileContextMenu(event, this._node); | |
| } | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorTreeNode = class { | |
| /** | |
| * @param {string} id | |
| * @param {string} type | |
| */ | |
| constructor(id, type) { | |
| this.id = id; | |
| this._type = type; | |
| /** @type {!Map.<string, !Sources.NavigatorTreeNode>} */ | |
| this._children = new Map(); | |
| } | |
| /** | |
| * @return {!UI.TreeElement} | |
| */ | |
| treeNode() { | |
| throw 'Not implemented'; | |
| } | |
| dispose() { | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| isRoot() { | |
| return false; | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| return true; | |
| } | |
| onattach() { | |
| } | |
| /** | |
| * @param {string} title | |
| */ | |
| setTitle(title) { | |
| throw 'Not implemented'; | |
| } | |
| populate() { | |
| if (this.isPopulated()) { | |
| return; | |
| } | |
| if (this.parent) { | |
| this.parent.populate(); | |
| } | |
| this._populated = true; | |
| this.wasPopulated(); | |
| } | |
| wasPopulated() { | |
| const children = this.children(); | |
| for (let i = 0; i < children.length; ++i) { | |
| this.treeNode().appendChild(/** @type {!UI.TreeElement} */ (children[i].treeNode())); | |
| } | |
| } | |
| /** | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| didAddChild(node) { | |
| if (this.isPopulated()) { | |
| this.treeNode().appendChild(/** @type {!UI.TreeElement} */ (node.treeNode())); | |
| } | |
| } | |
| /** | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| willRemoveChild(node) { | |
| if (this.isPopulated()) { | |
| this.treeNode().removeChild(/** @type {!UI.TreeElement} */ (node.treeNode())); | |
| } | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| isPopulated() { | |
| return this._populated; | |
| } | |
| /** | |
| * @return {boolean} | |
| */ | |
| isEmpty() { | |
| return !this._children.size; | |
| } | |
| /** | |
| * @return {!Array.<!Sources.NavigatorTreeNode>} | |
| */ | |
| children() { | |
| return this._children.valuesArray(); | |
| } | |
| /** | |
| * @param {string} id | |
| * @return {?Sources.NavigatorTreeNode} | |
| */ | |
| child(id) { | |
| return this._children.get(id) || null; | |
| } | |
| /** | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| appendChild(node) { | |
| this._children.set(node.id, node); | |
| node.parent = this; | |
| this.didAddChild(node); | |
| } | |
| /** | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| removeChild(node) { | |
| this.willRemoveChild(node); | |
| this._children.remove(node.id); | |
| delete node.parent; | |
| node.dispose(); | |
| } | |
| reset() { | |
| this._children.clear(); | |
| } | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorRootTreeNode = class extends Sources.NavigatorTreeNode { | |
| /** | |
| * @param {!Sources.NavigatorView} navigatorView | |
| */ | |
| constructor(navigatorView) { | |
| super('', Sources.NavigatorView.Types.Root); | |
| this._navigatorView = navigatorView; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| isRoot() { | |
| return true; | |
| } | |
| /** | |
| * @override | |
| * @return {!UI.TreeElement} | |
| */ | |
| treeNode() { | |
| return this._navigatorView._scriptsTree.rootElement(); | |
| } | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorUISourceCodeTreeNode = class extends Sources.NavigatorTreeNode { | |
| /** | |
| * @param {!Sources.NavigatorView} navigatorView | |
| * @param {!Workspace.UISourceCode} uiSourceCode | |
| * @param {?SDK.ResourceTreeFrame} frame | |
| */ | |
| constructor(navigatorView, uiSourceCode, frame) { | |
| super(uiSourceCode.project().id() + ':' + uiSourceCode.url(), Sources.NavigatorView.Types.File); | |
| this._navigatorView = navigatorView; | |
| this._uiSourceCode = uiSourceCode; | |
| this._treeElement = null; | |
| this._eventListeners = []; | |
| this._frame = frame; | |
| } | |
| /** | |
| * @return {?SDK.ResourceTreeFrame} | |
| */ | |
| frame() { | |
| return this._frame; | |
| } | |
| /** | |
| * @return {!Workspace.UISourceCode} | |
| */ | |
| uiSourceCode() { | |
| return this._uiSourceCode; | |
| } | |
| /** | |
| * @override | |
| * @return {!UI.TreeElement} | |
| */ | |
| treeNode() { | |
| if (this._treeElement) { | |
| return this._treeElement; | |
| } | |
| this._treeElement = new Sources.NavigatorSourceTreeElement(this._navigatorView, this._uiSourceCode, '', this); | |
| this.updateTitle(); | |
| const updateTitleBound = this.updateTitle.bind(this, undefined); | |
| this._eventListeners = [ | |
| this._uiSourceCode.addEventListener(Workspace.UISourceCode.Events.TitleChanged, updateTitleBound), | |
| this._uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyChanged, updateTitleBound), | |
| this._uiSourceCode.addEventListener(Workspace.UISourceCode.Events.WorkingCopyCommitted, updateTitleBound) | |
| ]; | |
| return this._treeElement; | |
| } | |
| /** | |
| * @param {boolean=} ignoreIsDirty | |
| */ | |
| updateTitle(ignoreIsDirty) { | |
| if (!this._treeElement) { | |
| return; | |
| } | |
| let titleText = this._uiSourceCode.displayName(); | |
| if (!ignoreIsDirty && this._uiSourceCode.isDirty()) { | |
| titleText = '*' + titleText; | |
| } | |
| this._treeElement.title = titleText; | |
| this._treeElement.updateIcon(); | |
| let tooltip = this._uiSourceCode.url(); | |
| if (this._uiSourceCode.contentType().isFromSourceMap()) { | |
| tooltip = Common.UIString('%s (from source map)', this._uiSourceCode.displayName()); | |
| } | |
| this._treeElement.tooltip = tooltip; | |
| } | |
| /** | |
| * @override | |
| * @return {boolean} | |
| */ | |
| hasChildren() { | |
| return false; | |
| } | |
| /** | |
| * @override | |
| */ | |
| dispose() { | |
| Common.EventTarget.removeEventListeners(this._eventListeners); | |
| } | |
| /** | |
| * @param {boolean=} select | |
| */ | |
| reveal(select) { | |
| this.parent.populate(); | |
| this.parent.treeNode().expand(); | |
| this._treeElement.reveal(true); | |
| if (select) { | |
| this._treeElement.select(true); | |
| } | |
| } | |
| /** | |
| * @param {function(boolean)=} callback | |
| */ | |
| rename(callback) { | |
| if (!this._treeElement) { | |
| return; | |
| } | |
| this._treeElement.listItemElement.focus(); | |
| // Tree outline should be marked as edited as well as the tree element to prevent search from starting. | |
| const treeOutlineElement = this._treeElement.treeOutline.element; | |
| UI.markBeingEdited(treeOutlineElement, true); | |
| /** | |
| * @param {!Element} element | |
| * @param {string} newTitle | |
| * @param {string} oldTitle | |
| * @this {Sources.NavigatorUISourceCodeTreeNode} | |
| */ | |
| function commitHandler(element, newTitle, oldTitle) { | |
| if (newTitle !== oldTitle) { | |
| this._treeElement.title = newTitle; | |
| this._uiSourceCode.rename(newTitle).then(renameCallback.bind(this)); | |
| return; | |
| } | |
| afterEditing.call(this, true); | |
| } | |
| /** | |
| * @param {boolean} success | |
| * @this {Sources.NavigatorUISourceCodeTreeNode} | |
| */ | |
| function renameCallback(success) { | |
| if (!success) { | |
| UI.markBeingEdited(treeOutlineElement, false); | |
| this.updateTitle(); | |
| this.rename(callback); | |
| return; | |
| } | |
| afterEditing.call(this, true); | |
| } | |
| /** | |
| * @param {boolean} committed | |
| * @this {Sources.NavigatorUISourceCodeTreeNode} | |
| */ | |
| function afterEditing(committed) { | |
| UI.markBeingEdited(treeOutlineElement, false); | |
| this.updateTitle(); | |
| if (callback) { | |
| callback(committed); | |
| } | |
| } | |
| this.updateTitle(true); | |
| this._treeElement.startEditingTitle( | |
| new UI.InplaceEditor.Config(commitHandler.bind(this), afterEditing.bind(this, false))); | |
| } | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorFolderTreeNode = class extends Sources.NavigatorTreeNode { | |
| /** | |
| * @param {!Sources.NavigatorView} navigatorView | |
| * @param {?Workspace.Project} project | |
| * @param {string} id | |
| * @param {string} type | |
| * @param {string} folderPath | |
| * @param {string} title | |
| */ | |
| constructor(navigatorView, project, id, type, folderPath, title) { | |
| super(id, type); | |
| this._navigatorView = navigatorView; | |
| this._project = project; | |
| this._folderPath = folderPath; | |
| this._title = title; | |
| } | |
| /** | |
| * @override | |
| * @return {!UI.TreeElement} | |
| */ | |
| treeNode() { | |
| if (this._treeElement) { | |
| return this._treeElement; | |
| } | |
| this._treeElement = this._createTreeElement(this._title, this); | |
| this.updateTitle(); | |
| return this._treeElement; | |
| } | |
| updateTitle() { | |
| if (!this._treeElement || this._project.type() !== Workspace.projectTypes.FileSystem) { | |
| return; | |
| } | |
| const absoluteFileSystemPath = | |
| Persistence.FileSystemWorkspaceBinding.fileSystemPath(this._project.id()) + '/' + this._folderPath; | |
| const hasMappedFiles = Persistence.persistence.filePathHasBindings(absoluteFileSystemPath); | |
| this._treeElement.listItemElement.classList.toggle('has-mapped-files', hasMappedFiles); | |
| } | |
| /** | |
| * @return {!UI.TreeElement} | |
| */ | |
| _createTreeElement(title, node) { | |
| if (this._project.type() !== Workspace.projectTypes.FileSystem) { | |
| try { | |
| title = decodeURI(title); | |
| } catch (e) { | |
| } | |
| } | |
| const treeElement = new Sources.NavigatorFolderTreeElement(this._navigatorView, this._type, title); | |
| treeElement.setNode(node); | |
| return treeElement; | |
| } | |
| /** | |
| * @override | |
| */ | |
| wasPopulated() { | |
| if (!this._treeElement || this._treeElement._node !== this) { | |
| return; | |
| } | |
| this._addChildrenRecursive(); | |
| } | |
| _addChildrenRecursive() { | |
| const children = this.children(); | |
| for (let i = 0; i < children.length; ++i) { | |
| const child = children[i]; | |
| this.didAddChild(child); | |
| if (child instanceof Sources.NavigatorFolderTreeNode) { | |
| child._addChildrenRecursive(); | |
| } | |
| } | |
| } | |
| _shouldMerge(node) { | |
| return this._type !== Sources.NavigatorView.Types.Domain && node instanceof Sources.NavigatorFolderTreeNode; | |
| } | |
| /** | |
| * @param {!Sources.NavigatorTreeNode} node | |
| * @override | |
| */ | |
| didAddChild(node) { | |
| function titleForNode(node) { | |
| return node._title; | |
| } | |
| if (!this._treeElement) { | |
| return; | |
| } | |
| let children = this.children(); | |
| if (children.length === 1 && this._shouldMerge(node)) { | |
| node._isMerged = true; | |
| this._treeElement.title = this._treeElement.title + '/' + node._title; | |
| node._treeElement = this._treeElement; | |
| this._treeElement.setNode(node); | |
| return; | |
| } | |
| let oldNode; | |
| if (children.length === 2) { | |
| oldNode = children[0] !== node ? children[0] : children[1]; | |
| } | |
| if (oldNode && oldNode._isMerged) { | |
| delete oldNode._isMerged; | |
| const mergedToNodes = []; | |
| mergedToNodes.push(this); | |
| let treeNode = this; | |
| while (treeNode._isMerged) { | |
| treeNode = treeNode.parent; | |
| mergedToNodes.push(treeNode); | |
| } | |
| mergedToNodes.reverse(); | |
| const titleText = mergedToNodes.map(titleForNode).join('/'); | |
| const nodes = []; | |
| treeNode = oldNode; | |
| do { | |
| nodes.push(treeNode); | |
| children = treeNode.children(); | |
| treeNode = children.length === 1 ? children[0] : null; | |
| } while (treeNode && treeNode._isMerged); | |
| if (!this.isPopulated()) { | |
| this._treeElement.title = titleText; | |
| this._treeElement.setNode(this); | |
| for (let i = 0; i < nodes.length; ++i) { | |
| delete nodes[i]._treeElement; | |
| delete nodes[i]._isMerged; | |
| } | |
| return; | |
| } | |
| const oldTreeElement = this._treeElement; | |
| const treeElement = this._createTreeElement(titleText, this); | |
| for (let i = 0; i < mergedToNodes.length; ++i) { | |
| mergedToNodes[i]._treeElement = treeElement; | |
| } | |
| oldTreeElement.parent.appendChild(treeElement); | |
| oldTreeElement.setNode(nodes[nodes.length - 1]); | |
| oldTreeElement.title = nodes.map(titleForNode).join('/'); | |
| oldTreeElement.parent.removeChild(oldTreeElement); | |
| this._treeElement.appendChild(oldTreeElement); | |
| if (oldTreeElement.expanded) { | |
| treeElement.expand(); | |
| } | |
| } | |
| if (this.isPopulated()) { | |
| this._treeElement.appendChild(node.treeNode()); | |
| } | |
| } | |
| /** | |
| * @override | |
| * @param {!Sources.NavigatorTreeNode} node | |
| */ | |
| willRemoveChild(node) { | |
| if (node._isMerged || !this.isPopulated()) { | |
| return; | |
| } | |
| this._treeElement.removeChild(node._treeElement); | |
| } | |
| }; | |
| /** | |
| * @unrestricted | |
| */ | |
| Sources.NavigatorGroupTreeNode = class extends Sources.NavigatorTreeNode { | |
| /** | |
| * @param {!Sources.NavigatorView} navigatorView | |
| * @param {!Workspace.Project} project | |
| * @param {string} id | |
| * @param {string} type | |
| * @param {string} title | |
| */ | |
| constructor(navigatorView, project, id, type, title) { | |
| super(id, type); | |
| this._project = project; | |
| this._navigatorView = navigatorView; | |
| this._title = title; | |
| this.populate(); | |
| } | |
| /** | |
| * @param {function(boolean)} hoverCallback | |
| */ | |
| setHoverCallback(hoverCallback) { | |
| this._hoverCallback = hoverCallback; | |
| } | |
| /** | |
| * @override | |
| * @return {!UI.TreeElement} | |
| */ | |
| treeNode() { | |
| if (this._treeElement) { | |
| return this._treeElement; | |
| } | |
| this._treeElement = | |
| new Sources.NavigatorFolderTreeElement(this._navigatorView, this._type, this._title, this._hoverCallback); | |
| this._treeElement.setNode(this); | |
| return this._treeElement; | |
| } | |
| /** | |
| * @override | |
| */ | |
| onattach() { | |
| this.updateTitle(); | |
| } | |
| updateTitle() { | |
| if (!this._treeElement || this._project.type() !== Workspace.projectTypes.FileSystem) { | |
| return; | |
| } | |
| const fileSystemPath = Persistence.FileSystemWorkspaceBinding.fileSystemPath(this._project.id()); | |
| const wasActive = this._treeElement.listItemElement.classList.contains('has-mapped-files'); | |
| const isActive = Persistence.persistence.filePathHasBindings(fileSystemPath); | |
| if (wasActive === isActive) { | |
| return; | |
| } | |
| this._treeElement.listItemElement.classList.toggle('has-mapped-files', isActive); | |
| if (this._treeElement.childrenListElement.hasFocus()) { | |
| return; | |
| } | |
| if (isActive) { | |
| this._treeElement.expand(); | |
| } else { | |
| this._treeElement.collapse(); | |
| } | |
| } | |
| /** | |
| * @param {string} title | |
| * @override | |
| */ | |
| setTitle(title) { | |
| this._title = title; | |
| if (this._treeElement) { | |
| this._treeElement.title = this._title; | |
| } | |
| } | |
| }; |