1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 } from "resource://devtools/shared/loader/Loader.sys.mjs";
10 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
17 } from "resource://devtools/client/styleeditor/StyleEditorUtil.sys.mjs";
18 import { StyleSheetEditor } from "resource://devtools/client/styleeditor/StyleSheetEditor.sys.mjs";
20 const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
22 const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
25 } = require("resource://devtools/shared/inspector/css-logic.js");
29 loader.lazyRequireGetter(
32 "resource://devtools/client/shared/keycodes.js",
36 loader.lazyRequireGetter(
39 "resource://devtools/client/styleeditor/original-source.js",
43 ChromeUtils.defineESModuleGetters(lazy, {
44 FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
45 NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
47 loader.lazyRequireGetter(
49 "ResponsiveUIManager",
50 "resource://devtools/client/responsive/manager.js"
52 loader.lazyRequireGetter(
55 "resource://devtools/client/shared/link.js",
58 loader.lazyRequireGetter(
61 "resource://devtools/shared/platform/clipboard.js",
65 const LOAD_ERROR = "error-load";
66 const PREF_AT_RULES_SIDEBAR = "devtools.styleeditor.showAtRulesSidebar";
67 const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.atRulesSidebarWidth";
68 const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
69 const PREF_ORIG_SOURCES = "devtools.source-map.client-service.enabled";
71 const FILTERED_CLASSNAME = "splitview-filtered";
72 const ALL_FILTERED_CLASSNAME = "splitview-all-filtered";
74 const HTML_NS = "http://www.w3.org/1999/xhtml";
77 * StyleEditorUI is controls and builds the UI of the Style Editor, including
78 * maintaining a list of editors for each stylesheet on a debuggee.
81 * 'editor-added': A new editor was added to the UI
82 * 'editor-selected': An editor was selected
83 * 'error': An error occured
86 export class StyleEditorUI extends EventEmitter {
87 #activeSummary = null;
90 #contextMenuStyleSheet;
95 #filterInputClearButton;
105 #seenSheets = new Map();
108 #sourceMapPrefObserver;
109 #styleSheetBoundToSelect;
112 * Maps keyed by summary element whose value is an object containing:
113 * - {Element} details: The associated details element (i.e. container for CodeMirror)
114 * - {StyleSheetEditor} editor: The associated editor, for easy retrieval
116 #summaryDataMap = new WeakMap();
120 #uiAbortController = new AbortController();
124 * @param {Toolbox} toolbox
125 * @param {Object} commands Object defined from devtools/shared/commands to interact with the devtools backend
126 * @param {Document} panelDoc
127 * Document of the toolbox panel to populate UI in.
128 * @param {CssProperties} A css properties database.
130 constructor(toolbox, commands, panelDoc, cssProperties) {
133 this.#toolbox = toolbox;
134 this.#commands = commands;
135 this.#panelDoc = panelDoc;
136 this.#cssProperties = cssProperties;
137 this.#window = this.#panelDoc.defaultView;
138 this.#root = this.#panelDoc.getElementById("style-editor-chrome");
141 this.selectedEditor = null;
142 this.savedLocations = {};
144 this.#prefObserver = new PrefObserver("devtools.styleeditor.");
145 this.#prefObserver.on(
146 PREF_AT_RULES_SIDEBAR,
147 this.#onAtRulesSidebarPrefChanged
149 this.#sourceMapPrefObserver = new PrefObserver(
150 "devtools.source-map.client-service."
152 this.#sourceMapPrefObserver.on(
154 this.#onOrigSourcesPrefChanged
158 get cssProperties() {
159 return this.#cssProperties;
162 get currentTarget() {
163 return this.#commands.targetCommand.targetFront;
167 * Index of selected stylesheet in document.styleSheets
169 get selectedStyleSheetIndex() {
170 return this.selectedEditor
171 ? this.selectedEditor.styleSheet.styleSheetIndex
176 * Initiates the style editor ui creation, and start to track TargetCommand updates.
178 * @params {Object} options
179 * @params {Object} options.stylesheetToSelect
180 * @params {StyleSheetResource} options.stylesheetToSelect.stylesheet
181 * @params {Integer} options.stylesheetToSelect.line
182 * @params {Integer} options.stylesheetToSelect.column
184 async initialize(options = {}) {
187 if (options.stylesheetToSelect) {
188 const { stylesheet, line, column } = options.stylesheetToSelect;
189 // If a stylesheet resource and its location was passed (e.g. user clicked on a stylesheet
190 // location in the rule view), we can directly add it to the list and select it
191 // before watching for resources, for improved performance.
192 if (stylesheet.resourceId) {
194 await this.#handleStyleSheetResource(stylesheet);
195 await this.selectStyleSheet(
198 column ? column - 1 : 0
206 await this.#toolbox.resourceCommand.watchResources(
207 [this.#toolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
208 { onAvailable: this.#onResourceAvailable }
210 await this.#commands.targetCommand.watchTargets({
211 types: [this.#commands.targetCommand.TYPES.FRAME],
212 onAvailable: this.#onTargetAvailable,
213 onDestroyed: this.#onTargetDestroyed,
216 this.#startLoadingStyleSheets();
217 await this.#toolbox.resourceCommand.watchResources(
218 [this.#toolbox.resourceCommand.TYPES.STYLESHEET],
220 onAvailable: this.#onResourceAvailable,
221 onUpdated: this.#onResourceUpdated,
222 onDestroyed: this.#onResourceDestroyed,
225 await this.#waitForLoadingStyleSheets();
229 * Build the initial UI and wire buttons with event handlers.
232 this.#filterInput = this.#root.querySelector(".devtools-filterinput");
233 this.#filterInputClearButton = this.#root.querySelector(
234 ".devtools-searchinput-clear"
236 this.#nav = this.#root.querySelector(".splitview-nav");
237 this.#side = this.#root.querySelector(".splitview-side-details");
238 this.#tplSummary = this.#root.querySelector(
239 "#splitview-tpl-summary-stylesheet"
241 this.#tplDetails = this.#root.querySelector(
242 "#splitview-tpl-details-stylesheet"
245 const eventListenersConfig = { signal: this.#uiAbortController.signal };
247 // Add click event on the "new stylesheet" button in the toolbar and on the
248 // "append a new stylesheet" link (visible when there are no stylesheets).
249 for (const el of this.#root.querySelectorAll(".style-editor-newButton")) {
253 const stylesheetsFront =
254 await this.currentTarget.getFront("stylesheets");
255 stylesheetsFront.addStyleSheet(null);
256 this.#clearFilterInput();
262 this.#root.querySelector(".style-editor-importButton").addEventListener(
265 this.#importFromFile(this._mockImportFile || null, this.#window);
266 this.#clearFilterInput();
271 this.#prettyPrintButton = this.#root.querySelector(
272 ".style-editor-prettyPrintButton"
274 this.#prettyPrintButton.addEventListener(
277 if (!this.selectedEditor) {
281 this.selectedEditor.prettifySourceText();
287 .querySelector("#style-editor-options")
290 this.#onOptionsButtonClick,
294 this.#filterInput.addEventListener(
296 this.#onFilterInputChange,
300 this.#filterInputClearButton.addEventListener(
302 () => this.#clearFilterInput(),
306 this.#panelDoc.addEventListener(
309 this.#contextMenuStyleSheet = null;
311 { ...eventListenersConfig, capture: true }
314 this.#optionsButton = this.#panelDoc.getElementById("style-editor-options");
316 this.#contextMenu = this.#panelDoc.getElementById("sidebar-context");
317 this.#contextMenu.addEventListener(
319 this.#updateContextMenuItems,
323 this.#openLinkNewTabItem = this.#panelDoc.getElementById(
324 "context-openlinknewtab"
326 this.#openLinkNewTabItem.addEventListener(
328 this.#openLinkNewTab,
332 this.#copyUrlItem = this.#panelDoc.getElementById("context-copyurl");
333 this.#copyUrlItem.addEventListener(
339 // items list focus and search-on-type handling
340 this.#nav.addEventListener(
346 this.#shortcuts = new KeyShortcuts({
347 window: this.#window,
350 `CmdOrCtrl+${getString("focusFilterInput.commandkey")}`,
351 this.#onFocusFilterInputKeyboardShortcut
354 const nav = this.#panelDoc.querySelector(".splitview-controller");
355 nav.style.width = Services.prefs.getIntPref(PREF_NAV_WIDTH) + "px";
358 #clearFilterInput() {
359 this.#filterInput.value = "";
360 this.#onFilterInputChange();
363 #onFilterInputChange = () => {
364 this.#filter = this.#filterInput.value;
365 this.#filterInputClearButton.toggleAttribute("hidden", !this.#filter);
367 for (const summary of this.#nav.childNodes) {
368 // Don't update nav class for every element, we do it after the loop.
369 this.handleSummaryVisibility(summary, {
370 triggerOnFilterStateChange: false,
374 this.#onFilterStateChange();
376 if (this.#activeSummary == null) {
377 const firstVisibleSummary = Array.from(this.#nav.childNodes).find(
378 node => !node.classList.contains(FILTERED_CLASSNAME)
381 if (firstVisibleSummary) {
382 this.setActiveSummary(firstVisibleSummary, { reason: "filter-auto" });
387 #onFilterStateChange() {
388 const summaries = Array.from(this.#nav.childNodes);
389 const hasVisibleSummary = summaries.some(
390 node => !node.classList.contains(FILTERED_CLASSNAME)
392 const allFiltered = !!summaries.length && !hasVisibleSummary;
394 this.#nav.classList.toggle(ALL_FILTERED_CLASSNAME, allFiltered);
397 .closest(".devtools-searchbox")
398 .classList.toggle("devtools-searchbox-no-match", !!allFiltered);
401 #onFocusFilterInputKeyboardShortcut = e => {
402 // Prevent the print modal to be displayed.
407 this.#filterInput.select();
410 #onNavKeyDown = event => {
411 function getFocusedItemWithin(nav) {
412 let node = nav.ownerDocument.activeElement;
413 while (node && node.parentNode != nav) {
414 node = node.parentNode;
419 // do not steal focus from inside iframes or textboxes
421 event.target.ownerDocument != this.#nav.ownerDocument ||
422 event.target.tagName == "input" ||
423 event.target.tagName == "textarea" ||
424 event.target.classList.contains("textbox")
429 // handle keyboard navigation within the items list
430 const visibleElements = Array.from(
431 this.#nav.querySelectorAll(`li:not(.${FILTERED_CLASSNAME})`)
433 // Elements have a different visual order (due to the use of order), so
434 // we need to sort them by their data-ordinal attribute
435 visibleElements.sort(
436 (a, b) => a.getAttribute("data-ordinal") - b.getAttribute("data-ordinal")
441 event.keyCode == lazy.KeyCodes.DOM_VK_PAGE_UP ||
442 event.keyCode == lazy.KeyCodes.DOM_VK_HOME
444 elementToFocus = visibleElements[0];
446 event.keyCode == lazy.KeyCodes.DOM_VK_PAGE_DOWN ||
447 event.keyCode == lazy.KeyCodes.DOM_VK_END
449 elementToFocus = visibleElements.at(-1);
450 } else if (event.keyCode == lazy.KeyCodes.DOM_VK_UP) {
451 const focusedIndex = visibleElements.indexOf(
452 getFocusedItemWithin(this.#nav)
454 elementToFocus = visibleElements[focusedIndex - 1];
455 } else if (event.keyCode == lazy.KeyCodes.DOM_VK_DOWN) {
456 const focusedIndex = visibleElements.indexOf(
457 getFocusedItemWithin(this.#nav)
459 elementToFocus = visibleElements[focusedIndex + 1];
462 if (elementToFocus !== undefined) {
463 event.stopPropagation();
464 event.preventDefault();
465 elementToFocus.focus();
473 * Opens the Options Popup Menu
475 * @params {number} screenX
476 * @params {number} screenY
477 * Both obtained from the event object, used to position the popup
479 #onOptionsButtonClick = ({ screenX, screenY }) => {
480 this.#optionsMenu = optionsPopupMenu(
481 this.#toggleOrigSources,
482 this.#toggleAtRulesSidebar
485 this.#optionsMenu.once("open", () => {
486 this.#optionsButton.setAttribute("open", true);
488 this.#optionsMenu.once("close", () => {
489 this.#optionsButton.removeAttribute("open");
492 this.#optionsMenu.popup(screenX, screenY, this.#toolbox.doc);
496 * Be called when changing the original sources pref.
498 #onOrigSourcesPrefChanged = async () => {
500 // When we toggle the source-map preference, we clear the panel and re-fetch the exact
501 // same stylesheet resources from ResourceCommand, but `_addStyleSheet` will trigger
502 // or ignore the additional source-map mapping.
503 this.#root.classList.add("loading");
504 for (const resource of this.#toolbox.resourceCommand.getAllResources(
505 this.#toolbox.resourceCommand.TYPES.STYLESHEET
507 await this.#handleStyleSheetResource(resource);
510 this.#root.classList.remove("loading");
512 this.emit("stylesheets-refreshed");
516 * Remove all editors and add loading indicator.
519 // remember selected sheet and line number for next load
520 if (this.selectedEditor && this.selectedEditor.sourceEditor) {
521 const href = this.selectedEditor.styleSheet.href;
522 const { line, ch } = this.selectedEditor.sourceEditor.getCursor();
524 this.#styleSheetToSelect = {
531 // remember saved file locations
532 for (const editor of this.editors) {
533 if (editor.savedFile) {
534 const identifier = this.getStyleSheetIdentifier(editor.styleSheet);
535 this.savedLocations[identifier] = editor.savedFile;
539 this.#clearStyleSheetEditors();
540 // Clear the left sidebar items and their associated elements.
541 while (this.#nav.hasChildNodes()) {
542 this.removeSplitViewItem(this.#nav.firstChild);
545 this.selectedEditor = null;
546 // Here the keys are style sheet actors, and the values are
547 // promises that resolve to the sheet's editor. See |_addStyleSheet|.
548 this.#seenSheets = new Map();
550 this.emit("stylesheets-clear");
554 * Add an editor for this stylesheet. Add editors for its original sources
555 * instead (e.g. Sass sources), if applicable.
557 * @param {Resource} resource
558 * The STYLESHEET resource which is received from resource command.
560 * A promise that resolves to the style sheet's editor when the style sheet has
561 * been fully loaded. If the style sheet has a source map, and source mapping
562 * is enabled, then the promise resolves to null.
564 #addStyleSheet(resource) {
565 if (!this.#seenSheets.has(resource)) {
566 const promise = (async () => {
567 // When the StyleSheet is mapped to one or many original sources,
568 // do not create an editor for the minified StyleSheet.
569 const hasValidOriginalSource =
570 await this.#tryAddingOriginalStyleSheets(resource);
571 if (hasValidOriginalSource) {
574 // Otherwise, if source-map failed or this is a non-source-map CSS
575 // create an editor for it.
576 return this.#addStyleSheetEditor(resource);
578 this.#seenSheets.set(resource, promise);
580 return this.#seenSheets.get(resource);
584 * Check if the given StyleSheet relates to an original StyleSheet (via source maps).
585 * If one is found, create an editor for the original one.
587 * @param {Resource} resource
588 * The STYLESHEET resource which is received from resource command.
590 * Return true, when we found a viable related original StyleSheet.
592 async #tryAddingOriginalStyleSheets(resource) {
593 // Avoid querying the SourceMap if this feature is disabled.
594 if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
598 const sourceMapLoader = this.#toolbox.sourceMapLoader;
608 sources = await sourceMapLoader.getOriginalURLs({
610 url: href || nodeHref,
615 // Ignore any source map error, they will be logged
616 // via the SourceMapLoader and Toolbox into the Web Console.
620 // Return the generated CSS if the source-map failed to be parsed
621 // or did not generate any original source.
622 if (!sources || !sources.length) {
626 // A single generated sheet might map to multiple original
627 // sheets, so make editors for each of them.
628 for (const { id: originalId, url: originalURL } of sources) {
629 const original = new lazy.OriginalSource(
635 // set so the first sheet will be selected, even if it's a source
636 original.styleSheetIndex = resource.styleSheetIndex;
637 original.relatedStyleSheet = resource;
638 original.resourceId = resource.resourceId;
639 original.targetFront = resource.targetFront;
640 original.atRules = resource.atRules;
641 await this.#addStyleSheetEditor(original);
647 #removeStyleSheet(resource, editor) {
648 this.#seenSheets.delete(resource);
649 this.#removeStyleSheetEditor(editor);
652 #getInlineStyleSheetsCount() {
653 return this.editors.filter(editor => !editor.styleSheet.href).length;
656 #getNewStyleSheetsCount() {
657 return this.editors.filter(editor => editor.isNew).length;
661 * Finds the index to be shown in the Style Editor for inline or
662 * user-created style sheets, returns undefined if not of either type.
664 * @param {StyleSheet} styleSheet
665 * Object representing stylesheet
666 * @return {(Number|undefined)}
667 * Optional Integer representing the index of the current stylesheet
668 * among all stylesheets of its type (inline or user-created)
670 #getNextFriendlyIndex(styleSheet) {
671 if (styleSheet.href) {
675 return styleSheet.isNew
676 ? this.#getNewStyleSheetsCount()
677 : this.#getInlineStyleSheetsCount();
681 * Add a new editor to the UI for a source.
683 * @param {Resource} resource
684 * The resource which is received from resource command.
685 * @return {Promise} that is resolved with the created StyleSheetEditor when
686 * the editor is fully initialized or rejected on error.
688 async #addStyleSheetEditor(resource) {
689 const editor = new StyleSheetEditor(
692 this.#getNextFriendlyIndex(resource)
695 editor.on("property-change", this.#summaryChange.bind(this, editor));
696 editor.on("at-rules-changed", this.#updateAtRulesList.bind(this, editor));
697 editor.on("linked-css-file", this.#summaryChange.bind(this, editor));
698 editor.on("linked-css-file-error", this.#summaryChange.bind(this, editor));
699 editor.on("error", this.#onError);
701 "filter-input-keyboard-shortcut",
702 this.#onFocusFilterInputKeyboardShortcut
705 // onAtRulesChanged fires at-rules-changed, so call the function after
706 // registering the listener in order to ensure to get at-rules-changed event.
707 editor.onAtRulesChanged(resource.atRules);
709 this.editors.push(editor);
712 await editor.fetchSource();
714 // if the editor was destroyed while fetching dependencies, we don't want to go further.
715 if (!this.editors.includes(editor)) {
721 this.#sourceLoaded(editor);
723 if (resource.fileName) {
724 this.emit("test:editor-updated", editor);
731 * Import a style sheet from file and asynchronously create a
732 * new stylesheet on the debuggee for it.
734 * @param {mixed} file
735 * Optional nsIFile or filename string.
736 * If not set a file picker will be shown.
737 * @param {nsIWindow} parentWindow
738 * Optional parent window for the file picker.
740 #importFromFile(file, parentWindow) {
741 const onFileSelected = selectedFile => {
746 lazy.NetUtil.asyncFetch(
748 uri: lazy.NetUtil.newURI(selectedFile),
749 loadingNode: this.#window.document,
751 Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
752 contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
754 async (stream, status) => {
755 if (!Components.isSuccessCode(status)) {
756 this.emit("error", { key: LOAD_ERROR, level: "warning" });
759 const source = lazy.NetUtil.readInputStreamToString(
765 const stylesheetsFront =
766 await this.currentTarget.getFront("stylesheets");
767 stylesheetsFront.addStyleSheet(source, selectedFile.path);
772 showFilePicker(file, false, parentWindow, onFileSelected);
776 * Forward any error from a stylesheet.
782 this.emit("error", data);
786 * Toggle the original sources pref.
788 #toggleOrigSources() {
789 const isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
790 Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
794 * Toggle the pref for showing the at-rules sidebar (for @media, @layer, @container, …)
797 #toggleAtRulesSidebar() {
798 const isEnabled = Services.prefs.getBoolPref(PREF_AT_RULES_SIDEBAR);
799 Services.prefs.setBoolPref(PREF_AT_RULES_SIDEBAR, !isEnabled);
803 * Toggle the at-rules sidebar in each editor depending on the setting.
805 #onAtRulesSidebarPrefChanged = () => {
806 this.editors.forEach(this.#updateAtRulesList);
810 * This method handles the following cases related to the context
811 * menu items "_openLinkNewTabItem" and "_copyUrlItem":
813 * 1) There was a stylesheet clicked on and it is external: show and
814 * enable the context menu item
815 * 2) There was a stylesheet clicked on and it is inline: show and
816 * disable the context menu item
817 * 3) There was no stylesheet clicked on (the right click happened
818 * below the list): hide the context menu
820 #updateContextMenuItems = async () => {
821 this.#openLinkNewTabItem.hidden = !this.#contextMenuStyleSheet;
822 this.#copyUrlItem.hidden = !this.#contextMenuStyleSheet;
824 if (this.#contextMenuStyleSheet) {
825 this.#openLinkNewTabItem.setAttribute(
827 !this.#contextMenuStyleSheet.href
829 this.#copyUrlItem.setAttribute(
831 !this.#contextMenuStyleSheet.href
837 * Open a particular stylesheet in a new tab.
839 #openLinkNewTab = () => {
840 if (this.#contextMenuStyleSheet) {
841 lazy.openContentLink(this.#contextMenuStyleSheet.href);
846 * Copies a stylesheet's URL.
849 if (this.#contextMenuStyleSheet) {
850 lazy.copyString(this.#contextMenuStyleSheet.href);
855 * Remove a particular stylesheet editor from the UI
857 * @param {StyleSheetEditor} editor
858 * The editor to remove.
860 #removeStyleSheetEditor(editor) {
861 if (editor.summary) {
862 this.removeSplitViewItem(editor.summary);
865 this.on("editor-added", function onAdd(added) {
866 if (editor == added) {
867 self.off("editor-added", onAdd);
868 self.removeSplitViewItem(editor.summary);
874 this.editors.splice(this.editors.indexOf(editor), 1);
878 * Clear all the editors from the UI.
880 #clearStyleSheetEditors() {
881 for (const editor of this.editors) {
888 * Called when a StyleSheetEditor's source has been fetched.
889 * Add new sidebar item and editor to the UI
891 * @param {StyleSheetEditor} editor
892 * Editor to create UI for.
894 #sourceLoaded(editor) {
895 // Create the detail and summary nodes from the templates node (declared in index.xhtml)
896 const details = this.#tplDetails.cloneNode(true);
898 const summary = this.#tplSummary.cloneNode(true);
901 let ordinal = editor.styleSheet.styleSheetIndex;
902 ordinal = ordinal == -1 ? Number.MAX_SAFE_INTEGER : ordinal;
903 summary.style.order = ordinal;
904 summary.setAttribute("data-ordinal", ordinal);
906 const isSystem = !!editor.styleSheet.system;
908 summary.classList.add("stylesheet-system");
911 this.#nav.appendChild(summary);
912 this.#side.appendChild(details);
914 this.#summaryDataMap.set(summary, {
919 const createdEditor = editor;
920 createdEditor.summary = summary;
921 createdEditor.details = details;
923 const eventListenersConfig = { signal: this.#uiAbortController.signal };
925 summary.addEventListener(
928 event.stopPropagation();
929 this.setActiveSummary(summary);
934 const stylesheetToggle = summary.querySelector(".stylesheet-toggle");
936 stylesheetToggle.disabled = true;
937 this.#window.document.l10n.setAttributes(
939 "styleeditor-visibility-toggle-system"
942 stylesheetToggle.addEventListener(
945 event.stopPropagation();
948 createdEditor.toggleDisabled();
954 summary.querySelector(".stylesheet-name").addEventListener(
957 if (event.keyCode == lazy.KeyCodes.DOM_VK_RETURN) {
958 this.setActiveSummary(summary);
964 summary.querySelector(".stylesheet-saveButton").addEventListener(
967 event.stopPropagation();
970 createdEditor.saveToFile(createdEditor.savedFile);
975 this.#updateSummaryForEditor(createdEditor, summary);
977 summary.addEventListener(
980 this.#contextMenuStyleSheet = createdEditor.styleSheet;
985 summary.addEventListener(
987 function onSummaryFocus(event) {
988 if (event.target == summary) {
989 // autofocus the stylesheet name
990 summary.querySelector(".stylesheet-name").focus();
996 const sidebar = details.querySelector(".stylesheet-sidebar");
997 sidebar.style.width = Services.prefs.getIntPref(PREF_SIDEBAR_WIDTH) + "px";
999 const splitter = details.querySelector(".devtools-side-splitter");
1000 splitter.addEventListener(
1003 const sidebarWidth = parseInt(sidebar.style.width, 10);
1004 if (!isNaN(sidebarWidth)) {
1005 Services.prefs.setIntPref(PREF_SIDEBAR_WIDTH, sidebarWidth);
1007 // update all at-rules sidebars for consistency
1009 ...this.#panelDoc.querySelectorAll(".stylesheet-sidebar"),
1011 for (const atRuleSidebar of sidebars) {
1012 atRuleSidebar.style.width = sidebarWidth + "px";
1016 eventListenersConfig
1019 // autofocus if it's a new user-created stylesheet
1020 if (createdEditor.isNew) {
1021 this.#selectEditor(createdEditor);
1024 if (this.#isEditorToSelect(createdEditor)) {
1025 this.switchToSelectedSheet();
1028 // If this is the first stylesheet and there is no pending request to
1029 // select a particular style sheet, select this sheet.
1031 !this.selectedEditor &&
1032 !this.#styleSheetBoundToSelect &&
1033 createdEditor.styleSheet.styleSheetIndex == 0 &&
1034 !summary.classList.contains(FILTERED_CLASSNAME)
1036 this.#selectEditor(createdEditor);
1038 this.emit("editor-added", createdEditor);
1042 * Switch to the editor that has been marked to be selected.
1045 * Promise that will resolve when the editor is selected.
1047 switchToSelectedSheet() {
1048 const toSelect = this.#styleSheetToSelect;
1050 for (const editor of this.editors) {
1051 if (this.#isEditorToSelect(editor)) {
1052 // The _styleSheetBoundToSelect will always hold the latest pending
1053 // requested style sheet (with line and column) which is not yet
1054 // selected by the source editor. Only after we select that particular
1055 // editor and go the required line and column, it will become null.
1056 this.#styleSheetBoundToSelect = this.#styleSheetToSelect;
1057 this.#styleSheetToSelect = null;
1058 return this.#selectEditor(editor, toSelect.line, toSelect.col);
1062 return Promise.resolve();
1066 * Returns whether a given editor is the current editor to be selected. Tests
1067 * based on href or underlying stylesheet.
1069 * @param {StyleSheetEditor} editor
1070 * The editor to test.
1072 #isEditorToSelect(editor) {
1073 const toSelect = this.#styleSheetToSelect;
1078 toSelect.stylesheet === null || typeof toSelect.stylesheet == "string";
1081 (isHref && editor.styleSheet.href == toSelect.stylesheet) ||
1082 toSelect.stylesheet == editor.styleSheet
1087 * Select an editor in the UI.
1089 * @param {StyleSheetEditor} editor
1090 * Editor to switch to.
1091 * @param {number} line
1092 * Line number to jump to
1093 * @param {number} col
1094 * Column number to jump to
1096 * Promise that will resolve when the editor is selected and ready
1099 #selectEditor(editor, line = null, col = null) {
1100 // Don't go further if the editor was destroyed in the meantime
1101 if (!this.editors.includes(editor)) {
1105 const editorPromise = editor.getSourceEditor().then(() => {
1106 // line/col are null when the style editor is initialized and the first stylesheet
1107 // editor is selected. Unfortunately, this function might be called also when the
1108 // panel is opened from clicking on a CSS warning in the WebConsole panel, in which
1109 // case we have specific line+col.
1110 // There's no guarantee which one could be called first, and it happened that we
1111 // were setting the cursor once for the correct line coming from the webconsole,
1112 // and then re-setting it to the default value (which was <0,0>).
1113 // To avoid the race, we simply don't explicitly set the cursor to any default value,
1114 // which is not a big deal as CodeMirror does init it to <0,0> anyway.
1115 // See Bug 1738124 for more information.
1116 if (line !== null || col !== null) {
1117 editor.setCursor(line, col);
1119 this.#styleSheetBoundToSelect = null;
1122 const summaryPromise = this.getEditorSummary(editor).then(summary => {
1123 // Don't go further if the editor was destroyed in the meantime
1124 if (!this.editors.includes(editor)) {
1125 throw new Error("Editor was destroyed");
1127 this.setActiveSummary(summary);
1130 return Promise.all([editorPromise, summaryPromise]);
1133 getEditorSummary(editor) {
1136 if (editor.summary) {
1137 return Promise.resolve(editor.summary);
1140 return new Promise(resolve => {
1141 this.on("editor-added", function onAdd(selected) {
1142 if (selected == editor) {
1143 self.off("editor-added", onAdd);
1144 resolve(editor.summary);
1150 getEditorDetails(editor) {
1153 if (editor.details) {
1154 return Promise.resolve(editor.details);
1157 return new Promise(resolve => {
1158 this.on("editor-added", function onAdd(selected) {
1159 if (selected == editor) {
1160 self.off("editor-added", onAdd);
1161 resolve(editor.details);
1168 * Returns an identifier for the given style sheet.
1170 * @param {StyleSheet} styleSheet
1171 * The style sheet to be identified.
1173 getStyleSheetIdentifier(styleSheet) {
1174 // Identify inline style sheets by their host page URI and index
1176 return styleSheet.href
1178 : "inline-" + styleSheet.styleSheetIndex + "-at-" + styleSheet.nodeHref;
1182 * Get the OriginalSource object for a given original sourceId returned from
1183 * the sourcemap worker service.
1185 * @param {string} sourceId
1186 * The ID to search for from the sourcemap worker.
1188 * @return {OriginalSource | null}
1190 getOriginalSourceSheet(sourceId) {
1191 for (const editor of this.editors) {
1192 const { styleSheet } = editor;
1193 if (styleSheet.isOriginalSource && styleSheet.sourceId === sourceId) {
1201 * Given an URL, find a stylesheet resource with that URL, if one has been
1202 * loaded into the editor.js
1204 * Do not use this unless you have no other way to get a StyleSheet resource
1205 * multiple sheets could share the same URL, so this will give you _one_
1206 * of possibly many sheets with that URL.
1208 * @param {string} url
1209 * An arbitrary URL to search for.
1211 * @return {StyleSheetResource|null}
1213 getStylesheetResourceForGeneratedURL(url) {
1214 for (const styleSheet of this.#seenSheets.keys()) {
1215 const sheetURL = styleSheet.href || styleSheet.nodeHref;
1216 if (!styleSheet.isOriginalSource && sheetURL === url) {
1224 * selects a stylesheet and optionally moves the cursor to a selected line
1226 * @param {StyleSheetResource} stylesheet
1227 * Stylesheet to select or href of stylesheet to select
1228 * @param {Number} line
1229 * Line to which the caret should be moved (zero-indexed).
1230 * @param {Number} col
1231 * Column to which the caret should be moved (zero-indexed).
1233 * Promise that will resolve when the editor is selected and ready
1236 selectStyleSheet(stylesheet, line, col) {
1237 this.#styleSheetToSelect = {
1243 /* Switch to the editor for this sheet, if it exists yet.
1244 Otherwise each editor will be checked when it's created. */
1245 return this.switchToSelectedSheet();
1249 * Handler for an editor's 'property-changed' event.
1250 * Update the summary in the UI.
1252 * @param {StyleSheetEditor} editor
1253 * Editor for which a property has changed
1255 #summaryChange(editor) {
1256 this.#updateSummaryForEditor(editor);
1260 * Update split view summary of given StyleEditor instance.
1262 * @param {StyleSheetEditor} editor
1263 * @param {DOMElement} summary
1264 * Optional item's summary element to update. If none, item
1265 * corresponding to passed editor is used.
1267 #updateSummaryForEditor(editor, summary) {
1268 summary = summary || editor.summary;
1273 let ruleCount = editor.styleSheet.ruleCount;
1274 if (editor.styleSheet.relatedStyleSheet) {
1275 ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount;
1277 if (ruleCount === undefined) {
1281 this.#panelDoc.l10n.setArgs(
1282 summary.querySelector(".stylesheet-rule-count"),
1288 summary.classList.toggle("disabled", !!editor.styleSheet.disabled);
1289 summary.classList.toggle("unsaved", !!editor.unsaved);
1290 summary.classList.toggle("linked-file-error", !!editor.linkedCSSFileError);
1292 const label = summary.querySelector(".stylesheet-name > label");
1293 label.setAttribute("value", editor.friendlyName);
1294 if (editor.styleSheet.href) {
1295 label.setAttribute("tooltiptext", editor.styleSheet.href);
1298 let linkedCSSSource = "";
1299 if (editor.linkedCSSFile) {
1300 linkedCSSSource = PathUtils.filename(editor.linkedCSSFile);
1301 } else if (editor.styleSheet.relatedStyleSheet) {
1302 // Compute a friendly name for the related generated source
1303 // (relatedStyleSheet is set on original CSS to refer to the generated one)
1304 linkedCSSSource = shortSource(editor.styleSheet.relatedStyleSheet);
1306 linkedCSSSource = decodeURI(linkedCSSSource);
1309 text(summary, ".stylesheet-linked-file", linkedCSSSource);
1310 text(summary, ".stylesheet-title", editor.styleSheet.title || "");
1312 // We may need to change the summary visibility as a result of the changes.
1313 this.handleSummaryVisibility(summary);
1317 * Update the pretty print button.
1318 * The button will be disabled if the selected file is an original file.
1320 #updatePrettyPrintButton() {
1322 !this.selectedEditor || !!this.selectedEditor.styleSheet.isOriginalSource;
1324 // Only update the button if its state needs it
1325 if (disable !== this.#prettyPrintButton.hasAttribute("disabled")) {
1326 this.#prettyPrintButton.toggleAttribute("disabled");
1327 const l10nString = disable
1328 ? "styleeditor-pretty-print-button-disabled"
1329 : "styleeditor-pretty-print-button";
1330 this.#window.document.l10n.setAttributes(
1331 this.#prettyPrintButton,
1338 * Update the at-rules sidebar for an editor. Hide if there are no rules
1339 * Display a list of the at-rules (@media, @layer, @container, …) in the editor's associated style sheet.
1340 * Emits a 'at-rules-list-changed' event after updating the UI.
1342 * @param {StyleSheetEditor} editor
1343 * Editor to update sidebar of
1345 #updateAtRulesList = editor => {
1346 (async function () {
1347 const details = await this.getEditorDetails(editor);
1348 const list = details.querySelector(".stylesheet-at-rules-list");
1350 while (list.firstChild) {
1351 list.firstChild.remove();
1354 const rules = editor.atRules;
1355 const showSidebar = Services.prefs.getBoolPref(PREF_AT_RULES_SIDEBAR);
1356 const sidebar = details.querySelector(".stylesheet-sidebar");
1358 let inSource = false;
1360 for (const rule of rules) {
1361 const { line, column } = rule;
1366 source: editor.styleSheet.href,
1367 styleSheet: editor.styleSheet,
1369 if (editor.styleSheet.isOriginalSource) {
1370 const styleSheet = editor.cssSheet;
1371 location = await editor.styleSheet.getOriginalLocation(
1378 // this at-rule is from a different original source
1379 if (location.source != editor.styleSheet.href) {
1384 const div = this.#panelDoc.createElementNS(HTML_NS, "div");
1385 div.classList.add("at-rule-label", rule.type);
1386 div.addEventListener(
1388 this.#jumpToLocation.bind(this, location)
1391 const ruleTextContainer = this.#panelDoc.createElementNS(
1395 const type = this.#panelDoc.createElementNS(HTML_NS, "span");
1396 type.className = "at-rule-type";
1397 type.append(this.#panelDoc.createTextNode(`@${rule.type}\u00A0`));
1398 if (rule.type == "layer" && rule.layerName) {
1399 type.append(this.#panelDoc.createTextNode(`${rule.layerName}\u00A0`));
1400 } else if (rule.type === "property") {
1402 this.#panelDoc.createTextNode(`${rule.propertyName}\u00A0`)
1406 const cond = this.#panelDoc.createElementNS(HTML_NS, "span");
1407 cond.className = "at-rule-condition";
1408 if (rule.type == "media" && !rule.matches) {
1409 cond.classList.add("media-condition-unmatched");
1411 if (this.#commands.descriptorFront.isLocalTab) {
1412 this.#setConditionContents(cond, rule.conditionText, rule.type);
1414 cond.textContent = rule.conditionText;
1417 const link = this.#panelDoc.createElementNS(HTML_NS, "div");
1418 link.className = "at-rule-line theme-link";
1419 if (location.line != -1) {
1420 link.textContent = ":" + location.line;
1423 ruleTextContainer.append(type, cond);
1424 div.append(ruleTextContainer, link);
1425 list.appendChild(div);
1428 sidebar.hidden = !showSidebar || !inSource;
1430 this.emit("at-rules-list-changed", editor);
1433 .catch(console.error);
1437 * Set the condition text for the at-rule element.
1438 * For media queries, it also injects links to open RDM at a specific size.
1440 * @param {HTMLElement} element
1441 * The element corresponding to the media sidebar condition
1442 * @param {String} ruleConditionText
1443 * The rule conditionText
1444 * @param {String} type
1445 * The type of the at-rule (e.g. "media", "layer", "supports", …)
1447 #setConditionContents(element, ruleConditionText, type) {
1448 if (!ruleConditionText) {
1452 // For non-media rules, we don't do anything more than displaying the conditionText
1453 // as there are no other condition text that would justify opening RDM at a specific
1454 // size (e.g. `@container` condition is relative to a container size, which varies
1455 // depending the node the rule applies to).
1456 if (type !== "media") {
1457 const node = this.#panelDoc.createTextNode(ruleConditionText);
1458 element.appendChild(node);
1462 const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/gi;
1464 let match = minMaxPattern.exec(ruleConditionText);
1466 while (match && match.index != minMaxPattern.lastIndex) {
1467 const matchEnd = match.index + match[0].length;
1468 const node = this.#panelDoc.createTextNode(
1469 ruleConditionText.substring(lastParsed, match.index)
1471 element.appendChild(node);
1473 const link = this.#panelDoc.createElementNS(HTML_NS, "a");
1475 link.className = "media-responsive-mode-toggle";
1476 link.textContent = ruleConditionText.substring(match.index, matchEnd);
1477 link.addEventListener("click", this.#onMediaConditionClick.bind(this));
1478 element.appendChild(link);
1480 match = minMaxPattern.exec(ruleConditionText);
1481 lastParsed = matchEnd;
1484 const node = this.#panelDoc.createTextNode(
1485 ruleConditionText.substring(lastParsed, ruleConditionText.length)
1487 element.appendChild(node);
1491 * Called when a media condition is clicked
1492 * If a responsive mode link is clicked, it will launch it.
1497 #onMediaConditionClick(e) {
1498 const conditionText = e.target.textContent;
1499 const isWidthCond = conditionText.toLowerCase().indexOf("width") > -1;
1500 const mediaVal = parseInt(/\d+/.exec(conditionText), 10);
1502 const options = isWidthCond ? { width: mediaVal } : { height: mediaVal };
1503 this.#launchResponsiveMode(options);
1505 e.stopPropagation();
1509 * Launches the responsive mode with a specific width or height.
1511 * @param {object} options
1512 * Object with width or/and height properties.
1514 async #launchResponsiveMode(options = {}) {
1515 const tab = this.#commands.descriptorFront.localTab;
1516 const win = tab.ownerDocument.defaultView;
1518 await lazy.ResponsiveUIManager.openIfNeeded(win, tab, {
1519 trigger: "style_editor",
1521 this.emit("responsive-mode-opened");
1523 lazy.ResponsiveUIManager.getResponsiveUIForTab(tab).setViewportSize(
1529 * Jump cursor to the editor for a stylesheet and line number for a rule.
1531 * @param {object} location
1532 * Location object with 'line', 'column', and 'source' properties.
1534 #jumpToLocation(location) {
1535 const source = location.styleSheet || location.source;
1536 this.selectStyleSheet(source, location.line - 1, location.column - 1);
1539 #startLoadingStyleSheets() {
1540 this.#root.classList.add("loading");
1541 this.#loadingStyleSheets = [];
1544 async #waitForLoadingStyleSheets() {
1545 while (this.#loadingStyleSheets?.length > 0) {
1546 const pending = this.#loadingStyleSheets;
1547 this.#loadingStyleSheets = [];
1548 await Promise.all(pending);
1551 this.#loadingStyleSheets = null;
1552 this.#root.classList.remove("loading");
1553 this.emit("reloaded");
1556 async #handleStyleSheetResource(resource) {
1558 // The fileName is in resource means this stylesheet was imported from file by user.
1559 const { fileName } = resource;
1560 let file = fileName ? new lazy.FileUtils.File(fileName) : null;
1562 // recall location of saved file for this sheet after page reload
1564 const identifier = this.getStyleSheetIdentifier(resource);
1565 const savedFile = this.savedLocations[identifier];
1570 resource.file = file;
1572 await this.#addStyleSheet(resource);
1575 this.emit("error", { key: LOAD_ERROR, level: "warning" });
1579 // onAvailable is a mandatory argument for watchTargets,
1580 // but we don't do anything when a new target gets created.
1581 #onTargetAvailable = () => {};
1583 #onTargetDestroyed = ({ targetFront }) => {
1584 // Iterate over a copy of the list in order to prevent skipping
1585 // over some items when removing items of this list
1586 const editorsCopy = [...this.editors];
1587 for (const editor of editorsCopy) {
1588 const { styleSheet } = editor;
1589 if (styleSheet.targetFront == targetFront) {
1590 this.#removeStyleSheet(styleSheet, editor);
1595 #onResourceAvailable = async resources => {
1596 const promises = [];
1597 for (const resource of resources) {
1599 resource.resourceType === this.#toolbox.resourceCommand.TYPES.STYLESHEET
1601 const onStyleSheetHandled = this.#handleStyleSheetResource(resource);
1603 if (this.#loadingStyleSheets) {
1604 // In case of reloading/navigating and panel's opening
1605 this.#loadingStyleSheets.push(onStyleSheetHandled);
1607 promises.push(onStyleSheetHandled);
1611 if (!resource.targetFront.isTopLevel) {
1615 if (resource.name === "will-navigate") {
1616 this.#startLoadingStyleSheets();
1618 } else if (resource.name === "dom-complete") {
1619 promises.push(this.#waitForLoadingStyleSheets());
1622 await Promise.all(promises);
1625 #onResourceUpdated = async updates => {
1626 // The editors are instantiated asynchronously from onResourceAvailable,
1627 // but we may receive updates right after due to throttling.
1628 // Ensure waiting for this async work before trying to update the related editors.
1629 await this.#waitForLoadingStyleSheets();
1631 for (const { resource, update } of updates) {
1633 update.resourceType === this.#toolbox.resourceCommand.TYPES.STYLESHEET
1635 const editor = this.editors.find(
1636 e => e.resourceId === update.resourceId
1641 "Could not find StyleEditor to apply STYLESHEET resource update"
1646 switch (update.updateType) {
1647 case "style-applied": {
1648 editor.onStyleApplied(update);
1651 case "property-change": {
1652 for (const [property, value] of Object.entries(
1653 update.resourceUpdates
1655 editor.onPropertyChange(property, value);
1659 case "at-rules-changed":
1660 case "matches-change": {
1661 editor.onAtRulesChanged(resource.atRules);
1669 #onResourceDestroyed = resources => {
1670 for (const resource of resources) {
1672 resource.resourceType !== this.#toolbox.resourceCommand.TYPES.STYLESHEET
1677 const editorToRemove = this.editors.find(
1678 editor => editor.styleSheet.resourceId == resource.resourceId
1681 if (editorToRemove) {
1682 const { styleSheet } = editorToRemove;
1683 this.#removeStyleSheet(styleSheet, editorToRemove);
1689 * Set the active item's summary element.
1691 * @param DOMElement summary
1692 * @param {Object} options
1693 * @param {String=} options.reason: Indicates why the summary was selected. It's set to
1694 * "filter-auto" when the summary was automatically selected as the result
1695 * of the previous active summary being filtered out.
1697 setActiveSummary(summary, options = {}) {
1698 if (summary == this.#activeSummary) {
1702 if (this.#activeSummary) {
1703 const binding = this.#summaryDataMap.get(this.#activeSummary);
1705 this.#activeSummary.classList.remove("splitview-active");
1706 binding.details.classList.remove("splitview-active");
1709 this.#activeSummary = summary;
1711 this.selectedEditor = null;
1715 const { details } = this.#summaryDataMap.get(summary);
1716 summary.classList.add("splitview-active");
1717 details.classList.add("splitview-active");
1719 this.showSummaryEditor(summary, options);
1723 * Show summary's associated editor
1725 * @param DOMElement summary
1726 * @param {Object} options
1727 * @param {String=} options.reason: Indicates why the summary was selected. It's set to
1728 * "filter-auto" when the summary was automatically selected as the result
1729 * of the previous active summary being filtered out.
1731 async showSummaryEditor(summary, options) {
1732 const { details, editor } = this.#summaryDataMap.get(summary);
1733 this.selectedEditor = editor;
1736 if (!editor.sourceEditor) {
1737 // only initialize source editor when we switch to this view
1738 const inputElement = details.querySelector(".stylesheet-editor-input");
1739 await editor.load(inputElement, this.#cssProperties);
1742 editor.onShow(options);
1744 this.#updatePrettyPrintButton();
1746 this.emit("editor-selected", editor);
1753 * Remove an item from the split view.
1755 * @param DOMElement summary
1756 * Summary element of the item to remove.
1758 removeSplitViewItem(summary) {
1759 if (summary == this.#activeSummary) {
1760 this.setActiveSummary(null);
1763 const data = this.#summaryDataMap.get(summary);
1769 data.details.remove();
1773 * Make the passed element visible or not, depending if it matches the current filter
1775 * @param {Element} summary
1776 * @param {Object} options
1777 * @param {Boolean} options.triggerOnFilterStateChange: Set to false to avoid calling
1778 * #onFilterStateChange directly here. This can be useful when this
1779 * function is called for every item of the list, like in `setFilter`.
1781 handleSummaryVisibility(summary, { triggerOnFilterStateChange = true } = {}) {
1782 if (!this.#filter) {
1783 summary.classList.remove(FILTERED_CLASSNAME);
1787 const label = summary.querySelector(".stylesheet-name label");
1788 const itemText = label.value.toLowerCase();
1789 const matchesSearch = itemText.includes(this.#filter.toLowerCase());
1790 summary.classList.toggle(FILTERED_CLASSNAME, !matchesSearch);
1792 if (this.#activeSummary == summary && !matchesSearch) {
1793 this.setActiveSummary(null);
1796 if (triggerOnFilterStateChange) {
1797 this.#onFilterStateChange();
1802 this.#toolbox.resourceCommand.unwatchResources(
1804 this.#toolbox.resourceCommand.TYPES.DOCUMENT_EVENT,
1805 this.#toolbox.resourceCommand.TYPES.STYLESHEET,
1808 onAvailable: this.#onResourceAvailable,
1809 onUpdated: this.#onResourceUpdated,
1810 onDestroyed: this.#onResourceDestroyed,
1813 this.#commands.targetCommand.unwatchTargets({
1814 types: [this.#commands.targetCommand.TYPES.FRAME],
1815 onAvailable: this.#onTargetAvailable,
1816 onDestroyed: this.#onTargetDestroyed,
1819 if (this.#uiAbortController) {
1820 this.#uiAbortController.abort();
1821 this.#uiAbortController = null;
1823 this.#clearStyleSheetEditors();
1825 this.#seenSheets = null;
1826 this.#filterInput = null;
1827 this.#filterInputClearButton = null;
1829 this.#prettyPrintButton = null;
1831 this.#tplDetails = null;
1832 this.#tplSummary = null;
1834 const sidebar = this.#panelDoc.querySelector(".splitview-controller");
1835 const sidebarWidth = parseInt(sidebar.style.width, 10);
1836 if (!isNaN(sidebarWidth)) {
1837 Services.prefs.setIntPref(PREF_NAV_WIDTH, sidebarWidth);
1840 if (this.#sourceMapPrefObserver) {
1841 this.#sourceMapPrefObserver.off(
1843 this.#onOrigSourcesPrefChanged
1845 this.#sourceMapPrefObserver.destroy();
1846 this.#sourceMapPrefObserver = null;
1849 if (this.#prefObserver) {
1850 this.#prefObserver.off(
1851 PREF_AT_RULES_SIDEBAR,
1852 this.#onAtRulesSidebarPrefChanged
1854 this.#prefObserver.destroy();
1855 this.#prefObserver = null;
1858 if (this.#shortcuts) {
1859 this.#shortcuts.destroy();
1860 this.#shortcuts = null;