12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073 |
- /* vim:set ts=2 sw=2 sts=2 et: */
- /* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
- "use strict";
- this.EXPORTED_SYMBOLS = ["StyleEditorUI"];
- const Ci = Components.interfaces;
- const Cu = Components.utils;
- const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
- const Services = require("Services");
- const {NetUtil} = require("resource://gre/modules/NetUtil.jsm");
- const {OS} = require("resource://gre/modules/osfile.jsm");
- const {Task} = require("devtools/shared/task");
- const EventEmitter = require("devtools/shared/event-emitter");
- const {gDevTools} = require("devtools/client/framework/devtools");
- const {
- getString,
- text,
- wire,
- showFilePicker,
- } = require("resource://devtools/client/styleeditor/StyleEditorUtil.jsm");
- const {SplitView} = require("resource://devtools/client/shared/SplitView.jsm");
- const {StyleSheetEditor} = require("resource://devtools/client/styleeditor/StyleSheetEditor.jsm");
- const {PluralForm} = require("devtools/shared/plural-form");
- const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/client/styleeditor/utils");
- const csscoverage = require("devtools/shared/fronts/csscoverage");
- const {console} = require("resource://gre/modules/Console.jsm");
- const promise = require("promise");
- const defer = require("devtools/shared/defer");
- const {ResponsiveUIManager} =
- require("resource://devtools/client/responsivedesign/responsivedesign.jsm");
- const {KeyCodes} = require("devtools/client/shared/keycodes");
- const LOAD_ERROR = "error-load";
- const STYLE_EDITOR_TEMPLATE = "stylesheet";
- const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
- const PREF_MEDIA_SIDEBAR = "devtools.styleeditor.showMediaSidebar";
- const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.mediaSidebarWidth";
- const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
- /**
- * StyleEditorUI is controls and builds the UI of the Style Editor, including
- * maintaining a list of editors for each stylesheet on a debuggee.
- *
- * Emits events:
- * 'editor-added': A new editor was added to the UI
- * 'editor-selected': An editor was selected
- * 'error': An error occured
- *
- * @param {StyleEditorFront} debuggee
- * Client-side front for interacting with the page's stylesheets
- * @param {Target} target
- * Interface for the page we're debugging
- * @param {Document} panelDoc
- * Document of the toolbox panel to populate UI in.
- * @param {CssProperties} A css properties database.
- */
- function StyleEditorUI(debuggee, target, panelDoc, cssProperties) {
- EventEmitter.decorate(this);
- this._debuggee = debuggee;
- this._target = target;
- this._panelDoc = panelDoc;
- this._cssProperties = cssProperties;
- this._window = this._panelDoc.defaultView;
- this._root = this._panelDoc.getElementById("style-editor-chrome");
- this.editors = [];
- this.selectedEditor = null;
- this.savedLocations = {};
- this._seenSheets = new Map();
- // Don't add any style sheets that might arrive via events, until
- // the call to initialize. Style sheets can arrive from the server
- // at any time, for example if a new style sheet was added, or if
- // the style sheet actor was just created and is walking the style
- // sheets for the first time. In any case, in |initialize| we're
- // going to fetch the list of sheets anyway.
- this._suppressAdd = true;
- this._onOptionsPopupShowing = this._onOptionsPopupShowing.bind(this);
- this._onOptionsPopupHiding = this._onOptionsPopupHiding.bind(this);
- this._onNewDocument = this._onNewDocument.bind(this);
- this._onMediaPrefChanged = this._onMediaPrefChanged.bind(this);
- this._updateMediaList = this._updateMediaList.bind(this);
- this._clear = this._clear.bind(this);
- this._onError = this._onError.bind(this);
- this._updateOpenLinkItem = this._updateOpenLinkItem.bind(this);
- this._openLinkNewTab = this._openLinkNewTab.bind(this);
- this._addStyleSheet = this._addStyleSheet.bind(this);
- this._prefObserver = new PrefObserver("devtools.styleeditor.");
- this._prefObserver.on(PREF_ORIG_SOURCES, this._onNewDocument);
- this._prefObserver.on(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged);
- this._debuggee.on("stylesheet-added", this._addStyleSheet);
- }
- this.StyleEditorUI = StyleEditorUI;
- StyleEditorUI.prototype = {
- /**
- * Get whether any of the editors have unsaved changes.
- *
- * @return boolean
- */
- get isDirty() {
- if (this._markedDirty === true) {
- return true;
- }
- return this.editors.some((editor) => {
- return editor.sourceEditor && !editor.sourceEditor.isClean();
- });
- },
- /*
- * Mark the style editor as having or not having unsaved changes.
- */
- set isDirty(value) {
- this._markedDirty = value;
- },
- /*
- * Index of selected stylesheet in document.styleSheets
- */
- get selectedStyleSheetIndex() {
- return this.selectedEditor ?
- this.selectedEditor.styleSheet.styleSheetIndex : -1;
- },
- /**
- * Initiates the style editor ui creation, the inspector front to get
- * reference to the walker and the selector highlighter if available
- */
- initialize: Task.async(function* () {
- yield this.initializeHighlighter();
- this.createUI();
- let styleSheets = yield this._debuggee.getStyleSheets();
- yield this._resetStyleSheetList(styleSheets);
- this._target.on("will-navigate", this._clear);
- this._target.on("navigate", this._onNewDocument);
- }),
- initializeHighlighter: Task.async(function* () {
- let toolbox = gDevTools.getToolbox(this._target);
- yield toolbox.initInspector();
- this._walker = toolbox.walker;
- let hUtils = toolbox.highlighterUtils;
- if (hUtils.supportsCustomHighlighters()) {
- try {
- this._highlighter =
- yield hUtils.getHighlighterByType(SELECTOR_HIGHLIGHTER_TYPE);
- } catch (e) {
- // The selectorHighlighter can't always be instantiated, for example
- // it doesn't work with XUL windows (until bug 1094959 gets fixed);
- // or the selectorHighlighter doesn't exist on the backend.
- console.warn("The selectorHighlighter couldn't be instantiated, " +
- "elements matching hovered selectors will not be highlighted");
- }
- }
- }),
- /**
- * Build the initial UI and wire buttons with event handlers.
- */
- createUI: function () {
- let viewRoot = this._root.parentNode.querySelector(".splitview-root");
- this._view = new SplitView(viewRoot);
- wire(this._view.rootElement, ".style-editor-newButton", () =>{
- this._debuggee.addStyleSheet(null);
- });
- wire(this._view.rootElement, ".style-editor-importButton", ()=> {
- this._importFromFile(this._mockImportFile || null, this._window);
- });
- this._optionsButton = this._panelDoc.getElementById("style-editor-options");
- this._panelDoc.addEventListener("contextmenu", () => {
- this._contextMenuStyleSheet = null;
- }, true);
- this._contextMenu = this._panelDoc.getElementById("sidebar-context");
- this._contextMenu.addEventListener("popupshowing",
- this._updateOpenLinkItem);
- this._optionsMenu =
- this._panelDoc.getElementById("style-editor-options-popup");
- this._optionsMenu.addEventListener("popupshowing",
- this._onOptionsPopupShowing);
- this._optionsMenu.addEventListener("popuphiding",
- this._onOptionsPopupHiding);
- this._sourcesItem = this._panelDoc.getElementById("options-origsources");
- this._sourcesItem.addEventListener("command",
- this._toggleOrigSources);
- this._mediaItem = this._panelDoc.getElementById("options-show-media");
- this._mediaItem.addEventListener("command",
- this._toggleMediaSidebar);
- this._openLinkNewTabItem =
- this._panelDoc.getElementById("context-openlinknewtab");
- this._openLinkNewTabItem.addEventListener("command",
- this._openLinkNewTab);
- let nav = this._panelDoc.querySelector(".splitview-controller");
- nav.setAttribute("width", Services.prefs.getIntPref(PREF_NAV_WIDTH));
- },
- /**
- * Listener handling the 'gear menu' popup showing event.
- * Update options menu items to reflect current preference settings.
- */
- _onOptionsPopupShowing: function () {
- this._optionsButton.setAttribute("open", "true");
- this._sourcesItem.setAttribute("checked",
- Services.prefs.getBoolPref(PREF_ORIG_SOURCES));
- this._mediaItem.setAttribute("checked",
- Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR));
- },
- /**
- * Listener handling the 'gear menu' popup hiding event.
- */
- _onOptionsPopupHiding: function () {
- this._optionsButton.removeAttribute("open");
- },
- /**
- * Refresh editors to reflect the stylesheets in the document.
- *
- * @param {string} event
- * Event name
- * @param {StyleSheet} styleSheet
- * StyleSheet object for new sheet
- */
- _onNewDocument: function () {
- this._suppressAdd = true;
- this._debuggee.getStyleSheets().then((styleSheets) => {
- return this._resetStyleSheetList(styleSheets);
- }).then(null, e => console.error(e));
- },
- /**
- * Add editors for all the given stylesheets to the UI.
- *
- * @param {array} styleSheets
- * Array of StyleSheetFront
- */
- _resetStyleSheetList: Task.async(function* (styleSheets) {
- this._clear();
- this._suppressAdd = false;
- for (let sheet of styleSheets) {
- try {
- yield this._addStyleSheet(sheet);
- } catch (e) {
- this.emit("error", { key: LOAD_ERROR });
- }
- }
- this._root.classList.remove("loading");
- this.emit("stylesheets-reset");
- }),
- /**
- * Remove all editors and add loading indicator.
- */
- _clear: function () {
- // remember selected sheet and line number for next load
- if (this.selectedEditor && this.selectedEditor.sourceEditor) {
- let href = this.selectedEditor.styleSheet.href;
- let {line, ch} = this.selectedEditor.sourceEditor.getCursor();
- this._styleSheetToSelect = {
- stylesheet: href,
- line: line,
- col: ch
- };
- }
- // remember saved file locations
- for (let editor of this.editors) {
- if (editor.savedFile) {
- let identifier = this.getStyleSheetIdentifier(editor.styleSheet);
- this.savedLocations[identifier] = editor.savedFile;
- }
- }
- this._clearStyleSheetEditors();
- this._view.removeAll();
- this.selectedEditor = null;
- // Here the keys are style sheet actors, and the values are
- // promises that resolve to the sheet's editor. See |_addStyleSheet|.
- this._seenSheets = new Map();
- this._suppressAdd = true;
- this._root.classList.add("loading");
- },
- /**
- * Add an editor for this stylesheet. Add editors for its original sources
- * instead (e.g. Sass sources), if applicable.
- *
- * @param {StyleSheetFront} styleSheet
- * Style sheet to add to style editor
- * @param {Boolean} isNew
- * True if this style sheet was created by a call to the
- * style sheets actor's @see addStyleSheet method.
- * @return {Promise}
- * A promise that resolves to the style sheet's editor when the style sheet has
- * been fully loaded. If the style sheet has a source map, and source mapping
- * is enabled, then the promise resolves to null.
- */
- _addStyleSheet: function (styleSheet, isNew) {
- if (this._suppressAdd) {
- return null;
- }
- if (!this._seenSheets.has(styleSheet)) {
- let promise = (async () => {
- let editor = await this._addStyleSheetEditor(styleSheet, isNew);
- if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
- return editor;
- }
- let sources = await styleSheet.getOriginalSources();
- // A single generated sheet might map to multiple original
- // sheets, so make editors for each of them.
- if (sources && sources.length) {
- let parentEditorName = editor.friendlyName;
- this._removeStyleSheetEditor(editor);
- editor = null;
- for (let source of sources) {
- // set so the first sheet will be selected, even if it's a source
- source.styleSheetIndex = styleSheet.styleSheetIndex;
- source.relatedStyleSheet = styleSheet;
- source.relatedEditorName = parentEditorName;
- await this._addStyleSheetEditor(source);
- }
- }
- return editor;
- })();
- this._seenSheets.set(styleSheet, promise);
- }
- return this._seenSheets.get(styleSheet);
- },
- /**
- * Add a new editor to the UI for a source.
- *
- * @param {StyleSheet} styleSheet
- * Object representing stylesheet
- * @param {Boolean} isNew
- * Optional if stylesheet is a new sheet created by user
- * @return {Promise} that is resolved with the created StyleSheetEditor when
- * the editor is fully initialized or rejected on error.
- */
- _addStyleSheetEditor: Task.async(function* (styleSheet, isNew) {
- // recall location of saved file for this sheet after page reload
- let file = null;
- let identifier = this.getStyleSheetIdentifier(styleSheet);
- let savedFile = this.savedLocations[identifier];
- if (savedFile) {
- file = savedFile;
- }
- let editor = new StyleSheetEditor(styleSheet, this._window, file, isNew,
- this._walker, this._highlighter);
- editor.on("property-change", this._summaryChange.bind(this, editor));
- editor.on("media-rules-changed", this._updateMediaList.bind(this, editor));
- editor.on("linked-css-file", this._summaryChange.bind(this, editor));
- editor.on("linked-css-file-error", this._summaryChange.bind(this, editor));
- editor.on("error", this._onError);
- this.editors.push(editor);
- yield editor.fetchSource();
- this._sourceLoaded(editor);
- return editor;
- }),
- /**
- * Import a style sheet from file and asynchronously create a
- * new stylesheet on the debuggee for it.
- *
- * @param {mixed} file
- * Optional nsIFile or filename string.
- * If not set a file picker will be shown.
- * @param {nsIWindow} parentWindow
- * Optional parent window for the file picker.
- */
- _importFromFile: function (file, parentWindow) {
- let onFileSelected = (selectedFile) => {
- if (!selectedFile) {
- // nothing selected
- return;
- }
- NetUtil.asyncFetch({
- uri: NetUtil.newURI(selectedFile),
- loadingNode: this._window.document,
- securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_INHERITS,
- contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER
- }, (stream, status) => {
- if (!Components.isSuccessCode(status)) {
- this.emit("error", { key: LOAD_ERROR });
- return;
- }
- let source =
- NetUtil.readInputStreamToString(stream, stream.available());
- stream.close();
- this._suppressAdd = true;
- this._debuggee.addStyleSheet(source).then((styleSheet) => {
- this._suppressAdd = false;
- this._addStyleSheet(styleSheet, true).then(editor => {
- if (editor) {
- editor.savedFile = selectedFile;
- }
- // Just for testing purposes.
- this.emit("test:editor-updated", editor);
- });
- });
- });
- };
- showFilePicker(file, false, parentWindow, onFileSelected);
- },
- /**
- * Forward any error from a stylesheet.
- *
- * @param {string} event
- * Event name
- * @param {data} data
- * The event data
- */
- _onError: function (event, data) {
- this.emit("error", data);
- },
- /**
- * Toggle the original sources pref.
- */
- _toggleOrigSources: function () {
- let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
- Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
- },
- /**
- * Toggle the pref for showing a @media rules sidebar in each editor.
- */
- _toggleMediaSidebar: function () {
- let isEnabled = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR);
- Services.prefs.setBoolPref(PREF_MEDIA_SIDEBAR, !isEnabled);
- },
- /**
- * Toggle the @media sidebar in each editor depending on the setting.
- */
- _onMediaPrefChanged: function () {
- this.editors.forEach(this._updateMediaList);
- },
- /**
- * This method handles the following cases related to the context
- * menu item "_openLinkNewTabItem":
- *
- * 1) There was a stylesheet clicked on and it is external: show and
- * enable the context menu item
- * 2) There was a stylesheet clicked on and it is inline: show and
- * disable the context menu item
- * 3) There was no stylesheet clicked on (the right click happened
- * below the list): hide the context menu
- */
- _updateOpenLinkItem: function () {
- this._openLinkNewTabItem.setAttribute("hidden",
- !this._contextMenuStyleSheet);
- if (this._contextMenuStyleSheet) {
- this._openLinkNewTabItem.setAttribute("disabled",
- !this._contextMenuStyleSheet.href);
- }
- },
- /**
- * Open a particular stylesheet in a new tab.
- */
- _openLinkNewTab: function () {
- if (this._contextMenuStyleSheet) {
- this._window.openUILinkIn(this._contextMenuStyleSheet.href, "tab");
- }
- },
- /**
- * Remove a particular stylesheet editor from the UI
- *
- * @param {StyleSheetEditor} editor
- * The editor to remove.
- */
- _removeStyleSheetEditor: function (editor) {
- if (editor.summary) {
- this._view.removeItem(editor.summary);
- } else {
- let self = this;
- this.on("editor-added", function onAdd(event, added) {
- if (editor == added) {
- self.off("editor-added", onAdd);
- self._view.removeItem(editor.summary);
- }
- });
- }
- editor.destroy();
- this.editors.splice(this.editors.indexOf(editor), 1);
- },
- /**
- * Clear all the editors from the UI.
- */
- _clearStyleSheetEditors: function () {
- for (let editor of this.editors) {
- editor.destroy();
- }
- this.editors = [];
- },
- /**
- * Called when a StyleSheetEditor's source has been fetched. Create a
- * summary UI for the editor.
- *
- * @param {StyleSheetEditor} editor
- * Editor to create UI for.
- */
- _sourceLoaded: function (editor) {
- let ordinal = editor.styleSheet.styleSheetIndex;
- ordinal = ordinal == -1 ? Number.MAX_SAFE_INTEGER : ordinal;
- // add new sidebar item and editor to the UI
- this._view.appendTemplatedItem(STYLE_EDITOR_TEMPLATE, {
- data: {
- editor: editor
- },
- disableAnimations: this._alwaysDisableAnimations,
- ordinal: ordinal,
- onCreate: function (summary, details, data) {
- let createdEditor = data.editor;
- createdEditor.summary = summary;
- createdEditor.details = details;
- wire(summary, ".stylesheet-enabled", function onToggleDisabled(event) {
- event.stopPropagation();
- event.target.blur();
- createdEditor.toggleDisabled();
- });
- wire(summary, ".stylesheet-name", {
- events: {
- "keypress": (event) => {
- if (event.keyCode == KeyCodes.DOM_VK_RETURN) {
- this._view.activeSummary = summary;
- }
- }
- }
- });
- wire(summary, ".stylesheet-saveButton", function onSaveButton(event) {
- event.stopPropagation();
- event.target.blur();
- createdEditor.saveToFile(createdEditor.savedFile);
- });
- this._updateSummaryForEditor(createdEditor, summary);
- summary.addEventListener("contextmenu", () => {
- this._contextMenuStyleSheet = createdEditor.styleSheet;
- }, false);
- summary.addEventListener("focus", function onSummaryFocus(event) {
- if (event.target == summary) {
- // autofocus the stylesheet name
- summary.querySelector(".stylesheet-name").focus();
- }
- }, false);
- let sidebar = details.querySelector(".stylesheet-sidebar");
- sidebar.setAttribute("width",
- Services.prefs.getIntPref(PREF_SIDEBAR_WIDTH));
- let splitter = details.querySelector(".devtools-side-splitter");
- splitter.addEventListener("mousemove", () => {
- let sidebarWidth = sidebar.getAttribute("width");
- Services.prefs.setIntPref(PREF_SIDEBAR_WIDTH, sidebarWidth);
- // update all @media sidebars for consistency
- let sidebars =
- [...this._panelDoc.querySelectorAll(".stylesheet-sidebar")];
- for (let mediaSidebar of sidebars) {
- mediaSidebar.setAttribute("width", sidebarWidth);
- }
- });
- // autofocus if it's a new user-created stylesheet
- if (createdEditor.isNew) {
- this._selectEditor(createdEditor);
- }
- if (this._isEditorToSelect(createdEditor)) {
- this.switchToSelectedSheet();
- }
- // If this is the first stylesheet and there is no pending request to
- // select a particular style sheet, select this sheet.
- if (!this.selectedEditor && !this._styleSheetBoundToSelect
- && createdEditor.styleSheet.styleSheetIndex == 0) {
- this._selectEditor(createdEditor);
- }
- this.emit("editor-added", createdEditor);
- }.bind(this),
- onShow: function (summary, details, data) {
- let showEditor = data.editor;
- this.selectedEditor = showEditor;
- Task.spawn(function* () {
- if (!showEditor.sourceEditor) {
- // only initialize source editor when we switch to this view
- let inputElement =
- details.querySelector(".stylesheet-editor-input");
- yield showEditor.load(inputElement, this._cssProperties);
- }
- showEditor.onShow();
- this.emit("editor-selected", showEditor);
- // Is there any CSS coverage markup to include?
- let usage = yield csscoverage.getUsage(this._target);
- if (usage == null) {
- return;
- }
- let sheet = showEditor.styleSheet;
- let {reports} = yield usage.createEditorReportForSheet(sheet);
- showEditor.removeAllUnusedRegions();
- if (reports.length > 0) {
- // Only apply if this file isn't compressed. We detect a
- // compressed file if there are more rules than lines.
- let editorText = showEditor.sourceEditor.getText();
- let lineCount = editorText.split("\n").length;
- let ruleCount = showEditor.styleSheet.ruleCount;
- if (lineCount >= ruleCount) {
- showEditor.addUnusedRegions(reports);
- } else {
- this.emit("error", { key: "error-compressed", level: "info" });
- }
- }
- }.bind(this)).then(null, e => console.error(e));
- }.bind(this)
- });
- },
- /**
- * Switch to the editor that has been marked to be selected.
- *
- * @return {Promise}
- * Promise that will resolve when the editor is selected.
- */
- switchToSelectedSheet: function () {
- let toSelect = this._styleSheetToSelect;
- for (let editor of this.editors) {
- if (this._isEditorToSelect(editor)) {
- // The _styleSheetBoundToSelect will always hold the latest pending
- // requested style sheet (with line and column) which is not yet
- // selected by the source editor. Only after we select that particular
- // editor and go the required line and column, it will become null.
- this._styleSheetBoundToSelect = this._styleSheetToSelect;
- this._styleSheetToSelect = null;
- return this._selectEditor(editor, toSelect.line, toSelect.col);
- }
- }
- return promise.resolve();
- },
- /**
- * Returns whether a given editor is the current editor to be selected. Tests
- * based on href or underlying stylesheet.
- *
- * @param {StyleSheetEditor} editor
- * The editor to test.
- */
- _isEditorToSelect: function (editor) {
- let toSelect = this._styleSheetToSelect;
- if (!toSelect) {
- return false;
- }
- let isHref = toSelect.stylesheet === null ||
- typeof toSelect.stylesheet == "string";
- return (isHref && editor.styleSheet.href == toSelect.stylesheet) ||
- (toSelect.stylesheet == editor.styleSheet);
- },
- /**
- * Select an editor in the UI.
- *
- * @param {StyleSheetEditor} editor
- * Editor to switch to.
- * @param {number} line
- * Line number to jump to
- * @param {number} col
- * Column number to jump to
- * @return {Promise}
- * Promise that will resolve when the editor is selected and ready
- * to be used.
- */
- _selectEditor: function (editor, line, col) {
- line = line || 0;
- col = col || 0;
- let editorPromise = editor.getSourceEditor().then(() => {
- editor.sourceEditor.setCursor({line: line, ch: col});
- this._styleSheetBoundToSelect = null;
- });
- let summaryPromise = this.getEditorSummary(editor).then((summary) => {
- this._view.activeSummary = summary;
- });
- return promise.all([editorPromise, summaryPromise]);
- },
- getEditorSummary: function (editor) {
- if (editor.summary) {
- return promise.resolve(editor.summary);
- }
- let deferred = defer();
- let self = this;
- this.on("editor-added", function onAdd(e, selected) {
- if (selected == editor) {
- self.off("editor-added", onAdd);
- deferred.resolve(editor.summary);
- }
- });
- return deferred.promise;
- },
- getEditorDetails: function (editor) {
- if (editor.details) {
- return promise.resolve(editor.details);
- }
- let deferred = defer();
- let self = this;
- this.on("editor-added", function onAdd(e, selected) {
- if (selected == editor) {
- self.off("editor-added", onAdd);
- deferred.resolve(editor.details);
- }
- });
- return deferred.promise;
- },
- /**
- * Returns an identifier for the given style sheet.
- *
- * @param {StyleSheet} styleSheet
- * The style sheet to be identified.
- */
- getStyleSheetIdentifier: function (styleSheet) {
- // Identify inline style sheets by their host page URI and index
- // at the page.
- return styleSheet.href ? styleSheet.href :
- "inline-" + styleSheet.styleSheetIndex + "-at-" + styleSheet.nodeHref;
- },
- /**
- * selects a stylesheet and optionally moves the cursor to a selected line
- *
- * @param {StyleSheetFront} [stylesheet]
- * Stylesheet to select or href of stylesheet to select
- * @param {Number} [line]
- * Line to which the caret should be moved (zero-indexed).
- * @param {Number} [col]
- * Column to which the caret should be moved (zero-indexed).
- * @return {Promise}
- * Promise that will resolve when the editor is selected and ready
- * to be used.
- */
- selectStyleSheet: function (stylesheet, line, col) {
- this._styleSheetToSelect = {
- stylesheet: stylesheet,
- line: line,
- col: col,
- };
- /* Switch to the editor for this sheet, if it exists yet.
- Otherwise each editor will be checked when it's created. */
- return this.switchToSelectedSheet();
- },
- /**
- * Handler for an editor's 'property-changed' event.
- * Update the summary in the UI.
- *
- * @param {StyleSheetEditor} editor
- * Editor for which a property has changed
- */
- _summaryChange: function (editor) {
- this._updateSummaryForEditor(editor);
- },
- /**
- * Update split view summary of given StyleEditor instance.
- *
- * @param {StyleSheetEditor} editor
- * @param {DOMElement} summary
- * Optional item's summary element to update. If none, item
- * corresponding to passed editor is used.
- */
- _updateSummaryForEditor: function (editor, summary) {
- summary = summary || editor.summary;
- if (!summary) {
- return;
- }
- let ruleCount = editor.styleSheet.ruleCount;
- if (editor.styleSheet.relatedStyleSheet) {
- ruleCount = editor.styleSheet.relatedStyleSheet.ruleCount;
- }
- if (ruleCount === undefined) {
- ruleCount = "-";
- }
- let flags = [];
- if (editor.styleSheet.disabled) {
- flags.push("disabled");
- }
- if (editor.unsaved) {
- flags.push("unsaved");
- }
- if (editor.linkedCSSFileError) {
- flags.push("linked-file-error");
- }
- this._view.setItemClassName(summary, flags.join(" "));
- let label = summary.querySelector(".stylesheet-name > label");
- label.setAttribute("value", editor.friendlyName);
- if (editor.styleSheet.href) {
- label.setAttribute("tooltiptext", editor.styleSheet.href);
- }
- let linkedCSSSource = "";
- if (editor.linkedCSSFile) {
- linkedCSSSource = OS.Path.basename(editor.linkedCSSFile);
- } else if (editor.styleSheet.relatedEditorName) {
- linkedCSSSource = editor.styleSheet.relatedEditorName;
- }
- text(summary, ".stylesheet-linked-file", linkedCSSSource);
- text(summary, ".stylesheet-title", editor.styleSheet.title || "");
- text(summary, ".stylesheet-rule-count",
- PluralForm.get(ruleCount,
- getString("ruleCount.label")).replace("#1", ruleCount));
- },
- /**
- * Update the @media rules sidebar for an editor. Hide if there are no rules
- * Display a list of the @media rules in the editor's associated style sheet.
- * Emits a 'media-list-changed' event after updating the UI.
- *
- * @param {StyleSheetEditor} editor
- * Editor to update @media sidebar of
- */
- _updateMediaList: function (editor) {
- Task.spawn(function* () {
- let details = yield this.getEditorDetails(editor);
- let list = details.querySelector(".stylesheet-media-list");
- while (list.firstChild) {
- list.removeChild(list.firstChild);
- }
- let rules = editor.mediaRules;
- let showSidebar = Services.prefs.getBoolPref(PREF_MEDIA_SIDEBAR);
- let sidebar = details.querySelector(".stylesheet-sidebar");
- let inSource = false;
- for (let rule of rules) {
- let {line, column, parentStyleSheet} = rule;
- let location = {
- line: line,
- column: column,
- source: editor.styleSheet.href,
- styleSheet: parentStyleSheet
- };
- if (editor.styleSheet.isOriginalSource) {
- location = yield editor.cssSheet.getOriginalLocation(line, column);
- }
- // this @media rule is from a different original source
- if (location.source != editor.styleSheet.href) {
- continue;
- }
- inSource = true;
- let div = this._panelDoc.createElement("div");
- div.className = "media-rule-label";
- div.addEventListener("click",
- this._jumpToLocation.bind(this, location));
- let cond = this._panelDoc.createElement("div");
- cond.className = "media-rule-condition";
- if (!rule.matches) {
- cond.classList.add("media-condition-unmatched");
- }
- if (this._target.tab.tagName == "tab") {
- this._setConditionContents(cond, rule.conditionText);
- } else {
- cond.textContent = rule.conditionText;
- }
- div.appendChild(cond);
- let link = this._panelDoc.createElement("div");
- link.className = "media-rule-line theme-link";
- if (location.line != -1) {
- link.textContent = ":" + location.line;
- }
- div.appendChild(link);
- list.appendChild(div);
- }
- sidebar.hidden = !showSidebar || !inSource;
- this.emit("media-list-changed", editor);
- }.bind(this)).then(null, e => console.error(e));
- },
- /**
- * Used to safely inject media query links
- *
- * @param {HTMLElement} element
- * The element corresponding to the media sidebar condition
- * @param {String} rawText
- * The raw condition text to parse
- */
- _setConditionContents(element, rawText) {
- const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/ig;
- let match = minMaxPattern.exec(rawText);
- let lastParsed = 0;
- while (match && match.index != minMaxPattern.lastIndex) {
- let matchEnd = match.index + match[0].length;
- let node = this._panelDoc.createTextNode(
- rawText.substring(lastParsed, match.index)
- );
- element.appendChild(node);
- let link = this._panelDoc.createElement("a");
- link.href = "#";
- link.className = "media-responsive-mode-toggle";
- link.textContent = rawText.substring(match.index, matchEnd);
- link.addEventListener("click", this._onMediaConditionClick.bind(this));
- element.appendChild(link);
- match = minMaxPattern.exec(rawText);
- lastParsed = matchEnd;
- }
- let node = this._panelDoc.createTextNode(
- rawText.substring(lastParsed, rawText.length)
- );
- element.appendChild(node);
- },
- /**
- * Called when a media condition is clicked
- * If a responsive mode link is clicked, it will launch it.
- *
- * @param {object} e
- * Event object
- */
- _onMediaConditionClick: function (e) {
- let conditionText = e.target.textContent;
- let isWidthCond = conditionText.toLowerCase().indexOf("width") > -1;
- let mediaVal = parseInt(/\d+/.exec(conditionText), 10);
- let options = isWidthCond ? {width: mediaVal} : {height: mediaVal};
- this._launchResponsiveMode(options);
- e.preventDefault();
- e.stopPropagation();
- },
- /**
- * Launches the responsive mode with a specific width or height
- *
- * @param {object} options
- * Object with width or/and height properties.
- */
- _launchResponsiveMode: Task.async(function* (options = {}) {
- let tab = this._target.tab;
- let win = this._target.tab.ownerDocument.defaultView;
- yield ResponsiveUIManager.openIfNeeded(win, tab);
- ResponsiveUIManager.getResponsiveUIForTab(tab).setViewportSize(options);
- }),
- /**
- * Jump cursor to the editor for a stylesheet and line number for a rule.
- *
- * @param {object} location
- * Location object with 'line', 'column', and 'source' properties.
- */
- _jumpToLocation: function (location) {
- let source = location.styleSheet || location.source;
- this.selectStyleSheet(source, location.line - 1, location.column - 1);
- },
- destroy: function () {
- if (this._highlighter) {
- this._highlighter.finalize();
- this._highlighter = null;
- }
- this._clearStyleSheetEditors();
- this._seenSheets = null;
- this._suppressAdd = false;
- let sidebar = this._panelDoc.querySelector(".splitview-controller");
- let sidebarWidth = sidebar.getAttribute("width");
- Services.prefs.setIntPref(PREF_NAV_WIDTH, sidebarWidth);
- this._optionsMenu.removeEventListener("popupshowing",
- this._onOptionsPopupShowing);
- this._optionsMenu.removeEventListener("popuphiding",
- this._onOptionsPopupHiding);
- this._prefObserver.off(PREF_ORIG_SOURCES, this._onNewDocument);
- this._prefObserver.off(PREF_MEDIA_SIDEBAR, this._onMediaPrefChanged);
- this._prefObserver.destroy();
- this._debuggee.off("stylesheet-added", this._addStyleSheet);
- }
- };
|