StyleEditorUI.jsm 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073
  1. /* vim:set ts=2 sw=2 sts=2 et: */
  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. "use strict";
  6. this.EXPORTED_SYMBOLS = ["StyleEditorUI"];
  7. const Ci = Components.interfaces;
  8. const Cu = Components.utils;
  9. const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
  10. const Services = require("Services");
  11. const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
  12. const {OS} = require("resource://gre/modules/osfile.jsm");
  13. const {Task} = require("devtools/shared/task");
  14. const EventEmitter = require("devtools/shared/event-emitter");
  15. const {gDevTools} = require("devtools/client/framework/devtools");
  16. const {
  17. getString,
  18. text,
  19. wire,
  20. showFilePicker,
  21. } = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
  22. const {SplitView} = require("resource://devtools/client/shared/SplitView.jsm");
  23. const {StyleSheetEditor} = require("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
  24. const {PluralForm} = require("devtools/shared/plural-form");
  25. const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
  26. const csscoverage = require("devtools/shared/fronts/csscoverage");
  27. const {console} = require("resource://gre/modules/Console.jsm");
  28. const promise = require("promise");
  29. const defer = require("devtools/shared/defer");
  30. const {ResponsiveUIManager} =
  31. require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
  32. const {KeyCodes} = require("devtools/client/shared/keycodes");
  33. const LOAD_ERROR = "error-load";
  34. const STYLE_EDITOR_TEMPLATE = "stylesheet";
  35. const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
  36. const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
  37. const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
  38. const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
  39. /**
  40. * StyleEditorUI is controls and builds the UI of the Style Editor, including
  41. * maintaining a list of editors for each stylesheet on a debuggee.
  42. *
  43. * Emits events:
  44. * 'editor-added': A new editor was added to the UI
  45. * 'editor-selected': An editor was selected
  46. * 'error': An error occured
  47. *
  48. * @param {StyleEditorFront} debuggee
  49. * Client-side front for interacting with the page's stylesheets
  50. * @param {Target} target
  51. * Interface for the page we're debugging
  52. * @param {Document} panelDoc
  53. * Document of the toolbox panel to populate UI in.
  54. * @param {CssProperties} A css properties database.
  55. */
  56. function StyleEditorUI(debuggee, target, panelDoc, cssProperties) {
  57. EventEmitter.decorate(this);
  58. this._debuggee = debuggee;
  59. this._target = target;
  60. this._panelDoc = panelDoc;
  61. this._cssProperties = cssProperties;
  62. this._window = this._panelDoc.defaultView;
  63. this._root = this._panelDoc.getElementById("style-editor-chrome");
  64. this.editors = [];
  65. this.selectedEditor = null;
  66. this.savedLocations = {};
  67. this._seenSheets = new Map();
  68. // Don't add any style sheets that might arrive via events, until
  69. // the call to initialize. Style sheets can arrive from the server
  70. // at any time, for example if a new style sheet was added, or if
  71. // the style sheet actor was just created and is walking the style
  72. // sheets for the first time. In any case, in |initialize| we're
  73. // going to fetch the list of sheets anyway.
  74. this._suppressAdd = true;
  75. this._onOptionsPopupShowing = this._onOptionsPopupShowing.bind(this);
  76. this._onOptionsPopupHiding = this._onOptionsPopupHiding.bind(this);
  77. this._onNewDocument = this._onNewDocument.bind(this);
  78. this._onMediaPrefChanged = this._onMediaPrefChanged.bind(this);
  79. this._updateMediaList = this._updateMediaList.bind(this);
  80. this._clear = this._clear.bind(this);
  81. this._onError = this._onError.bind(this);
  82. this._updateOpenLinkItem = this._updateOpenLinkItem.bind(this);
  83. this._openLinkNewTab = this._openLinkNewTab.bind(this);
  84. this._addStyleSheet = this._addStyleSheet.bind(this);
  85. this._prefObserver = new PrefObserver("devtools.styleeditor.");
  86. this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument);
  87. this._prefObserver.on(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged);
  88. this._debuggee.on("stylesheet-added", this._addStyleSheet);
  89. }
  90. this.StyleEditorUI = StyleEditorUI;
  91. StyleEditorUI.prototype = {
  92. /**
  93. * Get whether any of the editors have unsaved changes.
  94. *
  95. * @return boolean
  96. */
  97. get isDirty() {
  98. if (this._markedDirty === true) {
  99. return true;
  100. }
  101. return this.editors.some((editor) => {
  102. return editor.sourceEditor && !editor.sourceEditor.isClean();
  103. });
  104. },
  105. /*
  106. * Mark the style editor as having or not having unsaved changes.
  107. */
  108. set isDirty(value) {
  109. this._markedDirty = value;
  110. },
  111. /*
  112. * Index of selected stylesheet in document.styleSheets
  113. */
  114. get selectedStyleSheetIndex() {
  115. return this.selectedEditor ?
  116. this.selectedEditor.styleSheet.styleSheetIndex : -1;
  117. },
  118. /**
  119. * Initiates the style editor ui creation, the inspector front to get
  120. * reference to the walker and the selector highlighter if available
  121. */
  122. initialize: Task.async(function* () {
  123. yield this.initializeHighlighter();
  124. this.createUI();
  125. let styleSheets = yield this._debuggee.getStyleSheets();
  126. yield this._resetStyleSheetList(styleSheets);
  127. this._target.on("will-navigate", this._clear);
  128. this._target.on("navigate", this._onNewDocument);
  129. }),
  130. initializeHighlighter: Task.async(function* () {
  131. let toolbox = gDevTools.getToolbox(this._target);
  132. yield toolbox.initInspector();
  133. this._walker = toolbox.walker;
  134. let hUtils = toolbox.highlighterUtils;
  135. if (hUtils.supportsCustomHighlighters()) {
  136. try {
  137. this._highlighter =
  138. yield hUtils.getHighlighterByType(SELECTOR_HIGHLIGHTER_TYPE);
  139. } catch (e) {
  140. // The selectorHighlighter can't always be instantiated, for example
  141. // it doesn't work with XUL windows (until bug 1094959 gets fixed);
  142. // or the selectorHighlighter doesn't exist on the backend.
  143. console.warn("The selectorHighlighter couldn't be instantiated, " +
  144. "elements matching hovered selectors will not be highlighted");
  145. }
  146. }
  147. }),
  148. /**
  149. * Build the initial UI and wire buttons with event handlers.
  150. */
  151. createUI: function () {
  152. let viewRoot = this._root.parentNode.querySelector(".splitview-root");
  153. this._view = new SplitView(viewRoot);
  154. wire(this._view.rootElement, ".style-editor-newButton", () =>{
  155. this._debuggee.addStyleSheet(null);
  156. });
  157. wire(this._view.rootElement, ".style-editor-importButton", ()=> {
  158. this._importFromFile(this._mockImportFile || null, this._window);
  159. });
  160. this._optionsButton = this._panelDoc.getElementById("style-editor-options");
  161. this._panelDoc.addEventListener("contextmenu", () => {
  162. this._contextMenuStyleSheet = null;
  163. }, true);
  164. this._contextMenu = this._panelDoc.getElementById("sidebar-context");
  165. this._contextMenu.addEventListener("popupshowing",
  166. this._updateOpenLinkItem);
  167. this._optionsMenu =
  168. this._panelDoc.getElementById("style-editor-options-popup");
  169. this._optionsMenu.addEventListener("popupshowing",
  170. this._onOptionsPopupShowing);
  171. this._optionsMenu.addEventListener("popuphiding",
  172. this._onOptionsPopupHiding);
  173. this._sourcesItem = this._panelDoc.getElementById("options-origsources");
  174. this._sourcesItem.addEventListener("command",
  175. this._toggleOrigSources);
  176. this._mediaItem = this._panelDoc.getElementById("options-show-media");
  177. this._mediaItem.addEventListener("command",
  178. this._toggleMediaSidebar);
  179. this._openLinkNewTabItem =
  180. this._panelDoc.getElementById("context-openlinknewtab");
  181. this._openLinkNewTabItem.addEventListener("command",
  182. this._openLinkNewTab);
  183. let nav = this._panelDoc.querySelector(".splitview-controller");
  184. nav.setAttribute("width", Services.prefs.getIntPref(PREF_NAV_WIDTH));
  185. },
  186. /**
  187. * Listener handling the 'gear menu' popup showing event.
  188. * Update options menu items to reflect current preference settings.
  189. */
  190. _onOptionsPopupShowing: function () {
  191. this._optionsButton.setAttribute("open", "true");
  192. this._sourcesItem.setAttribute("checked",
  193. Services.prefs.getBoolPref(PREF_ORIG_SOURCES));
  194. this._mediaItem.setAttribute("checked",
  195. Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR));
  196. },
  197. /**
  198. * Listener handling the 'gear menu' popup hiding event.
  199. */
  200. _onOptionsPopupHiding: function () {
  201. this._optionsButton.removeAttribute("open");
  202. },
  203. /**
  204. * Refresh editors to reflect the stylesheets in the document.
  205. *
  206. * @param {string} event
  207. * Event name
  208. * @param {StyleSheet} styleSheet
  209. * StyleSheet object for new sheet
  210. */
  211. _onNewDocument: function () {
  212. this._suppressAdd = true;
  213. this._debuggee.getStyleSheets().then((styleSheets) => {
  214. return this._resetStyleSheetList(styleSheets);
  215. }).then(null, e => console.error(e));
  216. },
  217. /**
  218. * Add editors for all the given stylesheets to the UI.
  219. *
  220. * @param {array} styleSheets
  221. * Array of StyleSheetFront
  222. */
  223. _resetStyleSheetList: Task.async(function* (styleSheets) {
  224. this._clear();
  225. this._suppressAdd = false;
  226. for (let sheet of styleSheets) {
  227. try {
  228. yield this._addStyleSheet(sheet);
  229. } catch (e) {
  230. this.emit("error", { key: LOAD_ERROR });
  231. }
  232. }
  233. this._root.classList.remove("loading");
  234. this.emit("stylesheets-reset");
  235. }),
  236. /**
  237. * Remove all editors and add loading indicator.
  238. */
  239. _clear: function () {
  240. // remember selected sheet and line number for next load
  241. if (this.selectedEditor && this.selectedEditor.sourceEditor) {
  242. let href = this.selectedEditor.styleSheet.href;
  243. let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
  244. this._styleSheetToSelect = {
  245. stylesheet: href,
  246. line: line,
  247. col: ch
  248. };
  249. }
  250. // remember saved file locations
  251. for (let editor of this.editors) {
  252. if (editor.savedFile) {
  253. let identifier = this.getStyleSheetIdentifier(editor.styleSheet);
  254. this.savedLocations[identifier] = editor.savedFile;
  255. }
  256. }
  257. this._clearStyleSheetEditors();
  258. this._view.removeAll();
  259. this.selectedEditor = null;
  260. // Here the keys are style sheet actors, and the values are
  261. // promises that resolve to the sheet's editor. See |_addStyleSheet|.
  262. this._seenSheets = new Map();
  263. this._suppressAdd = true;
  264. this._root.classList.add("loading");
  265. },
  266. /**
  267. * Add an editor for this stylesheet. Add editors for its original sources
  268. * instead (e.g. Sass sources), if applicable.
  269. *
  270. * @param {StyleSheetFront} styleSheet
  271. * Style sheet to add to style editor
  272. * @param {Boolean} isNew
  273. * True if this style sheet was created by a call to the
  274. * style sheets actor's @see addStyleSheet method.
  275. * @return {Promise}
  276. * A promise that resolves to the style sheet's editor when the style sheet has
  277. * been fully loaded. If the style sheet has a source map, and source mapping
  278. * is enabled, then the promise resolves to null.
  279. */
  280. _addStyleSheet: function (styleSheet, isNew) {
  281. if (this._suppressAdd) {
  282. return null;
  283. }
  284. if (!this._seenSheets.has(styleSheet)) {
  285. let promise = (async () => {
  286. let editor = await this._addStyleSheetEditor(styleSheet, isNew);
  287. if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
  288. return editor;
  289. }
  290. let sources = await styleSheet.getOriginalSources();
  291. // A single generated sheet might map to multiple original
  292. // sheets, so make editors for each of them.
  293. if (sources && sources.length) {
  294. let parentEditorName = editor.friendlyName;
  295. this._removeStyleSheetEditor(editor);
  296. editor = null;
  297. for (let source of sources) {
  298. // set so the first sheet will be selected, even if it's a source
  299. source.styleSheetIndex = styleSheet.styleSheetIndex;
  300. source.relatedStyleSheet = styleSheet;
  301. source.relatedEditorName = parentEditorName;
  302. await this._addStyleSheetEditor(source);
  303. }
  304. }
  305. return editor;
  306. })();
  307. this._seenSheets.set(styleSheet, promise);
  308. }
  309. return this._seenSheets.get(styleSheet);
  310. },
  311. /**
  312. * Add a new editor to the UI for a source.
  313. *
  314. * @param {StyleSheet} styleSheet
  315. * Object representing stylesheet
  316. * @param {Boolean} isNew
  317. * Optional if stylesheet is a new sheet created by user
  318. * @return {Promise} that is resolved with the created StyleSheetEditor when
  319. * the editor is fully initialized or rejected on error.
  320. */
  321. _addStyleSheetEditor: Task.async(function* (styleSheet, isNew) {
  322. // recall location of saved file for this sheet after page reload
  323. let file = null;
  324. let identifier = this.getStyleSheetIdentifier(styleSheet);
  325. let savedFile = this.savedLocations[identifier];
  326. if (savedFile) {
  327. file = savedFile;
  328. }
  329. let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew,
  330. this._walker, this._highlighter);
  331. editor.on("property-change", this._summaryChange.bind(this, editor));
  332. editor.on("media-rules-changed", this._updateMediaList.bind(this, editor));
  333. editor.on("linked-css-file", this._summaryChange.bind(this, editor));
  334. editor.on("linked-css-file-error", this._summaryChange.bind(this, editor));
  335. editor.on("error", this._onError);
  336. this.editors.push(editor);
  337. yield editor.fetchSource();
  338. this._sourceLoaded(editor);
  339. return editor;
  340. }),
  341. /**
  342. * Import a style sheet from file and asynchronously create a
  343. * new stylesheet on the debuggee for it.
  344. *
  345. * @param {mixed} file
  346. * Optional nsIFile or filename string.
  347. * If not set a file picker will be shown.
  348. * @param {nsIWindow} parentWindow
  349. * Optional parent window for the file picker.
  350. */
  351. _importFromFile: function (file, parentWindow) {
  352. let onFileSelected = (selectedFile) => {
  353. if (!selectedFile) {
  354. // nothing selected
  355. return;
  356. }
  357. NetUtil.asyncFetch({
  358. uri: NetUtil.newURI(selectedFile),
  359. loadingNode: this._window.document,
  360. securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
  361. contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
  362. }, (stream, status) => {
  363. if (!Components.isSuccessCode(status)) {
  364. this.emit("error", { key: LOAD_ERROR });
  365. return;
  366. }
  367. let source =
  368. NetUtil.readInputStreamToString(stream, stream.available());
  369. stream.close();
  370. this._suppressAdd = true;
  371. this._debuggee.addStyleSheet(source).then((styleSheet) => {
  372. this._suppressAdd = false;
  373. this._addStyleSheet(styleSheet, true).then(editor => {
  374. if (editor) {
  375. editor.savedFile = selectedFile;
  376. }
  377. // Just for testing purposes.
  378. this.emit("test:editor-updated", editor);
  379. });
  380. });
  381. });
  382. };
  383. showFilePicker(file, false, parentWindow, onFileSelected);
  384. },
  385. /**
  386. * Forward any error from a stylesheet.
  387. *
  388. * @param {string} event
  389. * Event name
  390. * @param {data} data
  391. * The event data
  392. */
  393. _onError: function (event, data) {
  394. this.emit("error", data);
  395. },
  396. /**
  397. * Toggle the original sources pref.
  398. */
  399. _toggleOrigSources: function () {
  400. let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
  401. Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
  402. },
  403. /**
  404. * Toggle the pref for showing a @media rules sidebar in each editor.
  405. */
  406. _toggleMediaSidebar: function () {
  407. let isEnabled = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR);
  408. Services.prefs.setBoolPref(PREF_MEDIA_SIDEBAR, !isEnabled);
  409. },
  410. /**
  411. * Toggle the @media sidebar in each editor depending on the setting.
  412. */
  413. _onMediaPrefChanged: function () {
  414. this.editors.forEach(this._updateMediaList);
  415. },
  416. /**
  417. * This method handles the following cases related to the context
  418. * menu item "_openLinkNewTabItem":
  419. *
  420. * 1) There was a stylesheet clicked on and it is external: show and
  421. * enable the context menu item
  422. * 2) There was a stylesheet clicked on and it is inline: show and
  423. * disable the context menu item
  424. * 3) There was no stylesheet clicked on (the right click happened
  425. * below the list): hide the context menu
  426. */
  427. _updateOpenLinkItem: function () {
  428. this._openLinkNewTabItem.setAttribute("hidden",
  429. !this._contextMenuStyleSheet);
  430. if (this._contextMenuStyleSheet) {
  431. this._openLinkNewTabItem.setAttribute("disabled",
  432. !this._contextMenuStyleSheet.href);
  433. }
  434. },
  435. /**
  436. * Open a particular stylesheet in a new tab.
  437. */
  438. _openLinkNewTab: function () {
  439. if (this._contextMenuStyleSheet) {
  440. this._window.openUILinkIn(this._contextMenuStyleSheet.href, "tab");
  441. }
  442. },
  443. /**
  444. * Remove a particular stylesheet editor from the UI
  445. *
  446. * @param {StyleSheetEditor} editor
  447. * The editor to remove.
  448. */
  449. _removeStyleSheetEditor: function (editor) {
  450. if (editor.summary) {
  451. this._view.removeItem(editor.summary);
  452. } else {
  453. let self = this;
  454. this.on("editor-added", function onAdd(event, added) {
  455. if (editor == added) {
  456. self.off("editor-added", onAdd);
  457. self._view.removeItem(editor.summary);
  458. }
  459. });
  460. }
  461. editor.destroy();
  462. this.editors.splice(this.editors.indexOf(editor), 1);
  463. },
  464. /**
  465. * Clear all the editors from the UI.
  466. */
  467. _clearStyleSheetEditors: function () {
  468. for (let editor of this.editors) {
  469. editor.destroy();
  470. }
  471. this.editors = [];
  472. },
  473. /**
  474. * Called when a StyleSheetEditor's source has been fetched. Create a
  475. * summary UI for the editor.
  476. *
  477. * @param {StyleSheetEditor} editor
  478. * Editor to create UI for.
  479. */
  480. _sourceLoaded: function (editor) {
  481. let ordinal = editor.styleSheet.styleSheetIndex;
  482. ordinal = ordinal == -1 ? Number.MAX_SAFE_INTEGER : ordinal;
  483. // add new sidebar item and editor to the UI
  484. this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
  485. data: {
  486. editor: editor
  487. },
  488. disableAnimations: this._alwaysDisableAnimations,
  489. ordinal: ordinal,
  490. onCreate: function (summary, details, data) {
  491. let createdEditor = data.editor;
  492. createdEditor.summary = summary;
  493. createdEditor.details = details;
  494. wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) {
  495. event.stopPropagation();
  496. event.target.blur();
  497. createdEditor.toggleDisabled();
  498. });
  499. wire(summary, ".stylesheet-name", {
  500. events: {
  501. "keypress": (event) => {
  502. if (event.keyCode == KeyCodes.DOM_VK_RETURN) {
  503. this._view.activeSummary = summary;
  504. }
  505. }
  506. }
  507. });
  508. wire(summary, ".stylesheet-saveButton", function onSaveButton(event) {
  509. event.stopPropagation();
  510. event.target.blur();
  511. createdEditor.saveToFile(createdEditor.savedFile);
  512. });
  513. this._updateSummaryForEditor(createdEditor, summary);
  514. summary.addEventListener("contextmenu", () => {
  515. this._contextMenuStyleSheet = createdEditor.styleSheet;
  516. }, false);
  517. summary.addEventListener("focus", function onSummaryFocus(event) {
  518. if (event.target == summary) {
  519. // autofocus the stylesheet name
  520. summary.querySelector(".stylesheet-name").focus();
  521. }
  522. }, false);
  523. let sidebar = details.querySelector(".stylesheet-sidebar");
  524. sidebar.setAttribute("width",
  525. Services.prefs.getIntPref(PREF_SIDEBAR_WIDTH));
  526. let splitter = details.querySelector(".devtools-side-splitter");
  527. splitter.addEventListener("mousemove", () => {
  528. let sidebarWidth = sidebar.getAttribute("width");
  529. Services.prefs.setIntPref(PREF_SIDEBAR_WIDTH, sidebarWidth);
  530. // update all @media sidebars for consistency
  531. let sidebars =
  532. [...this._panelDoc.querySelectorAll(".stylesheet-sidebar")];
  533. for (let mediaSidebar of sidebars) {
  534. mediaSidebar.setAttribute("width", sidebarWidth);
  535. }
  536. });
  537. // autofocus if it's a new user-created stylesheet
  538. if (createdEditor.isNew) {
  539. this._selectEditor(createdEditor);
  540. }
  541. if (this._isEditorToSelect(createdEditor)) {
  542. this.switchToSelectedSheet();
  543. }
  544. // If this is the first stylesheet and there is no pending request to
  545. // select a particular style sheet, select this sheet.
  546. if (!this.selectedEditor && !this._styleSheetBoundToSelect
  547. && createdEditor.styleSheet.styleSheetIndex == 0) {
  548. this._selectEditor(createdEditor);
  549. }
  550. this.emit("editor-added", createdEditor);
  551. }.bind(this),
  552. onShow: function (summary, details, data) {
  553. let showEditor = data.editor;
  554. this.selectedEditor = showEditor;
  555. Task.spawn(function* () {
  556. if (!showEditor.sourceEditor) {
  557. // only initialize source editor when we switch to this view
  558. let inputElement =
  559. details.querySelector(".stylesheet-editor-input");
  560. yield showEditor.load(inputElement, this._cssProperties);
  561. }
  562. showEditor.onShow();
  563. this.emit("editor-selected", showEditor);
  564. // Is there any CSS coverage markup to include?
  565. let usage = yield csscoverage.getUsage(this._target);
  566. if (usage == null) {
  567. return;
  568. }
  569. let sheet = showEditor.styleSheet;
  570. let {reports} = yield usage.createEditorReportForSheet(sheet);
  571. showEditor.removeAllUnusedRegions();
  572. if (reports.length > 0) {
  573. // Only apply if this file isn't compressed. We detect a
  574. // compressed file if there are more rules than lines.
  575. let editorText = showEditor.sourceEditor.getText();
  576. let lineCount = editorText.split("\n").length;
  577. let ruleCount = showEditor.styleSheet.ruleCount;
  578. if (lineCount >= ruleCount) {
  579. showEditor.addUnusedRegions(reports);
  580. } else {
  581. this.emit("error", { key: "error-compressed", level: "info" });
  582. }
  583. }
  584. }.bind(this)).then(null, e => console.error(e));
  585. }.bind(this)
  586. });
  587. },
  588. /**
  589. * Switch to the editor that has been marked to be selected.
  590. *
  591. * @return {Promise}
  592. * Promise that will resolve when the editor is selected.
  593. */
  594. switchToSelectedSheet: function () {
  595. let toSelect = this._styleSheetToSelect;
  596. for (let editor of this.editors) {
  597. if (this._isEditorToSelect(editor)) {
  598. // The _styleSheetBoundToSelect will always hold the latest pending
  599. // requested style sheet (with line and column) which is not yet
  600. // selected by the source editor. Only after we select that particular
  601. // editor and go the required line and column, it will become null.
  602. this._styleSheetBoundToSelect = this._styleSheetToSelect;
  603. this._styleSheetToSelect = null;
  604. return this._selectEditor(editor, toSelect.line, toSelect.col);
  605. }
  606. }
  607. return promise.resolve();
  608. },
  609. /**
  610. * Returns whether a given editor is the current editor to be selected. Tests
  611. * based on href or underlying stylesheet.
  612. *
  613. * @param {StyleSheetEditor} editor
  614. * The editor to test.
  615. */
  616. _isEditorToSelect: function (editor) {
  617. let toSelect = this._styleSheetToSelect;
  618. if (!toSelect) {
  619. return false;
  620. }
  621. let isHref = toSelect.stylesheet === null ||
  622. typeof toSelect.stylesheet == "string";
  623. return (isHref && editor.styleSheet.href == toSelect.stylesheet) ||
  624. (toSelect.stylesheet == editor.styleSheet);
  625. },
  626. /**
  627. * Select an editor in the UI.
  628. *
  629. * @param {StyleSheetEditor} editor
  630. * Editor to switch to.
  631. * @param {number} line
  632. * Line number to jump to
  633. * @param {number} col
  634. * Column number to jump to
  635. * @return {Promise}
  636. * Promise that will resolve when the editor is selected and ready
  637. * to be used.
  638. */
  639. _selectEditor: function (editor, line, col) {
  640. line = line || 0;
  641. col = col || 0;
  642. let editorPromise = editor.getSourceEditor().then(() => {
  643. editor.sourceEditor.setCursor({line: line, ch: col});
  644. this._styleSheetBoundToSelect = null;
  645. });
  646. let summaryPromise = this.getEditorSummary(editor).then((summary) => {
  647. this._view.activeSummary = summary;
  648. });
  649. return promise.all([editorPromise, summaryPromise]);
  650. },
  651. getEditorSummary: function (editor) {
  652. if (editor.summary) {
  653. return promise.resolve(editor.summary);
  654. }
  655. let deferred = defer();
  656. let self = this;
  657. this.on("editor-added", function onAdd(e, selected) {
  658. if (selected == editor) {
  659. self.off("editor-added", onAdd);
  660. deferred.resolve(editor.summary);
  661. }
  662. });
  663. return deferred.promise;
  664. },
  665. getEditorDetails: function (editor) {
  666. if (editor.details) {
  667. return promise.resolve(editor.details);
  668. }
  669. let deferred = defer();
  670. let self = this;
  671. this.on("editor-added", function onAdd(e, selected) {
  672. if (selected == editor) {
  673. self.off("editor-added", onAdd);
  674. deferred.resolve(editor.details);
  675. }
  676. });
  677. return deferred.promise;
  678. },
  679. /**
  680. * Returns an identifier for the given style sheet.
  681. *
  682. * @param {StyleSheet} styleSheet
  683. * The style sheet to be identified.
  684. */
  685. getStyleSheetIdentifier: function (styleSheet) {
  686. // Identify inline style sheets by their host page URI and index
  687. // at the page.
  688. return styleSheet.href ? styleSheet.href :
  689. "inline-" + styleSheet.styleSheetIndex + "-at-" + styleSheet.nodeHref;
  690. },
  691. /**
  692. * selects a stylesheet and optionally moves the cursor to a selected line
  693. *
  694. * @param {StyleSheetFront} [stylesheet]
  695. * Stylesheet to select or href of stylesheet to select
  696. * @param {Number} [line]
  697. * Line to which the caret should be moved (zero-indexed).
  698. * @param {Number} [col]
  699. * Column to which the caret should be moved (zero-indexed).
  700. * @return {Promise}
  701. * Promise that will resolve when the editor is selected and ready
  702. * to be used.
  703. */
  704. selectStyleSheet: function (stylesheet, line, col) {
  705. this._styleSheetToSelect = {
  706. stylesheet: stylesheet,
  707. line: line,
  708. col: col,
  709. };
  710. /* Switch to the editor for this sheet, if it exists yet.
  711. Otherwise each editor will be checked when it's created. */
  712. return this.switchToSelectedSheet();
  713. },
  714. /**
  715. * Handler for an editor's 'property-changed' event.
  716. * Update the summary in the UI.
  717. *
  718. * @param {StyleSheetEditor} editor
  719. * Editor for which a property has changed
  720. */
  721. _summaryChange: function (editor) {
  722. this._updateSummaryForEditor(editor);
  723. },
  724. /**
  725. * Update split view summary of given StyleEditor instance.
  726. *
  727. * @param {StyleSheetEditor} editor
  728. * @param {DOMElement} summary
  729. * Optional item's summary element to update. If none, item
  730. * corresponding to passed editor is used.
  731. */
  732. _updateSummaryForEditor: function (editor, summary) {
  733. summary = summary || editor.summary;
  734. if (!summary) {
  735. return;
  736. }
  737. let ruleCount = editor.styleSheet.ruleCount;
  738. if (editor.styleSheet.relatedStyleSheet) {
  739. ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount;
  740. }
  741. if (ruleCount === undefined) {
  742. ruleCount = "-";
  743. }
  744. let flags = [];
  745. if (editor.styleSheet.disabled) {
  746. flags.push("disabled");
  747. }
  748. if (editor.unsaved) {
  749. flags.push("unsaved");
  750. }
  751. if (editor.linkedCSSFileError) {
  752. flags.push("linked-file-error");
  753. }
  754. this._view.setItemClassName(summary, flags.join(" "));
  755. let label = summary.querySelector(".stylesheet-name > label");
  756. label.setAttribute("value", editor.friendlyName);
  757. if (editor.styleSheet.href) {
  758. label.setAttribute("tooltiptext", editor.styleSheet.href);
  759. }
  760. let linkedCSSSource = "";
  761. if (editor.linkedCSSFile) {
  762. linkedCSSSource = OS.Path.basename(editor.linkedCSSFile);
  763. } else if (editor.styleSheet.relatedEditorName) {
  764. linkedCSSSource = editor.styleSheet.relatedEditorName;
  765. }
  766. text(summary, ".stylesheet-linked-file", linkedCSSSource);
  767. text(summary, ".stylesheet-title", editor.styleSheet.title || "");
  768. text(summary, ".stylesheet-rule-count",
  769. PluralForm.get(ruleCount,
  770. getString("ruleCount.label")).replace("#1", ruleCount));
  771. },
  772. /**
  773. * Update the @media rules sidebar for an editor. Hide if there are no rules
  774. * Display a list of the @media rules in the editor's associated style sheet.
  775. * Emits a 'media-list-changed' event after updating the UI.
  776. *
  777. * @param {StyleSheetEditor} editor
  778. * Editor to update @media sidebar of
  779. */
  780. _updateMediaList: function (editor) {
  781. Task.spawn(function* () {
  782. let details = yield this.getEditorDetails(editor);
  783. let list = details.querySelector(".stylesheet-media-list");
  784. while (list.firstChild) {
  785. list.removeChild(list.firstChild);
  786. }
  787. let rules = editor.mediaRules;
  788. let showSidebar = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR);
  789. let sidebar = details.querySelector(".stylesheet-sidebar");
  790. let inSource = false;
  791. for (let rule of rules) {
  792. let {line, column, parentStyleSheet} = rule;
  793. let location = {
  794. line: line,
  795. column: column,
  796. source: editor.styleSheet.href,
  797. styleSheet: parentStyleSheet
  798. };
  799. if (editor.styleSheet.isOriginalSource) {
  800. location = yield editor.cssSheet.getOriginalLocation(line, column);
  801. }
  802. // this @media rule is from a different original source
  803. if (location.source != editor.styleSheet.href) {
  804. continue;
  805. }
  806. inSource = true;
  807. let div = this._panelDoc.createElement("div");
  808. div.className = "media-rule-label";
  809. div.addEventListener("click",
  810. this._jumpToLocation.bind(this, location));
  811. let cond = this._panelDoc.createElement("div");
  812. cond.className = "media-rule-condition";
  813. if (!rule.matches) {
  814. cond.classList.add("media-condition-unmatched");
  815. }
  816. if (this._target.tab.tagName == "tab") {
  817. this._setConditionContents(cond, rule.conditionText);
  818. } else {
  819. cond.textContent = rule.conditionText;
  820. }
  821. div.appendChild(cond);
  822. let link = this._panelDoc.createElement("div");
  823. link.className = "media-rule-line theme-link";
  824. if (location.line != -1) {
  825. link.textContent = ":" + location.line;
  826. }
  827. div.appendChild(link);
  828. list.appendChild(div);
  829. }
  830. sidebar.hidden = !showSidebar || !inSource;
  831. this.emit("media-list-changed", editor);
  832. }.bind(this)).then(null, e => console.error(e));
  833. },
  834. /**
  835. * Used to safely inject media query links
  836. *
  837. * @param {HTMLElement} element
  838. * The element corresponding to the media sidebar condition
  839. * @param {String} rawText
  840. * The raw condition text to parse
  841. */
  842. _setConditionContents(element, rawText) {
  843. const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/ig;
  844. let match = minMaxPattern.exec(rawText);
  845. let lastParsed = 0;
  846. while (match && match.index != minMaxPattern.lastIndex) {
  847. let matchEnd = match.index + match[0].length;
  848. let node = this._panelDoc.createTextNode(
  849. rawText.substring(lastParsed, match.index)
  850. );
  851. element.appendChild(node);
  852. let link = this._panelDoc.createElement("a");
  853. link.href = "#";
  854. link.className = "media-responsive-mode-toggle";
  855. link.textContent = rawText.substring(match.index, matchEnd);
  856. link.addEventListener("click", this._onMediaConditionClick.bind(this));
  857. element.appendChild(link);
  858. match = minMaxPattern.exec(rawText);
  859. lastParsed = matchEnd;
  860. }
  861. let node = this._panelDoc.createTextNode(
  862. rawText.substring(lastParsed, rawText.length)
  863. );
  864. element.appendChild(node);
  865. },
  866. /**
  867. * Called when a media condition is clicked
  868. * If a responsive mode link is clicked, it will launch it.
  869. *
  870. * @param {object} e
  871. * Event object
  872. */
  873. _onMediaConditionClick: function (e) {
  874. let conditionText = e.target.textContent;
  875. let isWidthCond = conditionText.toLowerCase().indexOf("width") > -1;
  876. let mediaVal = parseInt(/\d+/.exec(conditionText), 10);
  877. let options = isWidthCond ? {width: mediaVal} : {height: mediaVal};
  878. this._launchResponsiveMode(options);
  879. e.preventDefault();
  880. e.stopPropagation();
  881. },
  882. /**
  883. * Launches the responsive mode with a specific width or height
  884. *
  885. * @param {object} options
  886. * Object with width or/and height properties.
  887. */
  888. _launchResponsiveMode: Task.async(function* (options = {}) {
  889. let tab = this._target.tab;
  890. let win = this._target.tab.ownerDocument.defaultView;
  891. yield ResponsiveUIManager.openIfNeeded(win, tab);
  892. ResponsiveUIManager.getResponsiveUIForTab(tab).setViewportSize(options);
  893. }),
  894. /**
  895. * Jump cursor to the editor for a stylesheet and line number for a rule.
  896. *
  897. * @param {object} location
  898. * Location object with 'line', 'column', and 'source' properties.
  899. */
  900. _jumpToLocation: function (location) {
  901. let source = location.styleSheet || location.source;
  902. this.selectStyleSheet(source, location.line - 1, location.column - 1);
  903. },
  904. destroy: function () {
  905. if (this._highlighter) {
  906. this._highlighter.finalize();
  907. this._highlighter = null;
  908. }
  909. this._clearStyleSheetEditors();
  910. this._seenSheets = null;
  911. this._suppressAdd = false;
  912. let sidebar = this._panelDoc.querySelector(".splitview-controller");
  913. let sidebarWidth = sidebar.getAttribute("width");
  914. Services.prefs.setIntPref(PREF_NAV_WIDTH, sidebarWidth);
  915. this._optionsMenu.removeEventListener("popupshowing",
  916. this._onOptionsPopupShowing);
  917. this._optionsMenu.removeEventListener("popuphiding",
  918. this._onOptionsPopupHiding);
  919. this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument);
  920. this._prefObserver.off(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged);
  921. this._prefObserver.destroy();
  922. this._debuggee.off("stylesheet-added", this._addStyleSheet);
  923. }
  924. };