inspector.js 63 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964
  1. /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
  2. /* This Source Code Form is subject to the terms of the Mozilla Public
  3. * License, v. 2.0. If a copy of the MPL was not distributed with this
  4. * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
  5. /* global window */
  6. "use strict";
  7. var Cu = Components.utils;
  8. var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
  9. var Services = require("Services");
  10. var promise = require("promise");
  11. var defer = require("devtools/shared/defer");
  12. var EventEmitter = require("devtools/shared/event-emitter");
  13. const {executeSoon} = require("devtools/shared/DevToolsUtils");
  14. var {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
  15. var {Task} = require("devtools/shared/task");
  16. const {initCssProperties} = require("devtools/shared/fronts/css-properties");
  17. const nodeConstants = require("devtools/shared/dom-node-constants");
  18. const Telemetry = require("devtools/client/shared/telemetry");
  19. const Menu = require("devtools/client/framework/menu");
  20. const MenuItem = require("devtools/client/framework/menu-item");
  21. const {CommandUtils} = require("devtools/client/shared/developer-toolbar");
  22. const {ComputedViewTool} = require("devtools/client/inspector/computed/computed");
  23. const {FontInspector} = require("devtools/client/inspector/fonts/fonts");
  24. const {HTMLBreadcrumbs} = require("devtools/client/inspector/breadcrumbs");
  25. const {InspectorSearch} = require("devtools/client/inspector/inspector-search");
  26. const MarkupView = require("devtools/client/inspector/markup/markup");
  27. const {RuleViewTool} = require("devtools/client/inspector/rules/rules");
  28. const {ToolSidebar} = require("devtools/client/inspector/toolsidebar");
  29. const {ViewHelpers} = require("devtools/client/shared/widgets/view-helpers");
  30. const clipboardHelper = require("devtools/shared/platform/clipboard");
  31. const {LocalizationHelper, localizeMarkup} = require("devtools/shared/l10n");
  32. const INSPECTOR_L10N =
  33. new LocalizationHelper("devtools/client/locales/inspector.properties");
  34. const TOOLBOX_L10N = new LocalizationHelper("devtools/client/locales/toolbox.properties");
  35. // Sidebar dimensions
  36. const INITIAL_SIDEBAR_SIZE = 350;
  37. // If the toolbox width is smaller than given amount of pixels,
  38. // the sidebar automatically switches from 'landscape' to 'portrait' mode.
  39. const PORTRAIT_MODE_WIDTH = 700;
  40. /**
  41. * Represents an open instance of the Inspector for a tab.
  42. * The inspector controls the breadcrumbs, the markup view, and the sidebar
  43. * (computed view, rule view, font view and animation inspector).
  44. *
  45. * Events:
  46. * - ready
  47. * Fired when the inspector panel is opened for the first time and ready to
  48. * use
  49. * - new-root
  50. * Fired after a new root (navigation to a new page) event was fired by
  51. * the walker, and taken into account by the inspector (after the markup
  52. * view has been reloaded)
  53. * - markuploaded
  54. * Fired when the markup-view frame has loaded
  55. * - breadcrumbs-updated
  56. * Fired when the breadcrumb widget updates to a new node
  57. * - boxmodel-view-updated
  58. * Fired when the box model updates to a new node
  59. * - markupmutation
  60. * Fired after markup mutations have been processed by the markup-view
  61. * - computed-view-refreshed
  62. * Fired when the computed rules view updates to a new node
  63. * - computed-view-property-expanded
  64. * Fired when a property is expanded in the computed rules view
  65. * - computed-view-property-collapsed
  66. * Fired when a property is collapsed in the computed rules view
  67. * - computed-view-sourcelinks-updated
  68. * Fired when the stylesheet source links have been updated (when switching
  69. * to source-mapped files)
  70. * - computed-view-filtered
  71. * Fired when the computed rules view is filtered
  72. * - rule-view-refreshed
  73. * Fired when the rule view updates to a new node
  74. * - rule-view-sourcelinks-updated
  75. * Fired when the stylesheet source links have been updated (when switching
  76. * to source-mapped files)
  77. */
  78. function Inspector(toolbox) {
  79. this._toolbox = toolbox;
  80. this._target = toolbox.target;
  81. this.panelDoc = window.document;
  82. this.panelWin = window;
  83. this.panelWin.inspector = this;
  84. this.telemetry = new Telemetry();
  85. this.nodeMenuTriggerInfo = null;
  86. this._handleRejectionIfNotDestroyed = this._handleRejectionIfNotDestroyed.bind(this);
  87. this._onBeforeNavigate = this._onBeforeNavigate.bind(this);
  88. this.onNewRoot = this.onNewRoot.bind(this);
  89. this._onContextMenu = this._onContextMenu.bind(this);
  90. this.onTextBoxContextMenu = this.onTextBoxContextMenu.bind(this);
  91. this._updateSearchResultsLabel = this._updateSearchResultsLabel.bind(this);
  92. this.onNewSelection = this.onNewSelection.bind(this);
  93. this.onDetached = this.onDetached.bind(this);
  94. this.onPaneToggleButtonClicked = this.onPaneToggleButtonClicked.bind(this);
  95. this._onMarkupFrameLoad = this._onMarkupFrameLoad.bind(this);
  96. this.onPanelWindowResize = this.onPanelWindowResize.bind(this);
  97. this.onSidebarShown = this.onSidebarShown.bind(this);
  98. this.onSidebarHidden = this.onSidebarHidden.bind(this);
  99. this._target.on("will-navigate", this._onBeforeNavigate);
  100. this._detectingActorFeatures = this._detectActorFeatures();
  101. EventEmitter.decorate(this);
  102. }
  103. Inspector.prototype = {
  104. /**
  105. * open is effectively an asynchronous constructor
  106. */
  107. init: Task.async(function* () {
  108. // Localize all the nodes containing a data-localization attribute.
  109. localizeMarkup(this.panelDoc);
  110. this._cssPropertiesLoaded = initCssProperties(this.toolbox);
  111. yield this._cssPropertiesLoaded;
  112. yield this.target.makeRemote();
  113. yield this._getPageStyle();
  114. // This may throw if the document is still loading and we are
  115. // refering to a dead about:blank document
  116. let defaultSelection = yield this._getDefaultNodeForSelection()
  117. .catch(this._handleRejectionIfNotDestroyed);
  118. return yield this._deferredOpen(defaultSelection);
  119. }),
  120. get toolbox() {
  121. return this._toolbox;
  122. },
  123. get inspector() {
  124. return this._toolbox.inspector;
  125. },
  126. get walker() {
  127. return this._toolbox.walker;
  128. },
  129. get selection() {
  130. return this._toolbox.selection;
  131. },
  132. get highlighter() {
  133. return this._toolbox.highlighter;
  134. },
  135. get isOuterHTMLEditable() {
  136. return this._target.client.traits.editOuterHTML;
  137. },
  138. get hasUrlToImageDataResolver() {
  139. return this._target.client.traits.urlToImageDataResolver;
  140. },
  141. get canGetUniqueSelector() {
  142. return this._target.client.traits.getUniqueSelector;
  143. },
  144. get canGetCssPath() {
  145. return this._target.client.traits.getCssPath;
  146. },
  147. get canGetUsedFontFaces() {
  148. return this._target.client.traits.getUsedFontFaces;
  149. },
  150. get canPasteInnerOrAdjacentHTML() {
  151. return this._target.client.traits.pasteHTML;
  152. },
  153. /**
  154. * Handle promise rejections for various asynchronous actions, and only log errors if
  155. * the inspector panel still exists.
  156. * This is useful to silence useless errors that happen when the inspector is closed
  157. * while still initializing (and making protocol requests).
  158. */
  159. _handleRejectionIfNotDestroyed: function (e) {
  160. if (!this._panelDestroyer) {
  161. console.error(e);
  162. }
  163. },
  164. /**
  165. * Figure out what features the backend supports
  166. */
  167. _detectActorFeatures: function () {
  168. this._supportsDuplicateNode = false;
  169. this._supportsScrollIntoView = false;
  170. this._supportsResolveRelativeURL = false;
  171. // Use getActorDescription first so that all actorHasMethod calls use
  172. // a cached response from the server.
  173. return this._target.getActorDescription("domwalker").then(desc => {
  174. return promise.all([
  175. this._target.actorHasMethod("domwalker", "duplicateNode").then(value => {
  176. this._supportsDuplicateNode = value;
  177. }).catch(e => console.error(e)),
  178. this._target.actorHasMethod("domnode", "scrollIntoView").then(value => {
  179. this._supportsScrollIntoView = value;
  180. }).catch(e => console.error(e)),
  181. this._target.actorHasMethod("inspector", "resolveRelativeURL").then(value => {
  182. this._supportsResolveRelativeURL = value;
  183. }).catch(e => console.error(e)),
  184. ]);
  185. });
  186. },
  187. _deferredOpen: function (defaultSelection) {
  188. let deferred = defer();
  189. this.breadcrumbs = new HTMLBreadcrumbs(this);
  190. this.walker.on("new-root", this.onNewRoot);
  191. this.selection.on("new-node-front", this.onNewSelection);
  192. this.selection.on("detached-front", this.onDetached);
  193. if (this.target.isLocalTab) {
  194. // Show a warning when the debugger is paused.
  195. // We show the warning only when the inspector
  196. // is selected.
  197. this.updateDebuggerPausedWarning = () => {
  198. let notificationBox = this._toolbox.getNotificationBox();
  199. let notification =
  200. notificationBox.getNotificationWithValue("inspector-script-paused");
  201. if (!notification && this._toolbox.currentToolId == "inspector" &&
  202. this._toolbox.threadClient.paused) {
  203. let message = INSPECTOR_L10N.getStr("debuggerPausedWarning.message");
  204. notificationBox.appendNotification(message,
  205. "inspector-script-paused", "", notificationBox.PRIORITY_WARNING_HIGH);
  206. }
  207. if (notification && this._toolbox.currentToolId != "inspector") {
  208. notificationBox.removeNotification(notification);
  209. }
  210. if (notification && !this._toolbox.threadClient.paused) {
  211. notificationBox.removeNotification(notification);
  212. }
  213. };
  214. this.target.on("thread-paused", this.updateDebuggerPausedWarning);
  215. this.target.on("thread-resumed", this.updateDebuggerPausedWarning);
  216. this._toolbox.on("select", this.updateDebuggerPausedWarning);
  217. this.updateDebuggerPausedWarning();
  218. }
  219. this._initMarkup();
  220. this.isReady = false;
  221. this.once("markuploaded", () => {
  222. this.isReady = true;
  223. // All the components are initialized. Let's select a node.
  224. if (defaultSelection) {
  225. this.selection.setNodeFront(defaultSelection, "inspector-open");
  226. this.markup.expandNode(this.selection.nodeFront);
  227. }
  228. // And setup the toolbar only now because it may depend on the document.
  229. this.setupToolbar();
  230. this.emit("ready");
  231. deferred.resolve(this);
  232. });
  233. this.setupSearchBox();
  234. this.setupSidebar();
  235. return deferred.promise;
  236. },
  237. _onBeforeNavigate: function () {
  238. this._defaultNode = null;
  239. this.selection.setNodeFront(null);
  240. this._destroyMarkup();
  241. this.isDirty = false;
  242. this._pendingSelection = null;
  243. },
  244. _getPageStyle: function () {
  245. return this.inspector.getPageStyle().then(pageStyle => {
  246. this.pageStyle = pageStyle;
  247. }, this._handleRejectionIfNotDestroyed);
  248. },
  249. /**
  250. * Return a promise that will resolve to the default node for selection.
  251. */
  252. _getDefaultNodeForSelection: function () {
  253. if (this._defaultNode) {
  254. return this._defaultNode;
  255. }
  256. let walker = this.walker;
  257. let rootNode = null;
  258. let pendingSelection = this._pendingSelection;
  259. // A helper to tell if the target has or is about to navigate.
  260. // this._pendingSelection changes on "will-navigate" and "new-root" events.
  261. let hasNavigated = () => pendingSelection !== this._pendingSelection;
  262. // If available, set either the previously selected node or the body
  263. // as default selected, else set documentElement
  264. return walker.getRootNode().then(node => {
  265. if (hasNavigated()) {
  266. return promise.reject("navigated; resolution of _defaultNode aborted");
  267. }
  268. rootNode = node;
  269. if (this.selectionCssSelector) {
  270. return walker.querySelector(rootNode, this.selectionCssSelector);
  271. }
  272. return null;
  273. }).then(front => {
  274. if (hasNavigated()) {
  275. return promise.reject("navigated; resolution of _defaultNode aborted");
  276. }
  277. if (front) {
  278. return front;
  279. }
  280. return walker.querySelector(rootNode, "body");
  281. }).then(front => {
  282. if (hasNavigated()) {
  283. return promise.reject("navigated; resolution of _defaultNode aborted");
  284. }
  285. if (front) {
  286. return front;
  287. }
  288. return this.walker.documentElement();
  289. }).then(node => {
  290. if (hasNavigated()) {
  291. return promise.reject("navigated; resolution of _defaultNode aborted");
  292. }
  293. this._defaultNode = node;
  294. return node;
  295. });
  296. },
  297. /**
  298. * Target getter.
  299. */
  300. get target() {
  301. return this._target;
  302. },
  303. /**
  304. * Target setter.
  305. */
  306. set target(value) {
  307. this._target = value;
  308. },
  309. /**
  310. * Indicate that a tool has modified the state of the page. Used to
  311. * decide whether to show the "are you sure you want to navigate"
  312. * notification.
  313. */
  314. markDirty: function () {
  315. this.isDirty = true;
  316. },
  317. /**
  318. * Hooks the searchbar to show result and auto completion suggestions.
  319. */
  320. setupSearchBox: function () {
  321. this.searchBox = this.panelDoc.getElementById("inspector-searchbox");
  322. this.searchClearButton = this.panelDoc.getElementById("inspector-searchinput-clear");
  323. this.searchResultsLabel = this.panelDoc.getElementById("inspector-searchlabel");
  324. this.search = new InspectorSearch(this, this.searchBox, this.searchClearButton);
  325. this.search.on("search-cleared", this._updateSearchResultsLabel);
  326. this.search.on("search-result", this._updateSearchResultsLabel);
  327. let shortcuts = new KeyShortcuts({
  328. window: this.panelDoc.defaultView,
  329. });
  330. let key = INSPECTOR_L10N.getStr("inspector.searchHTML.key");
  331. shortcuts.on(key, (name, event) => {
  332. // Prevent overriding same shortcut from the computed/rule views
  333. if (event.target.closest("#sidebar-panel-ruleview") ||
  334. event.target.closest("#sidebar-panel-computedview")) {
  335. return;
  336. }
  337. event.preventDefault();
  338. this.searchBox.focus();
  339. });
  340. },
  341. get searchSuggestions() {
  342. return this.search.autocompleter;
  343. },
  344. _updateSearchResultsLabel: function (event, result) {
  345. let str = "";
  346. if (event !== "search-cleared") {
  347. if (result) {
  348. str = INSPECTOR_L10N.getFormatStr(
  349. "inspector.searchResultsCount2", result.resultsIndex + 1, result.resultsLength);
  350. } else {
  351. str = INSPECTOR_L10N.getStr("inspector.searchResultsNone");
  352. }
  353. }
  354. this.searchResultsLabel.textContent = str;
  355. },
  356. get React() {
  357. return this._toolbox.React;
  358. },
  359. get ReactDOM() {
  360. return this._toolbox.ReactDOM;
  361. },
  362. get ReactRedux() {
  363. return this._toolbox.ReactRedux;
  364. },
  365. get browserRequire() {
  366. return this._toolbox.browserRequire;
  367. },
  368. get InspectorTabPanel() {
  369. if (!this._InspectorTabPanel) {
  370. this._InspectorTabPanel =
  371. this.React.createFactory(this.browserRequire(
  372. "devtools/client/inspector/components/inspector-tab-panel"));
  373. }
  374. return this._InspectorTabPanel;
  375. },
  376. /**
  377. * Check if the inspector should use the landscape mode.
  378. *
  379. * @return {Boolean} true if the inspector should be in landscape mode.
  380. */
  381. useLandscapeMode: function () {
  382. let { clientWidth } = this.panelDoc.getElementById("inspector-splitter-box");
  383. return clientWidth > PORTRAIT_MODE_WIDTH;
  384. },
  385. /**
  386. * Build Splitter located between the main and side area of
  387. * the Inspector panel.
  388. */
  389. setupSplitter: function () {
  390. let SplitBox = this.React.createFactory(this.browserRequire(
  391. "devtools/client/shared/components/splitter/split-box"));
  392. let splitter = SplitBox({
  393. className: "inspector-sidebar-splitter",
  394. initialWidth: INITIAL_SIDEBAR_SIZE,
  395. initialHeight: INITIAL_SIDEBAR_SIZE,
  396. splitterSize: 1,
  397. endPanelControl: true,
  398. startPanel: this.InspectorTabPanel({
  399. id: "inspector-main-content"
  400. }),
  401. endPanel: this.InspectorTabPanel({
  402. id: "inspector-sidebar-container"
  403. }),
  404. vert: this.useLandscapeMode(),
  405. });
  406. this._splitter = this.ReactDOM.render(splitter,
  407. this.panelDoc.getElementById("inspector-splitter-box"));
  408. this.panelWin.addEventListener("resize", this.onPanelWindowResize, true);
  409. // Persist splitter state in preferences.
  410. this.sidebar.on("show", this.onSidebarShown);
  411. this.sidebar.on("hide", this.onSidebarHidden);
  412. this.sidebar.on("destroy", this.onSidebarHidden);
  413. },
  414. /**
  415. * Splitter clean up.
  416. */
  417. teardownSplitter: function () {
  418. this.panelWin.removeEventListener("resize", this.onPanelWindowResize, true);
  419. this.sidebar.off("show", this.onSidebarShown);
  420. this.sidebar.off("hide", this.onSidebarHidden);
  421. this.sidebar.off("destroy", this.onSidebarHidden);
  422. },
  423. /**
  424. * If Toolbox width is less than 600 px, the splitter changes its mode
  425. * to `horizontal` to support portrait view.
  426. */
  427. onPanelWindowResize: function () {
  428. this._splitter.setState({
  429. vert: this.useLandscapeMode(),
  430. });
  431. },
  432. onSidebarShown: function () {
  433. let width;
  434. let height;
  435. // Initialize splitter size from preferences.
  436. try {
  437. width = Services.prefs.getIntPref("devtools.toolsidebar-width.inspector");
  438. height = Services.prefs.getIntPref("devtools.toolsidebar-height.inspector");
  439. } catch (e) {
  440. // Set width and height of the splitter. Only one
  441. // value is really useful at a time depending on the current
  442. // orientation (vertical/horizontal).
  443. // Having both is supported by the splitter component.
  444. width = INITIAL_SIDEBAR_SIZE;
  445. height = INITIAL_SIDEBAR_SIZE;
  446. }
  447. this._splitter.setState({width, height});
  448. },
  449. onSidebarHidden: function () {
  450. // Store the current splitter size to preferences.
  451. let state = this._splitter.state;
  452. Services.prefs.setIntPref("devtools.toolsidebar-width.inspector", state.width);
  453. Services.prefs.setIntPref("devtools.toolsidebar-height.inspector", state.height);
  454. },
  455. /**
  456. * Build the sidebar.
  457. */
  458. setupSidebar: function () {
  459. let tabbox = this.panelDoc.querySelector("#inspector-sidebar");
  460. this.sidebar = new ToolSidebar(tabbox, this, "inspector", {
  461. showAllTabsMenu: true
  462. });
  463. let defaultTab = Services.prefs.getCharPref("devtools.inspector.activeSidebar");
  464. this._setDefaultSidebar = (event, toolId) => {
  465. Services.prefs.setCharPref("devtools.inspector.activeSidebar", toolId);
  466. };
  467. this.sidebar.on("select", this._setDefaultSidebar);
  468. if (!Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
  469. defaultTab == "fontinspector") {
  470. defaultTab = "ruleview";
  471. }
  472. // Append all side panels
  473. this.sidebar.addExistingTab(
  474. "ruleview",
  475. INSPECTOR_L10N.getStr("inspector.sidebar.ruleViewTitle"),
  476. defaultTab == "ruleview");
  477. this.sidebar.addExistingTab(
  478. "computedview",
  479. INSPECTOR_L10N.getStr("inspector.sidebar.computedViewTitle"),
  480. defaultTab == "computedview");
  481. this.ruleview = new RuleViewTool(this, this.panelWin);
  482. this.computedview = new ComputedViewTool(this, this.panelWin);
  483. if (Services.prefs.getBoolPref("devtools.layoutview.enabled")) {
  484. const {LayoutView} = this.browserRequire("devtools/client/inspector/layout/layout");
  485. this.layoutview = new LayoutView(this, this.panelWin);
  486. }
  487. if (this.target.form.animationsActor) {
  488. this.sidebar.addFrameTab(
  489. "animationinspector",
  490. INSPECTOR_L10N.getStr("inspector.sidebar.animationInspectorTitle"),
  491. "chrome://devtools/content/animationinspector/animation-inspector.xhtml",
  492. defaultTab == "animationinspector");
  493. }
  494. if (Services.prefs.getBoolPref("devtools.fontinspector.enabled") &&
  495. this.canGetUsedFontFaces) {
  496. this.sidebar.addExistingTab(
  497. "fontinspector",
  498. INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
  499. defaultTab == "fontinspector");
  500. this.fontInspector = new FontInspector(this, this.panelWin);
  501. this.sidebar.toggleTab(true, "fontinspector");
  502. }
  503. // Setup the splitter before the sidebar is displayed so,
  504. // we don't miss any events.
  505. this.setupSplitter();
  506. this.sidebar.show(defaultTab);
  507. },
  508. /**
  509. * Register a side-panel tab. This API can be used outside of
  510. * DevTools (e.g. from an extension) as well as by DevTools
  511. * code base.
  512. *
  513. * @param {string} tab uniq id
  514. * @param {string} title tab title
  515. * @param {React.Component} panel component. See `InspectorPanelTab` as an example.
  516. * @param {boolean} selected true if the panel should be selected
  517. */
  518. addSidebarTab: function (id, title, panel, selected) {
  519. this.sidebar.addTab(id, title, panel, selected);
  520. },
  521. setupToolbar: function () {
  522. this.teardownToolbar();
  523. // Setup the sidebar toggle button.
  524. let SidebarToggle = this.React.createFactory(this.browserRequire(
  525. "devtools/client/shared/components/sidebar-toggle"));
  526. let sidebarToggle = SidebarToggle({
  527. onClick: this.onPaneToggleButtonClicked,
  528. collapsed: false,
  529. expandPaneTitle: INSPECTOR_L10N.getStr("inspector.expandPane"),
  530. collapsePaneTitle: INSPECTOR_L10N.getStr("inspector.collapsePane"),
  531. });
  532. let parentBox = this.panelDoc.getElementById("inspector-sidebar-toggle-box");
  533. this._sidebarToggle = this.ReactDOM.render(sidebarToggle, parentBox);
  534. // Setup the add-node button.
  535. this.addNode = this.addNode.bind(this);
  536. this.addNodeButton = this.panelDoc.getElementById("inspector-element-add-button");
  537. this.addNodeButton.addEventListener("click", this.addNode);
  538. // Setup the eye-dropper icon if we're in an HTML document and we have actor support.
  539. if (this.selection.nodeFront && this.selection.nodeFront.isInHTMLDocument) {
  540. this.target.actorHasMethod("inspector", "pickColorFromPage").then(value => {
  541. if (!value) {
  542. return;
  543. }
  544. this.onEyeDropperDone = this.onEyeDropperDone.bind(this);
  545. this.onEyeDropperButtonClicked = this.onEyeDropperButtonClicked.bind(this);
  546. this.eyeDropperButton = this.panelDoc
  547. .getElementById("inspector-eyedropper-toggle");
  548. this.eyeDropperButton.disabled = false;
  549. this.eyeDropperButton.title = INSPECTOR_L10N.getStr("inspector.eyedropper.label");
  550. this.eyeDropperButton.addEventListener("click", this.onEyeDropperButtonClicked);
  551. }, e => console.error(e));
  552. } else {
  553. let eyeDropperButton = this.panelDoc.getElementById("inspector-eyedropper-toggle");
  554. eyeDropperButton.disabled = true;
  555. eyeDropperButton.title = INSPECTOR_L10N.getStr("eyedropper.disabled.title");
  556. }
  557. },
  558. teardownToolbar: function () {
  559. this._sidebarToggle = null;
  560. if (this.addNodeButton) {
  561. this.addNodeButton.removeEventListener("click", this.addNode);
  562. this.addNodeButton = null;
  563. }
  564. if (this.eyeDropperButton) {
  565. this.eyeDropperButton.removeEventListener("click", this.onEyeDropperButtonClicked);
  566. this.eyeDropperButton = null;
  567. }
  568. },
  569. /**
  570. * Reset the inspector on new root mutation.
  571. */
  572. onNewRoot: function () {
  573. this._defaultNode = null;
  574. this.selection.setNodeFront(null);
  575. this._destroyMarkup();
  576. this.isDirty = false;
  577. let onNodeSelected = defaultNode => {
  578. // Cancel this promise resolution as a new one had
  579. // been queued up.
  580. if (this._pendingSelection != onNodeSelected) {
  581. return;
  582. }
  583. this._pendingSelection = null;
  584. this.selection.setNodeFront(defaultNode, "navigateaway");
  585. this._initMarkup();
  586. this.once("markuploaded", () => {
  587. if (!this.markup) {
  588. return;
  589. }
  590. this.markup.expandNode(this.selection.nodeFront);
  591. this.emit("new-root");
  592. });
  593. // Setup the toolbar again, since its content may depend on the current document.
  594. this.setupToolbar();
  595. };
  596. this._pendingSelection = onNodeSelected;
  597. this._getDefaultNodeForSelection()
  598. .then(onNodeSelected, this._handleRejectionIfNotDestroyed);
  599. },
  600. _selectionCssSelector: null,
  601. /**
  602. * Set the currently selected node unique css selector.
  603. * Will store the current target url along with it to allow pre-selection at
  604. * reload
  605. */
  606. set selectionCssSelector(cssSelector = null) {
  607. if (this._panelDestroyer) {
  608. return;
  609. }
  610. this._selectionCssSelector = {
  611. selector: cssSelector,
  612. url: this._target.url
  613. };
  614. },
  615. /**
  616. * Get the current selection unique css selector if any, that is, if a node
  617. * is actually selected and that node has been selected while on the same url
  618. */
  619. get selectionCssSelector() {
  620. if (this._selectionCssSelector &&
  621. this._selectionCssSelector.url === this._target.url) {
  622. return this._selectionCssSelector.selector;
  623. }
  624. return null;
  625. },
  626. /**
  627. * Can a new HTML element be inserted into the currently selected element?
  628. * @return {Boolean}
  629. */
  630. canAddHTMLChild: function () {
  631. let selection = this.selection;
  632. // Don't allow to insert an element into these elements. This should only
  633. // contain elements where walker.insertAdjacentHTML has no effect.
  634. let invalidTagNames = ["html", "iframe"];
  635. return selection.isHTMLNode() &&
  636. selection.isElementNode() &&
  637. !selection.isPseudoElementNode() &&
  638. !selection.isAnonymousNode() &&
  639. invalidTagNames.indexOf(
  640. selection.nodeFront.nodeName.toLowerCase()) === -1;
  641. },
  642. /**
  643. * When a new node is selected.
  644. */
  645. onNewSelection: function (event, value, reason) {
  646. if (reason === "selection-destroy") {
  647. return;
  648. }
  649. // Wait for all the known tools to finish updating and then let the
  650. // client know.
  651. let selection = this.selection.nodeFront;
  652. // Update the state of the add button in the toolbar depending on the
  653. // current selection.
  654. let btn = this.panelDoc.querySelector("#inspector-element-add-button");
  655. if (this.canAddHTMLChild()) {
  656. btn.removeAttribute("disabled");
  657. } else {
  658. btn.setAttribute("disabled", "true");
  659. }
  660. // On any new selection made by the user, store the unique css selector
  661. // of the selected node so it can be restored after reload of the same page
  662. if (this.canGetUniqueSelector &&
  663. this.selection.isElementNode()) {
  664. selection.getUniqueSelector().then(selector => {
  665. this.selectionCssSelector = selector;
  666. }, this._handleRejectionIfNotDestroyed);
  667. }
  668. let selfUpdate = this.updating("inspector-panel");
  669. executeSoon(() => {
  670. try {
  671. selfUpdate(selection);
  672. } catch (ex) {
  673. console.error(ex);
  674. }
  675. });
  676. },
  677. /**
  678. * Delay the "inspector-updated" notification while a tool
  679. * is updating itself. Returns a function that must be
  680. * invoked when the tool is done updating with the node
  681. * that the tool is viewing.
  682. */
  683. updating: function (name) {
  684. if (this._updateProgress && this._updateProgress.node != this.selection.nodeFront) {
  685. this.cancelUpdate();
  686. }
  687. if (!this._updateProgress) {
  688. // Start an update in progress.
  689. let self = this;
  690. this._updateProgress = {
  691. node: this.selection.nodeFront,
  692. outstanding: new Set(),
  693. checkDone: function () {
  694. if (this !== self._updateProgress) {
  695. return;
  696. }
  697. // Cancel update if there is no `selection` anymore.
  698. // It can happen if the inspector panel is already destroyed.
  699. if (!self.selection || (this.node !== self.selection.nodeFront)) {
  700. self.cancelUpdate();
  701. return;
  702. }
  703. if (this.outstanding.size !== 0) {
  704. return;
  705. }
  706. self._updateProgress = null;
  707. self.emit("inspector-updated", name);
  708. },
  709. };
  710. }
  711. let progress = this._updateProgress;
  712. let done = function () {
  713. progress.outstanding.delete(done);
  714. progress.checkDone();
  715. };
  716. progress.outstanding.add(done);
  717. return done;
  718. },
  719. /**
  720. * Cancel notification of inspector updates.
  721. */
  722. cancelUpdate: function () {
  723. this._updateProgress = null;
  724. },
  725. /**
  726. * When a node is deleted, select its parent node or the defaultNode if no
  727. * parent is found (may happen when deleting an iframe inside which the
  728. * node was selected).
  729. */
  730. onDetached: function (event, parentNode) {
  731. this.breadcrumbs.cutAfter(this.breadcrumbs.indexOf(parentNode));
  732. this.selection.setNodeFront(parentNode ? parentNode : this._defaultNode, "detached");
  733. },
  734. /**
  735. * Destroy the inspector.
  736. */
  737. destroy: function () {
  738. if (this._panelDestroyer) {
  739. return this._panelDestroyer;
  740. }
  741. if (this.walker) {
  742. this.walker.off("new-root", this.onNewRoot);
  743. this.pageStyle = null;
  744. }
  745. this.cancelUpdate();
  746. this.target.off("will-navigate", this._onBeforeNavigate);
  747. this.target.off("thread-paused", this.updateDebuggerPausedWarning);
  748. this.target.off("thread-resumed", this.updateDebuggerPausedWarning);
  749. this._toolbox.off("select", this.updateDebuggerPausedWarning);
  750. if (this.ruleview) {
  751. this.ruleview.destroy();
  752. }
  753. if (this.computedview) {
  754. this.computedview.destroy();
  755. }
  756. if (this.layoutview) {
  757. this.layoutview.destroy();
  758. }
  759. if (this.fontInspector) {
  760. this.fontInspector.destroy();
  761. }
  762. let cssPropertiesDestroyer = this._cssPropertiesLoaded.then(({front}) => {
  763. if (front) {
  764. front.destroy();
  765. }
  766. });
  767. this.sidebar.off("select", this._setDefaultSidebar);
  768. let sidebarDestroyer = this.sidebar.destroy();
  769. this.teardownSplitter();
  770. this.sidebar = null;
  771. this.teardownToolbar();
  772. this.breadcrumbs.destroy();
  773. this.selection.off("new-node-front", this.onNewSelection);
  774. this.selection.off("detached-front", this.onDetached);
  775. let markupDestroyer = this._destroyMarkup();
  776. this.panelWin.inspector = null;
  777. this.target = null;
  778. this.panelDoc = null;
  779. this.panelWin = null;
  780. this.breadcrumbs = null;
  781. this._toolbox = null;
  782. this.search.destroy();
  783. this.search = null;
  784. this.searchBox = null;
  785. this._panelDestroyer = promise.all([
  786. sidebarDestroyer,
  787. markupDestroyer,
  788. cssPropertiesDestroyer
  789. ]);
  790. return this._panelDestroyer;
  791. },
  792. /**
  793. * Returns the clipboard content if it is appropriate for pasting
  794. * into the current node's outer HTML, otherwise returns null.
  795. */
  796. _getClipboardContentForPaste: function () {
  797. let flavors = clipboardHelper.getCurrentFlavors();
  798. if (flavors.indexOf("text") != -1 ||
  799. (flavors.indexOf("html") != -1 && flavors.indexOf("image") == -1)) {
  800. let content = clipboardHelper.getData();
  801. if (content && content.trim().length > 0) {
  802. return content;
  803. }
  804. }
  805. return null;
  806. },
  807. _onContextMenu: function (e) {
  808. e.preventDefault();
  809. this._openMenu({
  810. screenX: e.screenX,
  811. screenY: e.screenY,
  812. target: e.target,
  813. });
  814. },
  815. /**
  816. * This is meant to be called by all the search, filter, inplace text boxes in the
  817. * inspector, and just calls through to the toolbox openTextBoxContextMenu helper.
  818. * @param {DOMEvent} e
  819. */
  820. onTextBoxContextMenu: function (e) {
  821. e.stopPropagation();
  822. e.preventDefault();
  823. this.toolbox.openTextBoxContextMenu(e.screenX, e.screenY);
  824. },
  825. _openMenu: function ({ target, screenX = 0, screenY = 0 } = { }) {
  826. let markupContainer = this.markup.getContainer(this.selection.nodeFront);
  827. this.contextMenuTarget = target;
  828. this.nodeMenuTriggerInfo = markupContainer &&
  829. markupContainer.editor.getInfoAtNode(target);
  830. let isSelectionElement = this.selection.isElementNode() &&
  831. !this.selection.isPseudoElementNode();
  832. let isEditableElement = isSelectionElement &&
  833. !this.selection.isAnonymousNode();
  834. let isDuplicatableElement = isSelectionElement &&
  835. !this.selection.isAnonymousNode() &&
  836. !this.selection.isRoot();
  837. let isScreenshotable = isSelectionElement &&
  838. this.canGetUniqueSelector &&
  839. this.selection.nodeFront.isTreeDisplayed;
  840. let menu = new Menu();
  841. menu.append(new MenuItem({
  842. id: "node-menu-edithtml",
  843. label: INSPECTOR_L10N.getStr("inspectorHTMLEdit.label"),
  844. accesskey: INSPECTOR_L10N.getStr("inspectorHTMLEdit.accesskey"),
  845. disabled: !isEditableElement || !this.isOuterHTMLEditable,
  846. click: () => this.editHTML(),
  847. }));
  848. menu.append(new MenuItem({
  849. id: "node-menu-add",
  850. label: INSPECTOR_L10N.getStr("inspectorAddNode.label"),
  851. accesskey: INSPECTOR_L10N.getStr("inspectorAddNode.accesskey"),
  852. disabled: !this.canAddHTMLChild(),
  853. click: () => this.addNode(),
  854. }));
  855. menu.append(new MenuItem({
  856. id: "node-menu-duplicatenode",
  857. label: INSPECTOR_L10N.getStr("inspectorDuplicateNode.label"),
  858. hidden: !this._supportsDuplicateNode,
  859. disabled: !isDuplicatableElement,
  860. click: () => this.duplicateNode(),
  861. }));
  862. menu.append(new MenuItem({
  863. id: "node-menu-delete",
  864. label: INSPECTOR_L10N.getStr("inspectorHTMLDelete.label"),
  865. accesskey: INSPECTOR_L10N.getStr("inspectorHTMLDelete.accesskey"),
  866. disabled: !isEditableElement,
  867. click: () => this.deleteNode(),
  868. }));
  869. menu.append(new MenuItem({
  870. label: INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.label"),
  871. accesskey:
  872. INSPECTOR_L10N.getStr("inspectorAttributesSubmenu.accesskey"),
  873. submenu: this._getAttributesSubmenu(isEditableElement),
  874. }));
  875. menu.append(new MenuItem({
  876. type: "separator",
  877. }));
  878. // Set the pseudo classes
  879. for (let name of ["hover", "active", "focus"]) {
  880. let menuitem = new MenuItem({
  881. id: "node-menu-pseudo-" + name,
  882. label: name,
  883. type: "checkbox",
  884. click: this.togglePseudoClass.bind(this, ":" + name),
  885. });
  886. if (isSelectionElement) {
  887. let checked = this.selection.nodeFront.hasPseudoClassLock(":" + name);
  888. menuitem.checked = checked;
  889. } else {
  890. menuitem.disabled = true;
  891. }
  892. menu.append(menuitem);
  893. }
  894. menu.append(new MenuItem({
  895. type: "separator",
  896. }));
  897. let copySubmenu = new Menu();
  898. copySubmenu.append(new MenuItem({
  899. id: "node-menu-copyinner",
  900. label: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.label"),
  901. accesskey: INSPECTOR_L10N.getStr("inspectorCopyInnerHTML.accesskey"),
  902. disabled: !isSelectionElement,
  903. click: () => this.copyInnerHTML(),
  904. }));
  905. copySubmenu.append(new MenuItem({
  906. id: "node-menu-copyouter",
  907. label: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.label"),
  908. accesskey: INSPECTOR_L10N.getStr("inspectorCopyOuterHTML.accesskey"),
  909. disabled: !isSelectionElement,
  910. click: () => this.copyOuterHTML(),
  911. }));
  912. copySubmenu.append(new MenuItem({
  913. id: "node-menu-copyuniqueselector",
  914. label: INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.label"),
  915. accesskey:
  916. INSPECTOR_L10N.getStr("inspectorCopyCSSSelector.accesskey"),
  917. disabled: !isSelectionElement,
  918. hidden: !this.canGetUniqueSelector,
  919. click: () => this.copyUniqueSelector(),
  920. }));
  921. copySubmenu.append(new MenuItem({
  922. id: "node-menu-copycsspath",
  923. label: INSPECTOR_L10N.getStr("inspectorCopyCSSPath.label"),
  924. accesskey:
  925. INSPECTOR_L10N.getStr("inspectorCopyCSSPath.accesskey"),
  926. disabled: !isSelectionElement,
  927. hidden: !this.canGetCssPath,
  928. click: () => this.copyCssPath(),
  929. }));
  930. copySubmenu.append(new MenuItem({
  931. id: "node-menu-copyimagedatauri",
  932. label: INSPECTOR_L10N.getStr("inspectorImageDataUri.label"),
  933. disabled: !isSelectionElement || !markupContainer ||
  934. !markupContainer.isPreviewable(),
  935. click: () => this.copyImageDataUri(),
  936. }));
  937. menu.append(new MenuItem({
  938. label: INSPECTOR_L10N.getStr("inspectorCopyHTMLSubmenu.label"),
  939. submenu: copySubmenu,
  940. }));
  941. menu.append(new MenuItem({
  942. label: INSPECTOR_L10N.getStr("inspectorPasteHTMLSubmenu.label"),
  943. submenu: this._getPasteSubmenu(isEditableElement),
  944. }));
  945. menu.append(new MenuItem({
  946. type: "separator",
  947. }));
  948. let isNodeWithChildren = this.selection.isNode() &&
  949. markupContainer.hasChildren;
  950. menu.append(new MenuItem({
  951. id: "node-menu-expand",
  952. label: INSPECTOR_L10N.getStr("inspectorExpandNode.label"),
  953. disabled: !isNodeWithChildren,
  954. click: () => this.expandNode(),
  955. }));
  956. menu.append(new MenuItem({
  957. id: "node-menu-collapse",
  958. label: INSPECTOR_L10N.getStr("inspectorCollapseNode.label"),
  959. disabled: !isNodeWithChildren || !markupContainer.expanded,
  960. click: () => this.collapseNode(),
  961. }));
  962. menu.append(new MenuItem({
  963. type: "separator",
  964. }));
  965. menu.append(new MenuItem({
  966. id: "node-menu-scrollnodeintoview",
  967. label: INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.label"),
  968. accesskey:
  969. INSPECTOR_L10N.getStr("inspectorScrollNodeIntoView.accesskey"),
  970. hidden: !this._supportsScrollIntoView,
  971. disabled: !isSelectionElement,
  972. click: () => this.scrollNodeIntoView(),
  973. }));
  974. menu.append(new MenuItem({
  975. id: "node-menu-screenshotnode",
  976. label: INSPECTOR_L10N.getStr("inspectorScreenshotNode.label"),
  977. disabled: !isScreenshotable,
  978. click: () => this.screenshotNode(),
  979. }));
  980. menu.append(new MenuItem({
  981. id: "node-menu-useinconsole",
  982. label: INSPECTOR_L10N.getStr("inspectorUseInConsole.label"),
  983. click: () => this.useInConsole(),
  984. }));
  985. menu.append(new MenuItem({
  986. id: "node-menu-showdomproperties",
  987. label: INSPECTOR_L10N.getStr("inspectorShowDOMProperties.label"),
  988. click: () => this.showDOMProperties(),
  989. }));
  990. let nodeLinkMenuItems = this._getNodeLinkMenuItems();
  991. if (nodeLinkMenuItems.filter(item => item.visible).length > 0) {
  992. menu.append(new MenuItem({
  993. id: "node-menu-link-separator",
  994. type: "separator",
  995. }));
  996. }
  997. for (let menuitem of nodeLinkMenuItems) {
  998. menu.append(menuitem);
  999. }
  1000. menu.popup(screenX, screenY, this._toolbox);
  1001. return menu;
  1002. },
  1003. _getPasteSubmenu: function (isEditableElement) {
  1004. let isPasteable = isEditableElement && this._getClipboardContentForPaste();
  1005. let disableAdjacentPaste = !isPasteable ||
  1006. !this.canPasteInnerOrAdjacentHTML || this.selection.isRoot() ||
  1007. this.selection.isBodyNode() || this.selection.isHeadNode();
  1008. let disableFirstLastPaste = !isPasteable ||
  1009. !this.canPasteInnerOrAdjacentHTML || (this.selection.isHTMLNode() &&
  1010. this.selection.isRoot());
  1011. let pasteSubmenu = new Menu();
  1012. pasteSubmenu.append(new MenuItem({
  1013. id: "node-menu-pasteinnerhtml",
  1014. label: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.label"),
  1015. accesskey: INSPECTOR_L10N.getStr("inspectorPasteInnerHTML.accesskey"),
  1016. disabled: !isPasteable || !this.canPasteInnerOrAdjacentHTML,
  1017. click: () => this.pasteInnerHTML(),
  1018. }));
  1019. pasteSubmenu.append(new MenuItem({
  1020. id: "node-menu-pasteouterhtml",
  1021. label: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.label"),
  1022. accesskey: INSPECTOR_L10N.getStr("inspectorPasteOuterHTML.accesskey"),
  1023. disabled: !isPasteable || !this.isOuterHTMLEditable,
  1024. click: () => this.pasteOuterHTML(),
  1025. }));
  1026. pasteSubmenu.append(new MenuItem({
  1027. id: "node-menu-pastebefore",
  1028. label: INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.label"),
  1029. accesskey:
  1030. INSPECTOR_L10N.getStr("inspectorHTMLPasteBefore.accesskey"),
  1031. disabled: disableAdjacentPaste,
  1032. click: () => this.pasteAdjacentHTML("beforeBegin"),
  1033. }));
  1034. pasteSubmenu.append(new MenuItem({
  1035. id: "node-menu-pasteafter",
  1036. label: INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.label"),
  1037. accesskey:
  1038. INSPECTOR_L10N.getStr("inspectorHTMLPasteAfter.accesskey"),
  1039. disabled: disableAdjacentPaste,
  1040. click: () => this.pasteAdjacentHTML("afterEnd"),
  1041. }));
  1042. pasteSubmenu.append(new MenuItem({
  1043. id: "node-menu-pastefirstchild",
  1044. label: INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.label"),
  1045. accesskey:
  1046. INSPECTOR_L10N.getStr("inspectorHTMLPasteFirstChild.accesskey"),
  1047. disabled: disableFirstLastPaste,
  1048. click: () => this.pasteAdjacentHTML("afterBegin"),
  1049. }));
  1050. pasteSubmenu.append(new MenuItem({
  1051. id: "node-menu-pastelastchild",
  1052. label: INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.label"),
  1053. accesskey:
  1054. INSPECTOR_L10N.getStr("inspectorHTMLPasteLastChild.accesskey"),
  1055. disabled: disableFirstLastPaste,
  1056. click: () => this.pasteAdjacentHTML("beforeEnd"),
  1057. }));
  1058. return pasteSubmenu;
  1059. },
  1060. _getAttributesSubmenu: function (isEditableElement) {
  1061. let attributesSubmenu = new Menu();
  1062. let nodeInfo = this.nodeMenuTriggerInfo;
  1063. let isAttributeClicked = isEditableElement && nodeInfo &&
  1064. nodeInfo.type === "attribute";
  1065. attributesSubmenu.append(new MenuItem({
  1066. id: "node-menu-add-attribute",
  1067. label: INSPECTOR_L10N.getStr("inspectorAddAttribute.label"),
  1068. accesskey: INSPECTOR_L10N.getStr("inspectorAddAttribute.accesskey"),
  1069. disabled: !isEditableElement,
  1070. click: () => this.onAddAttribute(),
  1071. }));
  1072. attributesSubmenu.append(new MenuItem({
  1073. id: "node-menu-edit-attribute",
  1074. label: INSPECTOR_L10N.getFormatStr("inspectorEditAttribute.label",
  1075. isAttributeClicked ? `"${nodeInfo.name}"` : ""),
  1076. accesskey: INSPECTOR_L10N.getStr("inspectorEditAttribute.accesskey"),
  1077. disabled: !isAttributeClicked,
  1078. click: () => this.onEditAttribute(),
  1079. }));
  1080. attributesSubmenu.append(new MenuItem({
  1081. id: "node-menu-remove-attribute",
  1082. label: INSPECTOR_L10N.getFormatStr("inspectorRemoveAttribute.label",
  1083. isAttributeClicked ? `"${nodeInfo.name}"` : ""),
  1084. accesskey:
  1085. INSPECTOR_L10N.getStr("inspectorRemoveAttribute.accesskey"),
  1086. disabled: !isAttributeClicked,
  1087. click: () => this.onRemoveAttribute(),
  1088. }));
  1089. return attributesSubmenu;
  1090. },
  1091. /**
  1092. * Link menu items can be shown or hidden depending on the context and
  1093. * selected node, and their labels can vary.
  1094. *
  1095. * @return {Array} list of visible menu items related to links.
  1096. */
  1097. _getNodeLinkMenuItems: function () {
  1098. let linkFollow = new MenuItem({
  1099. id: "node-menu-link-follow",
  1100. visible: false,
  1101. click: () => this.onFollowLink(),
  1102. });
  1103. let linkCopy = new MenuItem({
  1104. id: "node-menu-link-copy",
  1105. visible: false,
  1106. click: () => this.onCopyLink(),
  1107. });
  1108. // Get information about the right-clicked node.
  1109. let popupNode = this.contextMenuTarget;
  1110. if (!popupNode || !popupNode.classList.contains("link")) {
  1111. return [linkFollow, linkCopy];
  1112. }
  1113. let type = popupNode.dataset.type;
  1114. if (this._supportsResolveRelativeURL &&
  1115. (type === "uri" || type === "cssresource" || type === "jsresource")) {
  1116. // Links can't be opened in new tabs in the browser toolbox.
  1117. if (type === "uri" && !this.target.chrome) {
  1118. linkFollow.visible = true;
  1119. linkFollow.label = INSPECTOR_L10N.getStr(
  1120. "inspector.menu.openUrlInNewTab.label");
  1121. } else if (type === "cssresource") {
  1122. linkFollow.visible = true;
  1123. linkFollow.label = TOOLBOX_L10N.getStr(
  1124. "toolbox.viewCssSourceInStyleEditor.label");
  1125. } else if (type === "jsresource") {
  1126. linkFollow.visible = true;
  1127. linkFollow.label = TOOLBOX_L10N.getStr(
  1128. "toolbox.viewJsSourceInDebugger.label");
  1129. }
  1130. linkCopy.visible = true;
  1131. linkCopy.label = INSPECTOR_L10N.getStr(
  1132. "inspector.menu.copyUrlToClipboard.label");
  1133. } else if (type === "idref") {
  1134. linkFollow.visible = true;
  1135. linkFollow.label = INSPECTOR_L10N.getFormatStr(
  1136. "inspector.menu.selectElement.label", popupNode.dataset.link);
  1137. }
  1138. return [linkFollow, linkCopy];
  1139. },
  1140. _initMarkup: function () {
  1141. let doc = this.panelDoc;
  1142. this._markupBox = doc.getElementById("markup-box");
  1143. // create tool iframe
  1144. this._markupFrame = doc.createElement("iframe");
  1145. this._markupFrame.setAttribute("flex", "1");
  1146. this._markupFrame.setAttribute("tooltip", "aHTMLTooltip");
  1147. this._markupFrame.addEventListener("contextmenu", this._onContextMenu);
  1148. // This is needed to enable tooltips inside the iframe document.
  1149. this._markupFrame.addEventListener("load", this._onMarkupFrameLoad, true);
  1150. this._markupBox.setAttribute("collapsed", true);
  1151. this._markupBox.appendChild(this._markupFrame);
  1152. this._markupFrame.setAttribute("src", "chrome://devtools/content/inspector/markup/markup.xhtml");
  1153. this._markupFrame.setAttribute("aria-label",
  1154. INSPECTOR_L10N.getStr("inspector.panelLabel.markupView"));
  1155. },
  1156. _onMarkupFrameLoad: function () {
  1157. this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
  1158. this._markupFrame.contentWindow.focus();
  1159. this._markupBox.removeAttribute("collapsed");
  1160. this.markup = new MarkupView(this, this._markupFrame, this._toolbox.win);
  1161. this.emit("markuploaded");
  1162. },
  1163. _destroyMarkup: function () {
  1164. let destroyPromise;
  1165. if (this._markupFrame) {
  1166. this._markupFrame.removeEventListener("load", this._onMarkupFrameLoad, true);
  1167. this._markupFrame.removeEventListener("contextmenu", this._onContextMenu);
  1168. }
  1169. if (this.markup) {
  1170. destroyPromise = this.markup.destroy();
  1171. this.markup = null;
  1172. } else {
  1173. destroyPromise = promise.resolve();
  1174. }
  1175. if (this._markupFrame) {
  1176. this._markupFrame.parentNode.removeChild(this._markupFrame);
  1177. this._markupFrame = null;
  1178. }
  1179. this._markupBox = null;
  1180. return destroyPromise;
  1181. },
  1182. /**
  1183. * When the pane toggle button is clicked or pressed, toggle the pane, change the button
  1184. * state and tooltip.
  1185. */
  1186. onPaneToggleButtonClicked: function (e) {
  1187. let sidePaneContainer = this.panelDoc.querySelector(
  1188. "#inspector-splitter-box .controlled");
  1189. let isVisible = !this._sidebarToggle.state.collapsed;
  1190. // Make sure the sidebar has width and height attributes before collapsing
  1191. // because ViewHelpers needs it.
  1192. if (isVisible) {
  1193. let rect = sidePaneContainer.getBoundingClientRect();
  1194. if (!sidePaneContainer.hasAttribute("width")) {
  1195. sidePaneContainer.setAttribute("width", rect.width);
  1196. }
  1197. // always refresh the height attribute before collapsing, it could have
  1198. // been modified by resizing the container.
  1199. sidePaneContainer.setAttribute("height", rect.height);
  1200. }
  1201. let onAnimationDone = () => {
  1202. if (isVisible) {
  1203. this._sidebarToggle.setState({collapsed: true});
  1204. } else {
  1205. this._sidebarToggle.setState({collapsed: false});
  1206. }
  1207. };
  1208. ViewHelpers.togglePane({
  1209. visible: !isVisible,
  1210. animated: true,
  1211. delayed: true,
  1212. callback: onAnimationDone
  1213. }, sidePaneContainer);
  1214. },
  1215. onEyeDropperButtonClicked: function () {
  1216. this.eyeDropperButton.hasAttribute("checked")
  1217. ? this.hideEyeDropper()
  1218. : this.showEyeDropper();
  1219. },
  1220. startEyeDropperListeners: function () {
  1221. this.inspector.once("color-pick-canceled", this.onEyeDropperDone);
  1222. this.inspector.once("color-picked", this.onEyeDropperDone);
  1223. this.walker.once("new-root", this.onEyeDropperDone);
  1224. },
  1225. stopEyeDropperListeners: function () {
  1226. this.inspector.off("color-pick-canceled", this.onEyeDropperDone);
  1227. this.inspector.off("color-picked", this.onEyeDropperDone);
  1228. this.walker.off("new-root", this.onEyeDropperDone);
  1229. },
  1230. onEyeDropperDone: function () {
  1231. this.eyeDropperButton.removeAttribute("checked");
  1232. this.stopEyeDropperListeners();
  1233. },
  1234. /**
  1235. * Show the eyedropper on the page.
  1236. * @return {Promise} resolves when the eyedropper is visible.
  1237. */
  1238. showEyeDropper: function () {
  1239. // The eyedropper button doesn't exist, most probably because the actor doesn't
  1240. // support the pickColorFromPage, or because the page isn't HTML.
  1241. if (!this.eyeDropperButton) {
  1242. return null;
  1243. }
  1244. this.telemetry.toolOpened("toolbareyedropper");
  1245. this.eyeDropperButton.setAttribute("checked", "true");
  1246. this.startEyeDropperListeners();
  1247. return this.inspector.pickColorFromPage(this.toolbox, {copyOnSelect: true})
  1248. .catch(e => console.error(e));
  1249. },
  1250. /**
  1251. * Hide the eyedropper.
  1252. * @return {Promise} resolves when the eyedropper is hidden.
  1253. */
  1254. hideEyeDropper: function () {
  1255. // The eyedropper button doesn't exist, most probably because the actor doesn't
  1256. // support the pickColorFromPage, or because the page isn't HTML.
  1257. if (!this.eyeDropperButton) {
  1258. return null;
  1259. }
  1260. this.eyeDropperButton.removeAttribute("checked");
  1261. this.stopEyeDropperListeners();
  1262. return this.inspector.cancelPickColorFromPage()
  1263. .catch(e => console.error(e));
  1264. },
  1265. /**
  1266. * Create a new node as the last child of the current selection, expand the
  1267. * parent and select the new node.
  1268. */
  1269. addNode: Task.async(function* () {
  1270. if (!this.canAddHTMLChild()) {
  1271. return;
  1272. }
  1273. let html = "<div></div>";
  1274. // Insert the html and expect a childList markup mutation.
  1275. let onMutations = this.once("markupmutation");
  1276. let {nodes} = yield this.walker.insertAdjacentHTML(this.selection.nodeFront,
  1277. "beforeEnd", html);
  1278. yield onMutations;
  1279. // Select the new node (this will auto-expand its parent).
  1280. this.selection.setNodeFront(nodes[0], "node-inserted");
  1281. }),
  1282. /**
  1283. * Toggle a pseudo class.
  1284. */
  1285. togglePseudoClass: function (pseudo) {
  1286. if (this.selection.isElementNode()) {
  1287. let node = this.selection.nodeFront;
  1288. if (node.hasPseudoClassLock(pseudo)) {
  1289. return this.walker.removePseudoClassLock(node, pseudo, {parents: true});
  1290. }
  1291. let hierarchical = pseudo == ":hover" || pseudo == ":active";
  1292. return this.walker.addPseudoClassLock(node, pseudo, {parents: hierarchical});
  1293. }
  1294. return promise.resolve();
  1295. },
  1296. /**
  1297. * Show DOM properties
  1298. */
  1299. showDOMProperties: function () {
  1300. this._toolbox.openSplitConsole().then(() => {
  1301. let panel = this._toolbox.getPanel("webconsole");
  1302. let jsterm = panel.hud.jsterm;
  1303. jsterm.execute("inspect($0)");
  1304. jsterm.focus();
  1305. });
  1306. },
  1307. /**
  1308. * Use in Console.
  1309. *
  1310. * Takes the currently selected node in the inspector and assigns it to a
  1311. * temp variable on the content window. Also opens the split console and
  1312. * autofills it with the temp variable.
  1313. */
  1314. useInConsole: function () {
  1315. this._toolbox.openSplitConsole().then(() => {
  1316. let panel = this._toolbox.getPanel("webconsole");
  1317. let jsterm = panel.hud.jsterm;
  1318. let evalString = `{ let i = 0;
  1319. while (window.hasOwnProperty("temp" + i) && i < 1000) {
  1320. i++;
  1321. }
  1322. window["temp" + i] = $0;
  1323. "temp" + i;
  1324. }`;
  1325. let options = {
  1326. selectedNodeActor: this.selection.nodeFront.actorID,
  1327. };
  1328. jsterm.requestEvaluation(evalString, options).then((res) => {
  1329. jsterm.setInputValue(res.result);
  1330. this.emit("console-var-ready");
  1331. });
  1332. });
  1333. },
  1334. /**
  1335. * Edit the outerHTML of the selected Node.
  1336. */
  1337. editHTML: function () {
  1338. if (!this.selection.isNode()) {
  1339. return;
  1340. }
  1341. if (this.markup) {
  1342. this.markup.beginEditingOuterHTML(this.selection.nodeFront);
  1343. }
  1344. },
  1345. /**
  1346. * Paste the contents of the clipboard into the selected Node's outer HTML.
  1347. */
  1348. pasteOuterHTML: function () {
  1349. let content = this._getClipboardContentForPaste();
  1350. if (!content) {
  1351. return promise.reject("No clipboard content for paste");
  1352. }
  1353. let node = this.selection.nodeFront;
  1354. return this.markup.getNodeOuterHTML(node).then(oldContent => {
  1355. this.markup.updateNodeOuterHTML(node, content, oldContent);
  1356. });
  1357. },
  1358. /**
  1359. * Paste the contents of the clipboard into the selected Node's inner HTML.
  1360. */
  1361. pasteInnerHTML: function () {
  1362. let content = this._getClipboardContentForPaste();
  1363. if (!content) {
  1364. return promise.reject("No clipboard content for paste");
  1365. }
  1366. let node = this.selection.nodeFront;
  1367. return this.markup.getNodeInnerHTML(node).then(oldContent => {
  1368. this.markup.updateNodeInnerHTML(node, content, oldContent);
  1369. });
  1370. },
  1371. /**
  1372. * Paste the contents of the clipboard as adjacent HTML to the selected Node.
  1373. * @param position
  1374. * The position as specified for Element.insertAdjacentHTML
  1375. * (i.e. "beforeBegin", "afterBegin", "beforeEnd", "afterEnd").
  1376. */
  1377. pasteAdjacentHTML: function (position) {
  1378. let content = this._getClipboardContentForPaste();
  1379. if (!content) {
  1380. return promise.reject("No clipboard content for paste");
  1381. }
  1382. let node = this.selection.nodeFront;
  1383. return this.markup.insertAdjacentHTMLToNode(node, position, content);
  1384. },
  1385. /**
  1386. * Copy the innerHTML of the selected Node to the clipboard.
  1387. */
  1388. copyInnerHTML: function () {
  1389. if (!this.selection.isNode()) {
  1390. return;
  1391. }
  1392. this._copyLongString(this.walker.innerHTML(this.selection.nodeFront));
  1393. },
  1394. /**
  1395. * Copy the outerHTML of the selected Node to the clipboard.
  1396. */
  1397. copyOuterHTML: function () {
  1398. if (!this.selection.isNode()) {
  1399. return;
  1400. }
  1401. let node = this.selection.nodeFront;
  1402. switch (node.nodeType) {
  1403. case nodeConstants.ELEMENT_NODE :
  1404. this._copyLongString(this.walker.outerHTML(node));
  1405. break;
  1406. case nodeConstants.COMMENT_NODE :
  1407. this._getLongString(node.getNodeValue()).then(comment => {
  1408. clipboardHelper.copyString("<!--" + comment + "-->");
  1409. });
  1410. break;
  1411. case nodeConstants.DOCUMENT_TYPE_NODE :
  1412. clipboardHelper.copyString(node.doctypeString);
  1413. break;
  1414. }
  1415. },
  1416. /**
  1417. * Copy the data-uri for the currently selected image in the clipboard.
  1418. */
  1419. copyImageDataUri: function () {
  1420. let container = this.markup.getContainer(this.selection.nodeFront);
  1421. if (container && container.isPreviewable()) {
  1422. container.copyImageDataUri();
  1423. }
  1424. },
  1425. /**
  1426. * Copy the content of a longString (via a promise resolving a
  1427. * LongStringActor) to the clipboard
  1428. * @param {Promise} longStringActorPromise
  1429. * promise expected to resolve a LongStringActor instance
  1430. * @return {Promise} promise resolving (with no argument) when the
  1431. * string is sent to the clipboard
  1432. */
  1433. _copyLongString: function (longStringActorPromise) {
  1434. return this._getLongString(longStringActorPromise).then(string => {
  1435. clipboardHelper.copyString(string);
  1436. }).catch(e => console.error(e));
  1437. },
  1438. /**
  1439. * Retrieve the content of a longString (via a promise resolving a LongStringActor)
  1440. * @param {Promise} longStringActorPromise
  1441. * promise expected to resolve a LongStringActor instance
  1442. * @return {Promise} promise resolving with the retrieved string as argument
  1443. */
  1444. _getLongString: function (longStringActorPromise) {
  1445. return longStringActorPromise.then(longStringActor => {
  1446. return longStringActor.string().then(string => {
  1447. longStringActor.release().catch(e => console.error(e));
  1448. return string;
  1449. });
  1450. }).catch(e => console.error(e));
  1451. },
  1452. /**
  1453. * Copy a unique selector of the selected Node to the clipboard.
  1454. */
  1455. copyUniqueSelector: function () {
  1456. if (!this.selection.isNode()) {
  1457. return;
  1458. }
  1459. this.telemetry.toolOpened("copyuniquecssselector");
  1460. this.selection.nodeFront.getUniqueSelector().then(selector => {
  1461. clipboardHelper.copyString(selector);
  1462. }).catch(e => console.error);
  1463. },
  1464. /**
  1465. * Copy the full CSS Path of the selected Node to the clipboard.
  1466. */
  1467. copyCssPath: function () {
  1468. if (!this.selection.isNode()) {
  1469. return;
  1470. }
  1471. this.telemetry.toolOpened("copyfullcssselector");
  1472. this.selection.nodeFront.getCssPath().then(path => {
  1473. clipboardHelper.copyString(path);
  1474. }).catch(e => console.error);
  1475. },
  1476. /**
  1477. * Initiate gcli screenshot command on selected node
  1478. */
  1479. screenshotNode: function () {
  1480. CommandUtils.createRequisition(this._target, {
  1481. environment: CommandUtils.createEnvironment(this, "_target")
  1482. }).then(requisition => {
  1483. // Bug 1180314 - CssSelector might contain white space so need to make sure it is
  1484. // passed to screenshot as a single parameter. More work *might* be needed if
  1485. // CssSelector could contain escaped single- or double-quotes, backslashes, etc.
  1486. requisition.updateExec("screenshot --selector '" + this.selectionCssSelector + "'");
  1487. });
  1488. },
  1489. /**
  1490. * Scroll the node into view.
  1491. */
  1492. scrollNodeIntoView: function () {
  1493. if (!this.selection.isNode()) {
  1494. return;
  1495. }
  1496. this.selection.nodeFront.scrollIntoView();
  1497. },
  1498. /**
  1499. * Duplicate the selected node
  1500. */
  1501. duplicateNode: function () {
  1502. let selection = this.selection;
  1503. if (!selection.isElementNode() ||
  1504. selection.isRoot() ||
  1505. selection.isAnonymousNode() ||
  1506. selection.isPseudoElementNode()) {
  1507. return;
  1508. }
  1509. this.walker.duplicateNode(selection.nodeFront).catch(e => console.error(e));
  1510. },
  1511. /**
  1512. * Delete the selected node.
  1513. */
  1514. deleteNode: function () {
  1515. if (!this.selection.isNode() ||
  1516. this.selection.isRoot()) {
  1517. return;
  1518. }
  1519. // If the markup panel is active, use the markup panel to delete
  1520. // the node, making this an undoable action.
  1521. if (this.markup) {
  1522. this.markup.deleteNode(this.selection.nodeFront);
  1523. } else {
  1524. // remove the node from content
  1525. this.walker.removeNode(this.selection.nodeFront);
  1526. }
  1527. },
  1528. /**
  1529. * Add attribute to node.
  1530. * Used for node context menu and shouldn't be called directly.
  1531. */
  1532. onAddAttribute: function () {
  1533. let container = this.markup.getContainer(this.selection.nodeFront);
  1534. container.addAttribute();
  1535. },
  1536. /**
  1537. * Edit attribute for node.
  1538. * Used for node context menu and shouldn't be called directly.
  1539. */
  1540. onEditAttribute: function () {
  1541. let container = this.markup.getContainer(this.selection.nodeFront);
  1542. container.editAttribute(this.nodeMenuTriggerInfo.name);
  1543. },
  1544. /**
  1545. * Remove attribute from node.
  1546. * Used for node context menu and shouldn't be called directly.
  1547. */
  1548. onRemoveAttribute: function () {
  1549. let container = this.markup.getContainer(this.selection.nodeFront);
  1550. container.removeAttribute(this.nodeMenuTriggerInfo.name);
  1551. },
  1552. expandNode: function () {
  1553. this.markup.expandAll(this.selection.nodeFront);
  1554. },
  1555. collapseNode: function () {
  1556. this.markup.collapseNode(this.selection.nodeFront);
  1557. },
  1558. /**
  1559. * This method is here for the benefit of the node-menu-link-follow menu item
  1560. * in the inspector contextual-menu.
  1561. */
  1562. onFollowLink: function () {
  1563. let type = this.contextMenuTarget.dataset.type;
  1564. let link = this.contextMenuTarget.dataset.link;
  1565. this.followAttributeLink(type, link);
  1566. },
  1567. /**
  1568. * Given a type and link found in a node's attribute in the markup-view,
  1569. * attempt to follow that link (which may result in opening a new tab, the
  1570. * style editor or debugger).
  1571. */
  1572. followAttributeLink: function (type, link) {
  1573. if (!type || !link) {
  1574. return;
  1575. }
  1576. if (type === "uri" || type === "cssresource" || type === "jsresource") {
  1577. // Open link in a new tab.
  1578. // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we
  1579. // already checked that resolveRelativeURL existed.
  1580. this.inspector.resolveRelativeURL(
  1581. link, this.selection.nodeFront).then(url => {
  1582. if (type === "uri") {
  1583. let browserWin = this.target.tab.ownerDocument.defaultView;
  1584. browserWin.openUILinkIn(url, "tab");
  1585. } else if (type === "cssresource") {
  1586. return this.toolbox.viewSourceInStyleEditor(url);
  1587. } else if (type === "jsresource") {
  1588. return this.toolbox.viewSourceInDebugger(url);
  1589. }
  1590. return null;
  1591. }).catch(e => console.error(e));
  1592. } else if (type == "idref") {
  1593. // Select the node in the same document.
  1594. this.walker.document(this.selection.nodeFront).then(doc => {
  1595. return this.walker.querySelector(doc, "#" + CSS.escape(link)).then(node => {
  1596. if (!node) {
  1597. this.emit("idref-attribute-link-failed");
  1598. return;
  1599. }
  1600. this.selection.setNodeFront(node);
  1601. });
  1602. }).catch(e => console.error(e));
  1603. }
  1604. },
  1605. /**
  1606. * This method is here for the benefit of the node-menu-link-copy menu item
  1607. * in the inspector contextual-menu.
  1608. */
  1609. onCopyLink: function () {
  1610. let link = this.contextMenuTarget.dataset.link;
  1611. this.copyAttributeLink(link);
  1612. },
  1613. /**
  1614. * This method is here for the benefit of copying links.
  1615. */
  1616. copyAttributeLink: function (link) {
  1617. // When the inspector menu was setup on click (see _getNodeLinkMenuItems), we
  1618. // already checked that resolveRelativeURL existed.
  1619. this.inspector.resolveRelativeURL(link, this.selection.nodeFront).then(url => {
  1620. clipboardHelper.copyString(url);
  1621. }, console.error);
  1622. }
  1623. };
  1624. // URL constructor doesn't support chrome: scheme
  1625. let href = window.location.href.replace(/chrome:/, "http://");
  1626. let url = new window.URL(href);
  1627. // Only use this method to attach the toolbox if some query parameters are given
  1628. if (url.search.length > 1) {
  1629. const { targetFromURL } = require("devtools/client/framework/target-from-url");
  1630. const { attachThread } = require("devtools/client/framework/attach-thread");
  1631. const { BrowserLoader } =
  1632. Cu.import("resource://devtools/client/shared/browser-loader.js", {});
  1633. const { Selection } = require("devtools/client/framework/selection");
  1634. const { InspectorFront } = require("devtools/shared/fronts/inspector");
  1635. const { getHighlighterUtils } = require("devtools/client/framework/toolbox-highlighter-utils");
  1636. Task.spawn(function* () {
  1637. let target = yield targetFromURL(url);
  1638. let notImplemented = function () {
  1639. throw new Error("Not implemented in a tab");
  1640. };
  1641. let fakeToolbox = {
  1642. target,
  1643. hostType: "bottom",
  1644. doc: window.document,
  1645. win: window,
  1646. on() {}, emit() {}, off() {},
  1647. initInspector() {},
  1648. browserRequire: BrowserLoader({
  1649. window: window,
  1650. useOnlyShared: true
  1651. }).require,
  1652. get React() {
  1653. return this.browserRequire("devtools/client/shared/vendor/react");
  1654. },
  1655. get ReactDOM() {
  1656. return this.browserRequire("devtools/client/shared/vendor/react-dom");
  1657. },
  1658. isToolRegistered() {
  1659. return false;
  1660. },
  1661. currentToolId: "inspector",
  1662. getCurrentPanel() {
  1663. return "inspector";
  1664. },
  1665. get textboxContextMenuPopup() {
  1666. notImplemented();
  1667. },
  1668. getPanel: notImplemented,
  1669. openSplitConsole: notImplemented,
  1670. viewCssSourceInStyleEditor: notImplemented,
  1671. viewJsSourceInDebugger: notImplemented,
  1672. viewSource: notImplemented,
  1673. viewSourceInDebugger: notImplemented,
  1674. viewSourceInStyleEditor: notImplemented,
  1675. // For attachThread:
  1676. highlightTool() {},
  1677. unhighlightTool() {},
  1678. selectTool() {},
  1679. raise() {},
  1680. getNotificationBox() {}
  1681. };
  1682. // attachThread also expect a toolbox as argument
  1683. fakeToolbox.threadClient = yield attachThread(fakeToolbox);
  1684. let inspector = InspectorFront(target.client, target.form);
  1685. let showAllAnonymousContent =
  1686. Services.prefs.getBoolPref("devtools.inspector.showAllAnonymousContent");
  1687. let walker = yield inspector.getWalker({ showAllAnonymousContent });
  1688. let selection = new Selection(walker);
  1689. let highlighter = yield inspector.getHighlighter(false);
  1690. fakeToolbox.inspector = inspector;
  1691. fakeToolbox.walker = walker;
  1692. fakeToolbox.selection = selection;
  1693. fakeToolbox.highlighter = highlighter;
  1694. fakeToolbox.highlighterUtils = getHighlighterUtils(fakeToolbox);
  1695. let inspectorUI = new Inspector(fakeToolbox);
  1696. inspectorUI.init();
  1697. }).then(null, e => {
  1698. window.alert("Unable to start the inspector:" + e.message + "\n" + e.stack);
  1699. });
  1700. }