DOMTreeContentView.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430
  1. /*
  2. * Copyright (C) 2013 Apple Inc. All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without
  5. * modification, are permitted provided that the following conditions
  6. * are met:
  7. * 1. Redistributions of source code must retain the above copyright
  8. * notice, this list of conditions and the following disclaimer.
  9. * 2. Redistributions in binary form must reproduce the above copyright
  10. * notice, this list of conditions and the following disclaimer in the
  11. * documentation and/or other materials provided with the distribution.
  12. *
  13. * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
  14. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
  15. * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
  16. * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
  17. * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  18. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  19. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  20. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  21. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  22. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
  23. * THE POSSIBILITY OF SUCH DAMAGE.
  24. */
  25. WebInspector.DOMTreeContentView = function(domTree)
  26. {
  27. console.assert(domTree);
  28. WebInspector.ContentView.call(this, domTree);
  29. // The navigation item for the compositing borders button.
  30. this._compositingBordersButtonNavigationItem = new WebInspector.ActivateButtonNavigationItem("layer-borders", WebInspector.UIString("Show compositing borders"), WebInspector.UIString("Hide compositing borders"), "Images/LayerBorders.pdf", 16, 16);
  31. this._compositingBordersButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._toggleCompositingBorders, this);
  32. this._compositingBordersButtonNavigationItem.enabled = !!PageAgent.getCompositingBordersVisible;
  33. // The navigation item for the shadow tree toggle button.
  34. WebInspector.showShadowDOMSetting.addEventListener(WebInspector.Setting.Event.Changed, this._showShadowDOMSettingChanged, this);
  35. this._showsShadowDOMButtonNavigationItem = new WebInspector.ActivateButtonNavigationItem("shows-shadow-DOM", WebInspector.UIString("Show shadow DOM nodes"), WebInspector.UIString("Hide shadow DOM nodes"), "Images/ShadowDOM.pdf", 16, 16);
  36. this._showsShadowDOMButtonNavigationItem.addEventListener(WebInspector.ButtonNavigationItem.Event.Clicked, this._toggleShowsShadowDOMSetting, this);
  37. this._showShadowDOMSettingChanged();
  38. this.element.classList.add(WebInspector.DOMTreeContentView.StyleClassName);
  39. this.element.addEventListener("click", this._mouseWasClicked.bind(this), false);
  40. this._domTree = domTree;
  41. this._domTree.addEventListener(WebInspector.DOMTree.Event.RootDOMNodeInvalidated, this._rootDOMNodeInvalidated, this);
  42. this._domTreeOutline = new WebInspector.DOMTreeOutline(true, true, false);
  43. this._domTreeOutline.addEventListener(WebInspector.DOMTreeOutline.Event.SelectedNodeChanged, this._selectedNodeDidChange, this);
  44. this._domTreeOutline.wireToDomAgent();
  45. this.element.appendChild(this._domTreeOutline.element);
  46. WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.AttributeModified, this._domNodeChanged, this);
  47. WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.AttributeRemoved, this._domNodeChanged, this);
  48. WebInspector.domTreeManager.addEventListener(WebInspector.DOMTreeManager.Event.CharacterDataModified, this._domNodeChanged, this);
  49. this._lastSelectedNodePathSetting = new WebInspector.Setting("last-selected-node-path", null);
  50. this._numberOfSearchResults = null;
  51. this._requestRootDOMNode();
  52. };
  53. WebInspector.DOMTreeContentView.StyleClassName = "dom-tree";
  54. WebInspector.DOMTreeContentView.prototype = {
  55. constructor: WebInspector.DOMTreeContentView,
  56. // Public
  57. get navigationItems()
  58. {
  59. return [this._showsShadowDOMButtonNavigationItem, this._compositingBordersButtonNavigationItem];
  60. },
  61. get domTree()
  62. {
  63. return this._domTree;
  64. },
  65. get scrollableElements()
  66. {
  67. return [this.element];
  68. },
  69. updateLayout: function()
  70. {
  71. this._domTreeOutline.updateSelection();
  72. },
  73. shown: function()
  74. {
  75. this._domTreeOutline.setVisible(true, WebInspector.isConsoleFocused());
  76. this._updateCompositingBordersButtonToMatchPageSettings();
  77. },
  78. hidden: function()
  79. {
  80. WebInspector.domTreeManager.hideDOMNodeHighlight();
  81. this._domTreeOutline.setVisible(false);
  82. },
  83. closed: function()
  84. {
  85. this._domTree.removeEventListener(null, null, this);
  86. WebInspector.domTreeManager.removeEventListener(null, null, this);
  87. this._domTreeOutline.close();
  88. },
  89. get selectionPathComponents()
  90. {
  91. var treeElement = this._domTreeOutline.selectedTreeElement;
  92. var pathComponents = [];
  93. while (treeElement && !treeElement.root) {
  94. // The close tag is contained within the element it closes. So skip it since we don't want to
  95. // show the same node twice in the hierarchy.
  96. if (treeElement.isCloseTag()) {
  97. treeElement = treeElement.parent;
  98. continue;
  99. }
  100. var pathComponent = new WebInspector.DOMTreeElementPathComponent(treeElement, treeElement.representedObject);
  101. pathComponent.addEventListener(WebInspector.HierarchicalPathComponent.Event.SiblingWasSelected, this._pathComponentSelected, this);
  102. pathComponents.unshift(pathComponent);
  103. treeElement = treeElement.parent;
  104. }
  105. return pathComponents;
  106. },
  107. selectAndRevealDOMNode: function(domNode, preventFocusChange)
  108. {
  109. this._domTreeOutline.selectDOMNode(domNode, !preventFocusChange);
  110. },
  111. handleCopyEvent: function(event)
  112. {
  113. var selectedDOMNode = this._domTreeOutline.selectedDOMNode();
  114. if (!selectedDOMNode)
  115. return;
  116. event.clipboardData.clearData();
  117. event.preventDefault();
  118. selectedDOMNode.copyNode();
  119. },
  120. get supportsSearch()
  121. {
  122. return true;
  123. },
  124. get numberOfSearchResults()
  125. {
  126. return this._numberOfSearchResults;
  127. },
  128. get hasPerformedSearch()
  129. {
  130. return this._numberOfSearchResults !== null;
  131. },
  132. set automaticallyRevealFirstSearchResult(reveal)
  133. {
  134. this._automaticallyRevealFirstSearchResult = reveal;
  135. // If we haven't shown a search result yet, reveal one now.
  136. if (this._automaticallyRevealFirstSearchResult && this._numberOfSearchResults > 0) {
  137. if (this._currentSearchResultIndex === -1)
  138. this.revealNextSearchResult();
  139. }
  140. },
  141. performSearch: function(query)
  142. {
  143. if (this._searchQuery === query)
  144. return;
  145. if (this._searchIdentifier)
  146. DOMAgent.discardSearchResults(this._searchIdentifier);
  147. this._searchQuery = query;
  148. this._searchIdentifier = null;
  149. this._numberOfSearchResults = null;
  150. this._currentSearchResultIndex = -1;
  151. function searchResultsReady(error, searchIdentifier, resultsCount)
  152. {
  153. if (error)
  154. return;
  155. this._searchIdentifier = searchIdentifier;
  156. this._numberOfSearchResults = resultsCount;
  157. this.dispatchEventToListeners(WebInspector.ContentView.Event.NumberOfSearchResultsDidChange);
  158. if (this._automaticallyRevealFirstSearchResult)
  159. this.revealNextSearchResult();
  160. }
  161. DOMAgent.performSearch(query, searchResultsReady.bind(this));
  162. },
  163. searchCleared: function()
  164. {
  165. if (this._searchIdentifier)
  166. DOMAgent.discardSearchResults(this._searchIdentifier);
  167. this._searchQuery = null;
  168. this._searchIdentifier = null;
  169. this._numberOfSearchResults = null;
  170. this._currentSearchResultIndex = -1;
  171. },
  172. revealPreviousSearchResult: function(changeFocus)
  173. {
  174. if (!this._numberOfSearchResults)
  175. return;
  176. if (this._currentSearchResultIndex > 0)
  177. --this._currentSearchResultIndex;
  178. else
  179. this._currentSearchResultIndex = this._numberOfSearchResults - 1;
  180. this._revealSearchResult(this._currentSearchResultIndex, changeFocus);
  181. },
  182. revealNextSearchResult: function(changeFocus)
  183. {
  184. if (!this._numberOfSearchResults)
  185. return;
  186. if (this._currentSearchResultIndex + 1 < this._numberOfSearchResults)
  187. ++this._currentSearchResultIndex;
  188. else
  189. this._currentSearchResultIndex = 0;
  190. this._revealSearchResult(this._currentSearchResultIndex, changeFocus);
  191. },
  192. // Private
  193. _revealSearchResult: function(index, changeFocus)
  194. {
  195. console.assert(this._searchIdentifier);
  196. var searchIdentifier = this._searchIdentifier;
  197. function revealResult(error, nodeIdentifiers)
  198. {
  199. if (error)
  200. return;
  201. // Bail if the searchIdentifier changed since we started.
  202. if (this._searchIdentifier !== searchIdentifier)
  203. return;
  204. console.assert(nodeIdentifiers.length === 1);
  205. var domNode = WebInspector.domTreeManager.nodeForId(nodeIdentifiers[0]);
  206. console.assert(domNode);
  207. if (!domNode)
  208. return;
  209. this._domTreeOutline.selectDOMNode(domNode, changeFocus);
  210. }
  211. DOMAgent.getSearchResults(this._searchIdentifier, index, index + 1, revealResult.bind(this));
  212. },
  213. _rootDOMNodeAvailable: function(rootDOMNode)
  214. {
  215. this._domTreeOutline.rootDOMNode = rootDOMNode;
  216. if (!rootDOMNode) {
  217. this._domTreeOutline.selectDOMNode(null, false);
  218. return;
  219. }
  220. function selectNode(lastSelectedNode)
  221. {
  222. // A selection was made while waiting for the async reply. Just bail now.
  223. if (this._domTreeOutline.selectedTreeElement)
  224. return;
  225. var nodeToFocus = lastSelectedNode;
  226. if (!nodeToFocus)
  227. nodeToFocus = rootDOMNode.body || rootDOMNode.documentElement;
  228. if (!nodeToFocus)
  229. return;
  230. this._dontSetLastSelectedNodePath = true;
  231. this.selectAndRevealDOMNode(nodeToFocus, WebInspector.isConsoleFocused());
  232. this._dontSetLastSelectedNodePath = false;
  233. // If this wasn't the last selected node, then expand it.
  234. if (!lastSelectedNode && this._domTreeOutline.selectedTreeElement)
  235. this._domTreeOutline.selectedTreeElement.expand();
  236. }
  237. function selectLastSelectedNode(nodeId)
  238. {
  239. selectNode.call(this, WebInspector.domTreeManager.nodeForId(nodeId));
  240. }
  241. if (this._lastSelectedNodePathSetting.value && this._lastSelectedNodePathSetting.value.path && this._lastSelectedNodePathSetting.value.url === this._domTree.frame.url.hash)
  242. WebInspector.domTreeManager.pushNodeByPathToFrontend(this._lastSelectedNodePathSetting.value.path, selectLastSelectedNode.bind(this));
  243. else
  244. selectNode.call(this);
  245. },
  246. _rootDOMNodeInvalidated: function(event)
  247. {
  248. this._requestRootDOMNode();
  249. },
  250. _requestRootDOMNode: function()
  251. {
  252. this._domTree.requestRootDOMNode(this._rootDOMNodeAvailable.bind(this));
  253. },
  254. _selectedNodeDidChange: function(event)
  255. {
  256. var selectedDOMNode = this._domTreeOutline.selectedDOMNode();
  257. if (selectedDOMNode && !this._dontSetLastSelectedNodePath)
  258. this._lastSelectedNodePathSetting.value = {url: this._domTree.frame.url.hash, path: selectedDOMNode.path()};
  259. if (selectedDOMNode)
  260. ConsoleAgent.addInspectedNode(selectedDOMNode.id);
  261. this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
  262. },
  263. _pathComponentSelected: function(event)
  264. {
  265. console.assert(event.data.pathComponent instanceof WebInspector.DOMTreeElementPathComponent);
  266. console.assert(event.data.pathComponent.domTreeElement instanceof WebInspector.DOMTreeElement);
  267. this._domTreeOutline.selectDOMNode(event.data.pathComponent.domTreeElement.representedObject, true);
  268. },
  269. _domNodeChanged: function(event)
  270. {
  271. var selectedDOMNode = this._domTreeOutline.selectedDOMNode();
  272. if (selectedDOMNode !== event.data.node)
  273. return;
  274. this.dispatchEventToListeners(WebInspector.ContentView.Event.SelectionPathComponentsDidChange);
  275. },
  276. _mouseWasClicked: function(event)
  277. {
  278. var anchorElement = event.target.enclosingNodeOrSelfWithNodeName("a");
  279. if (!anchorElement || !anchorElement.href)
  280. return;
  281. // Prevent the link from navigating, since we don't do any navigation by following links normally.
  282. event.preventDefault();
  283. event.stopPropagation();
  284. if (WebInspector.isBeingEdited(anchorElement)) {
  285. // Don't follow the link when it is being edited.
  286. return;
  287. }
  288. // Cancel any pending link navigation.
  289. if (this._followLinkTimeoutIdentifier) {
  290. clearTimeout(this._followLinkTimeoutIdentifier);
  291. delete this._followLinkTimeoutIdentifier;
  292. }
  293. // If this is a double-click (or multiple-click), return early.
  294. if (event.detail > 1)
  295. return;
  296. function followLink()
  297. {
  298. // Since followLink is delayed, the call to WebInspector.openURL can't look at window.event
  299. // to see if the command key is down like it normally would. So we need to do that check
  300. // before calling WebInspector.openURL.
  301. var alwaysOpenExternally = event ? event.metaKey : false;
  302. WebInspector.openURL(anchorElement.href, this._frame, alwaysOpenExternally, anchorElement.lineNumber);
  303. }
  304. // Start a timeout since this is a single click, if the timeout is canceled before it fires,
  305. // then a double-click happened or another link was clicked.
  306. // FIXME: The duration might be longer or shorter than the user's configured double click speed.
  307. this._followLinkTimeoutIdentifier = setTimeout(followLink.bind(this), 333);
  308. },
  309. _toggleCompositingBorders: function(event)
  310. {
  311. console.assert(PageAgent.setCompositingBordersVisible);
  312. var activated = !this._compositingBordersButtonNavigationItem.activated;
  313. this._compositingBordersButtonNavigationItem.activated = activated;
  314. PageAgent.setCompositingBordersVisible(activated);
  315. },
  316. _updateCompositingBordersButtonToMatchPageSettings: function()
  317. {
  318. if (!PageAgent.getCompositingBordersVisible)
  319. return;
  320. var button = this._compositingBordersButtonNavigationItem;
  321. // We need to sync with the page settings since these can be controlled
  322. // in a different way than just using the navigation bar button.
  323. PageAgent.getCompositingBordersVisible(function(error, compositingBordersVisible) {
  324. button.activated = error ? false : compositingBordersVisible;
  325. });
  326. },
  327. _showShadowDOMSettingChanged: function(event)
  328. {
  329. this._showsShadowDOMButtonNavigationItem.activated = WebInspector.showShadowDOMSetting.value;
  330. },
  331. _toggleShowsShadowDOMSetting: function(event)
  332. {
  333. WebInspector.showShadowDOMSetting.value = !WebInspector.showShadowDOMSetting.value;
  334. }
  335. };
  336. WebInspector.DOMTreeContentView.prototype.__proto__ = WebInspector.ContentView.prototype;