summaryrefslogtreecommitdiffstats
path: root/devtools/client/styleeditor
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/styleeditor')
-rw-r--r--devtools/client/styleeditor/StyleEditorUI.sys.mjs1849
-rw-r--r--devtools/client/styleeditor/StyleEditorUtil.sys.mjs213
-rw-r--r--devtools/client/styleeditor/StyleSheetEditor.sys.mjs1052
-rw-r--r--devtools/client/styleeditor/index.xhtml256
-rw-r--r--devtools/client/styleeditor/moz.build18
-rw-r--r--devtools/client/styleeditor/original-source.js103
-rw-r--r--devtools/client/styleeditor/panel.js172
-rw-r--r--devtools/client/styleeditor/test/autocomplete.html23
-rw-r--r--devtools/client/styleeditor/test/browser.toml227
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js36
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js340
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js42
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js284
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bom.js37
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_1247083_inline_stylesheet_numbering.js104
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_1405342_serviceworker_iframes.js27
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js107
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js63
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js49
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_copyurl.js43
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_enabled.js135
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_fetch-from-netmonitor.js80
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_filesave.js93
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_filter.js343
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_fission_switch_target.js33
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js223
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_import.js56
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_import_rule.js32
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_init.js52
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js103
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_loading.js36
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js76
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js165
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js62
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js37
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_navigate.js31
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_new.js106
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_nostyle.js56
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_opentab.js133
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_pretty.js112
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js76
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_reload.js42
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_remove_stylesheet.js29
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_resize_performance.js63
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_scroll.js96
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js25
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sidebars.js67
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemap_chrome.js47
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js34
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js157
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js152
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js89
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js85
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js53
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_sync.js73
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js52
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js30
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js50
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js38
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js41
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js51
-rw-r--r--devtools/client/styleeditor/test/browser_styleeditor_xul.js23
-rw-r--r--devtools/client/styleeditor/test/browser_toolbox_styleeditor.js104
-rw-r--r--devtools/client/styleeditor/test/bug_1405342_serviceworker_iframes.html10
-rw-r--r--devtools/client/styleeditor/test/doc_empty.html3
-rw-r--r--devtools/client/styleeditor/test/doc_fetch_from_netmonitor.html13
-rw-r--r--devtools/client/styleeditor/test/doc_long.css402
-rw-r--r--devtools/client/styleeditor/test/doc_long_string.css43
-rw-r--r--devtools/client/styleeditor/test/doc_short_string.css15
-rw-r--r--devtools/client/styleeditor/test/doc_sourcemap_chrome.html11
-rw-r--r--devtools/client/styleeditor/test/doc_xulpage.xhtml7
-rw-r--r--devtools/client/styleeditor/test/four.html25
-rw-r--r--devtools/client/styleeditor/test/head.js201
-rw-r--r--devtools/client/styleeditor/test/iframe_service_worker.js12
-rw-r--r--devtools/client/styleeditor/test/iframe_with_service_worker.html33
-rw-r--r--devtools/client/styleeditor/test/import.css8
-rw-r--r--devtools/client/styleeditor/test/import.html11
-rw-r--r--devtools/client/styleeditor/test/import2.css8
-rw-r--r--devtools/client/styleeditor/test/inline-1.html19
-rw-r--r--devtools/client/styleeditor/test/inline-2.html19
-rw-r--r--devtools/client/styleeditor/test/longload.html29
-rw-r--r--devtools/client/styleeditor/test/longname.html12
-rw-r--r--devtools/client/styleeditor/test/many-media-rules-sourcemaps/index.html11
-rw-r--r--devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css201
-rw-r--r--devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css.map10
-rw-r--r--devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/_partial.scss25
-rw-r--r--devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/sourcemaps.scss27
-rw-r--r--devtools/client/styleeditor/test/media-rules-sourcemaps.html12
-rw-r--r--devtools/client/styleeditor/test/media-rules.css29
-rw-r--r--devtools/client/styleeditor/test/media-rules.html48
-rw-r--r--devtools/client/styleeditor/test/media-small.css4
-rw-r--r--devtools/client/styleeditor/test/media.html10
-rw-r--r--devtools/client/styleeditor/test/minified.html16
-rw-r--r--devtools/client/styleeditor/test/missing.html11
-rw-r--r--devtools/client/styleeditor/test/nostyle.html5
-rw-r--r--devtools/client/styleeditor/test/pretty.css2
-rw-r--r--devtools/client/styleeditor/test/resources_inpage.jsi12
-rw-r--r--devtools/client/styleeditor/test/resources_inpage1.css11
-rw-r--r--devtools/client/styleeditor/test/resources_inpage2.css11
-rw-r--r--devtools/client/styleeditor/test/selector-highlighter.html1
-rw-r--r--devtools/client/styleeditor/test/simple.css7
-rw-r--r--devtools/client/styleeditor/test/simple.css.gzbin0 -> 166 bytes
-rw-r--r--devtools/client/styleeditor/test/simple.css.gz^headers^4
-rw-r--r--devtools/client/styleeditor/test/simple.gz.html23
-rw-r--r--devtools/client/styleeditor/test/simple.html24
-rw-r--r--devtools/client/styleeditor/test/sjs_huge-css-server.sjs19
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/contained.css4
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/media-rules.css8
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map6
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css7
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map6
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map^headers^2
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/sourcemaps_chrome.css7
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css4513
-rw-r--r--devtools/client/styleeditor/test/sourcemap-css/test-stylus.css7
-rw-r--r--devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss11
-rw-r--r--devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss10
-rw-r--r--devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss^headers^2
-rw-r--r--devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl7
-rw-r--r--devtools/client/styleeditor/test/sourcemaps-inline.html17
-rw-r--r--devtools/client/styleeditor/test/sourcemaps-large.html11
-rw-r--r--devtools/client/styleeditor/test/sourcemaps-watching.html11
-rw-r--r--devtools/client/styleeditor/test/sourcemaps.html13
-rw-r--r--devtools/client/styleeditor/test/sync.html20
-rw-r--r--devtools/client/styleeditor/test/sync_with_csp.css10
-rw-r--r--devtools/client/styleeditor/test/sync_with_csp.html12
-rw-r--r--devtools/client/styleeditor/test/test_private.css3
-rw-r--r--devtools/client/styleeditor/test/test_private.html7
-rw-r--r--devtools/client/styleeditor/test/utf-16.cssbin0 -> 156 bytes
-rw-r--r--devtools/client/styleeditor/test/veryveryverylongnamethatcanbreakthestyleeditor.css7
130 files changed, 14591 insertions, 0 deletions
diff --git a/devtools/client/styleeditor/StyleEditorUI.sys.mjs b/devtools/client/styleeditor/StyleEditorUI.sys.mjs
new file mode 100644
index 0000000000..d48c50f556
--- /dev/null
+++ b/devtools/client/styleeditor/StyleEditorUI.sys.mjs
@@ -0,0 +1,1849 @@
+/* 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/. */
+
+import {
+ loader,
+ require,
+} from "resource://devtools/shared/loader/Loader.sys.mjs";
+
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+import {
+ getString,
+ text,
+ showFilePicker,
+ optionsPopupMenu,
+} from "resource://devtools/client/styleeditor/StyleEditorUtil.sys.mjs";
+import { StyleSheetEditor } from "resource://devtools/client/styleeditor/StyleSheetEditor.sys.mjs";
+
+const { PrefObserver } = require("resource://devtools/client/shared/prefs.js");
+
+const KeyShortcuts = require("resource://devtools/client/shared/key-shortcuts.js");
+const {
+ shortSource,
+} = require("resource://devtools/shared/inspector/css-logic.js");
+
+const lazy = {};
+
+loader.lazyRequireGetter(
+ lazy,
+ "KeyCodes",
+ "resource://devtools/client/shared/keycodes.js",
+ true
+);
+
+loader.lazyRequireGetter(
+ lazy,
+ "OriginalSource",
+ "resource://devtools/client/styleeditor/original-source.js",
+ true
+);
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+loader.lazyRequireGetter(
+ lazy,
+ "ResponsiveUIManager",
+ "resource://devtools/client/responsive/manager.js"
+);
+loader.lazyRequireGetter(
+ lazy,
+ "openContentLink",
+ "resource://devtools/client/shared/link.js",
+ true
+);
+loader.lazyRequireGetter(
+ lazy,
+ "copyString",
+ "resource://devtools/shared/platform/clipboard.js",
+ true
+);
+
+const LOAD_ERROR = "error-load";
+const PREF_AT_RULES_SIDEBAR = "devtools.styleeditor.showAtRulesSidebar";
+const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.atRulesSidebarWidth";
+const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
+const PREF_ORIG_SOURCES = "devtools.source-map.client-service.enabled";
+
+const FILTERED_CLASSNAME = "splitview-filtered";
+const ALL_FILTERED_CLASSNAME = "splitview-all-filtered";
+
+const HTML_NS = "http://www.w3.org/1999/xhtml";
+
+/**
+ * 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
+ *
+ */
+export class StyleEditorUI extends EventEmitter {
+ #activeSummary = null;
+ #commands;
+ #contextMenu;
+ #contextMenuStyleSheet;
+ #copyUrlItem;
+ #cssProperties;
+ #filter;
+ #filterInput;
+ #filterInputClearButton;
+ #loadingStyleSheets;
+ #nav;
+ #openLinkNewTabItem;
+ #optionsButton;
+ #optionsMenu;
+ #panelDoc;
+ #prefObserver;
+ #prettyPrintButton;
+ #root;
+ #seenSheets = new Map();
+ #shortcuts;
+ #side;
+ #sourceMapPrefObserver;
+ #styleSheetBoundToSelect;
+ #styleSheetToSelect;
+ /**
+ * Maps keyed by summary element whose value is an object containing:
+ * - {Element} details: The associated details element (i.e. container for CodeMirror)
+ * - {StyleSheetEditor} editor: The associated editor, for easy retrieval
+ */
+ #summaryDataMap = new WeakMap();
+ #toolbox;
+ #tplDetails;
+ #tplSummary;
+ #uiAbortController = new AbortController();
+ #window;
+
+ /**
+ * @param {Toolbox} toolbox
+ * @param {Object} commands Object defined from devtools/shared/commands to interact with the devtools backend
+ * @param {Document} panelDoc
+ * Document of the toolbox panel to populate UI in.
+ * @param {CssProperties} A css properties database.
+ */
+ constructor(toolbox, commands, panelDoc, cssProperties) {
+ super();
+
+ this.#toolbox = toolbox;
+ this.#commands = commands;
+ 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.#prefObserver = new PrefObserver("devtools.styleeditor.");
+ this.#prefObserver.on(
+ PREF_AT_RULES_SIDEBAR,
+ this.#onAtRulesSidebarPrefChanged
+ );
+ this.#sourceMapPrefObserver = new PrefObserver(
+ "devtools.source-map.client-service."
+ );
+ this.#sourceMapPrefObserver.on(
+ PREF_ORIG_SOURCES,
+ this.#onOrigSourcesPrefChanged
+ );
+ }
+
+ get cssProperties() {
+ return this.#cssProperties;
+ }
+
+ get currentTarget() {
+ return this.#commands.targetCommand.targetFront;
+ }
+
+ /*
+ * Index of selected stylesheet in document.styleSheets
+ */
+ get selectedStyleSheetIndex() {
+ return this.selectedEditor
+ ? this.selectedEditor.styleSheet.styleSheetIndex
+ : -1;
+ }
+
+ /**
+ * Initiates the style editor ui creation, and start to track TargetCommand updates.
+ *
+ * @params {Object} options
+ * @params {Object} options.stylesheetToSelect
+ * @params {StyleSheetResource} options.stylesheetToSelect.stylesheet
+ * @params {Integer} options.stylesheetToSelect.line
+ * @params {Integer} options.stylesheetToSelect.column
+ */
+ async initialize(options = {}) {
+ this.createUI();
+
+ if (options.stylesheetToSelect) {
+ const { stylesheet, line, column } = options.stylesheetToSelect;
+ // If a stylesheet resource and its location was passed (e.g. user clicked on a stylesheet
+ // location in the rule view), we can directly add it to the list and select it
+ // before watching for resources, for improved performance.
+ if (stylesheet.resourceId) {
+ try {
+ await this.#handleStyleSheetResource(stylesheet);
+ await this.selectStyleSheet(
+ stylesheet,
+ line - 1,
+ column ? column - 1 : 0
+ );
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+
+ await this.#toolbox.resourceCommand.watchResources(
+ [this.#toolbox.resourceCommand.TYPES.DOCUMENT_EVENT],
+ { onAvailable: this.#onResourceAvailable }
+ );
+ await this.#commands.targetCommand.watchTargets({
+ types: [this.#commands.targetCommand.TYPES.FRAME],
+ onAvailable: this.#onTargetAvailable,
+ onDestroyed: this.#onTargetDestroyed,
+ });
+
+ this.#startLoadingStyleSheets();
+ await this.#toolbox.resourceCommand.watchResources(
+ [this.#toolbox.resourceCommand.TYPES.STYLESHEET],
+ {
+ onAvailable: this.#onResourceAvailable,
+ onUpdated: this.#onResourceUpdated,
+ onDestroyed: this.#onResourceDestroyed,
+ }
+ );
+ await this.#waitForLoadingStyleSheets();
+ }
+
+ /**
+ * Build the initial UI and wire buttons with event handlers.
+ */
+ createUI() {
+ this.#filterInput = this.#root.querySelector(".devtools-filterinput");
+ this.#filterInputClearButton = this.#root.querySelector(
+ ".devtools-searchinput-clear"
+ );
+ this.#nav = this.#root.querySelector(".splitview-nav");
+ this.#side = this.#root.querySelector(".splitview-side-details");
+ this.#tplSummary = this.#root.querySelector(
+ "#splitview-tpl-summary-stylesheet"
+ );
+ this.#tplDetails = this.#root.querySelector(
+ "#splitview-tpl-details-stylesheet"
+ );
+
+ const eventListenersConfig = { signal: this.#uiAbortController.signal };
+
+ // Add click event on the "new stylesheet" button in the toolbar and on the
+ // "append a new stylesheet" link (visible when there are no stylesheets).
+ for (const el of this.#root.querySelectorAll(".style-editor-newButton")) {
+ el.addEventListener(
+ "click",
+ async () => {
+ const stylesheetsFront = await this.currentTarget.getFront(
+ "stylesheets"
+ );
+ stylesheetsFront.addStyleSheet(null);
+ this.#clearFilterInput();
+ },
+ eventListenersConfig
+ );
+ }
+
+ this.#root.querySelector(".style-editor-importButton").addEventListener(
+ "click",
+ () => {
+ this.#importFromFile(this._mockImportFile || null, this.#window);
+ this.#clearFilterInput();
+ },
+ eventListenersConfig
+ );
+
+ this.#prettyPrintButton = this.#root.querySelector(
+ ".style-editor-prettyPrintButton"
+ );
+ this.#prettyPrintButton.addEventListener(
+ "click",
+ () => {
+ if (!this.selectedEditor) {
+ return;
+ }
+
+ this.selectedEditor.prettifySourceText();
+ },
+ eventListenersConfig
+ );
+
+ this.#root
+ .querySelector("#style-editor-options")
+ .addEventListener(
+ "click",
+ this.#onOptionsButtonClick,
+ eventListenersConfig
+ );
+
+ this.#filterInput.addEventListener(
+ "input",
+ this.#onFilterInputChange,
+ eventListenersConfig
+ );
+
+ this.#filterInputClearButton.addEventListener(
+ "click",
+ () => this.#clearFilterInput(),
+ eventListenersConfig
+ );
+
+ this.#panelDoc.addEventListener(
+ "contextmenu",
+ () => {
+ this.#contextMenuStyleSheet = null;
+ },
+ { ...eventListenersConfig, capture: true }
+ );
+
+ this.#optionsButton = this.#panelDoc.getElementById("style-editor-options");
+
+ this.#contextMenu = this.#panelDoc.getElementById("sidebar-context");
+ this.#contextMenu.addEventListener(
+ "popupshowing",
+ this.#updateContextMenuItems,
+ eventListenersConfig
+ );
+
+ this.#openLinkNewTabItem = this.#panelDoc.getElementById(
+ "context-openlinknewtab"
+ );
+ this.#openLinkNewTabItem.addEventListener(
+ "command",
+ this.#openLinkNewTab,
+ eventListenersConfig
+ );
+
+ this.#copyUrlItem = this.#panelDoc.getElementById("context-copyurl");
+ this.#copyUrlItem.addEventListener(
+ "command",
+ this.#copyUrl,
+ eventListenersConfig
+ );
+
+ // items list focus and search-on-type handling
+ this.#nav.addEventListener(
+ "keydown",
+ this.#onNavKeyDown,
+ eventListenersConfig
+ );
+
+ this.#shortcuts = new KeyShortcuts({
+ window: this.#window,
+ });
+ this.#shortcuts.on(
+ `CmdOrCtrl+${getString("focusFilterInput.commandkey")}`,
+ this.#onFocusFilterInputKeyboardShortcut
+ );
+
+ const nav = this.#panelDoc.querySelector(".splitview-controller");
+ nav.style.width = Services.prefs.getIntPref(PREF_NAV_WIDTH) + "px";
+ }
+
+ #clearFilterInput() {
+ this.#filterInput.value = "";
+ this.#onFilterInputChange();
+ }
+
+ #onFilterInputChange = () => {
+ this.#filter = this.#filterInput.value;
+ this.#filterInputClearButton.toggleAttribute("hidden", !this.#filter);
+
+ for (const summary of this.#nav.childNodes) {
+ // Don't update nav class for every element, we do it after the loop.
+ this.handleSummaryVisibility(summary, {
+ triggerOnFilterStateChange: false,
+ });
+ }
+
+ this.#onFilterStateChange();
+
+ if (this.#activeSummary == null) {
+ const firstVisibleSummary = Array.from(this.#nav.childNodes).find(
+ node => !node.classList.contains(FILTERED_CLASSNAME)
+ );
+
+ if (firstVisibleSummary) {
+ this.setActiveSummary(firstVisibleSummary, { reason: "filter-auto" });
+ }
+ }
+ };
+
+ #onFilterStateChange() {
+ const summaries = Array.from(this.#nav.childNodes);
+ const hasVisibleSummary = summaries.some(
+ node => !node.classList.contains(FILTERED_CLASSNAME)
+ );
+ const allFiltered = !!summaries.length && !hasVisibleSummary;
+
+ this.#nav.classList.toggle(ALL_FILTERED_CLASSNAME, allFiltered);
+
+ this.#filterInput
+ .closest(".devtools-searchbox")
+ .classList.toggle("devtools-searchbox-no-match", !!allFiltered);
+ }
+
+ #onFocusFilterInputKeyboardShortcut = e => {
+ // Prevent the print modal to be displayed.
+ if (e) {
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ this.#filterInput.select();
+ };
+
+ #onNavKeyDown = event => {
+ function getFocusedItemWithin(nav) {
+ let node = nav.ownerDocument.activeElement;
+ while (node && node.parentNode != nav) {
+ node = node.parentNode;
+ }
+ return node;
+ }
+
+ // do not steal focus from inside iframes or textboxes
+ if (
+ event.target.ownerDocument != this.#nav.ownerDocument ||
+ event.target.tagName == "input" ||
+ event.target.tagName == "textarea" ||
+ event.target.classList.contains("textbox")
+ ) {
+ return false;
+ }
+
+ // handle keyboard navigation within the items list
+ const visibleElements = Array.from(
+ this.#nav.querySelectorAll(`li:not(.${FILTERED_CLASSNAME})`)
+ );
+ // Elements have a different visual order (due to the use of order), so
+ // we need to sort them by their data-ordinal attribute
+ visibleElements.sort(
+ (a, b) => a.getAttribute("data-ordinal") - b.getAttribute("data-ordinal")
+ );
+
+ let elementToFocus;
+ if (
+ event.keyCode == lazy.KeyCodes.DOM_VK_PAGE_UP ||
+ event.keyCode == lazy.KeyCodes.DOM_VK_HOME
+ ) {
+ elementToFocus = visibleElements[0];
+ } else if (
+ event.keyCode == lazy.KeyCodes.DOM_VK_PAGE_DOWN ||
+ event.keyCode == lazy.KeyCodes.DOM_VK_END
+ ) {
+ elementToFocus = visibleElements.at(-1);
+ } else if (event.keyCode == lazy.KeyCodes.DOM_VK_UP) {
+ const focusedIndex = visibleElements.indexOf(
+ getFocusedItemWithin(this.#nav)
+ );
+ elementToFocus = visibleElements[focusedIndex - 1];
+ } else if (event.keyCode == lazy.KeyCodes.DOM_VK_DOWN) {
+ const focusedIndex = visibleElements.indexOf(
+ getFocusedItemWithin(this.#nav)
+ );
+ elementToFocus = visibleElements[focusedIndex + 1];
+ }
+
+ if (elementToFocus !== undefined) {
+ event.stopPropagation();
+ event.preventDefault();
+ elementToFocus.focus();
+ return false;
+ }
+
+ return true;
+ };
+
+ /**
+ * Opens the Options Popup Menu
+ *
+ * @params {number} screenX
+ * @params {number} screenY
+ * Both obtained from the event object, used to position the popup
+ */
+ #onOptionsButtonClick = ({ screenX, screenY }) => {
+ this.#optionsMenu = optionsPopupMenu(
+ this.#toggleOrigSources,
+ this.#toggleAtRulesSidebar
+ );
+
+ this.#optionsMenu.once("open", () => {
+ this.#optionsButton.setAttribute("open", true);
+ });
+ this.#optionsMenu.once("close", () => {
+ this.#optionsButton.removeAttribute("open");
+ });
+
+ this.#optionsMenu.popup(screenX, screenY, this.#toolbox.doc);
+ };
+
+ /**
+ * Be called when changing the original sources pref.
+ */
+ #onOrigSourcesPrefChanged = async () => {
+ this.#clear();
+ // When we toggle the source-map preference, we clear the panel and re-fetch the exact
+ // same stylesheet resources from ResourceCommand, but `_addStyleSheet` will trigger
+ // or ignore the additional source-map mapping.
+ this.#root.classList.add("loading");
+ for (const resource of this.#toolbox.resourceCommand.getAllResources(
+ this.#toolbox.resourceCommand.TYPES.STYLESHEET
+ )) {
+ await this.#handleStyleSheetResource(resource);
+ }
+
+ this.#root.classList.remove("loading");
+
+ this.emit("stylesheets-refreshed");
+ };
+
+ /**
+ * Remove all editors and add loading indicator.
+ */
+ #clear = () => {
+ // remember selected sheet and line number for next load
+ if (this.selectedEditor && this.selectedEditor.sourceEditor) {
+ const href = this.selectedEditor.styleSheet.href;
+ const { line, ch } = this.selectedEditor.sourceEditor.getCursor();
+
+ this.#styleSheetToSelect = {
+ stylesheet: href,
+ line,
+ col: ch,
+ };
+ }
+
+ // remember saved file locations
+ for (const editor of this.editors) {
+ if (editor.savedFile) {
+ const identifier = this.getStyleSheetIdentifier(editor.styleSheet);
+ this.savedLocations[identifier] = editor.savedFile;
+ }
+ }
+
+ this.#clearStyleSheetEditors();
+ // Clear the left sidebar items and their associated elements.
+ while (this.#nav.hasChildNodes()) {
+ this.removeSplitViewItem(this.#nav.firstChild);
+ }
+
+ 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.emit("stylesheets-clear");
+ };
+
+ /**
+ * Add an editor for this stylesheet. Add editors for its original sources
+ * instead (e.g. Sass sources), if applicable.
+ *
+ * @param {Resource} resource
+ * The STYLESHEET resource which is received from resource command.
+ * @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(resource) {
+ if (!this.#seenSheets.has(resource)) {
+ const promise = (async () => {
+ // When the StyleSheet is mapped to one or many original sources,
+ // do not create an editor for the minified StyleSheet.
+ const hasValidOriginalSource = await this.#tryAddingOriginalStyleSheets(
+ resource
+ );
+ if (hasValidOriginalSource) {
+ return null;
+ }
+ // Otherwise, if source-map failed or this is a non-source-map CSS
+ // create an editor for it.
+ return this.#addStyleSheetEditor(resource);
+ })();
+ this.#seenSheets.set(resource, promise);
+ }
+ return this.#seenSheets.get(resource);
+ }
+
+ /**
+ * Check if the given StyleSheet relates to an original StyleSheet (via source maps).
+ * If one is found, create an editor for the original one.
+ *
+ * @param {Resource} resource
+ * The STYLESHEET resource which is received from resource command.
+ * @return Boolean
+ * Return true, when we found a viable related original StyleSheet.
+ */
+ async #tryAddingOriginalStyleSheets(resource) {
+ // Avoid querying the SourceMap if this feature is disabled.
+ if (!Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
+ return false;
+ }
+
+ const sourceMapLoader = this.#toolbox.sourceMapLoader;
+ const {
+ href,
+ nodeHref,
+ resourceId: id,
+ sourceMapURL,
+ sourceMapBaseURL,
+ } = resource;
+ let sources;
+ try {
+ sources = await sourceMapLoader.getOriginalURLs({
+ id,
+ url: href || nodeHref,
+ sourceMapBaseURL,
+ sourceMapURL,
+ });
+ } catch (e) {
+ // Ignore any source map error, they will be logged
+ // via the SourceMapLoader and Toolbox into the Web Console.
+ return false;
+ }
+
+ // Return the generated CSS if the source-map failed to be parsed
+ // or did not generate any original source.
+ if (!sources || !sources.length) {
+ return false;
+ }
+
+ // A single generated sheet might map to multiple original
+ // sheets, so make editors for each of them.
+ for (const { id: originalId, url: originalURL } of sources) {
+ const original = new lazy.OriginalSource(
+ originalURL,
+ originalId,
+ sourceMapLoader
+ );
+
+ // set so the first sheet will be selected, even if it's a source
+ original.styleSheetIndex = resource.styleSheetIndex;
+ original.relatedStyleSheet = resource;
+ original.resourceId = resource.resourceId;
+ original.targetFront = resource.targetFront;
+ original.atRules = resource.atRules;
+ await this.#addStyleSheetEditor(original);
+ }
+
+ return true;
+ }
+
+ #removeStyleSheet(resource, editor) {
+ this.#seenSheets.delete(resource);
+ this.#removeStyleSheetEditor(editor);
+ }
+
+ #getInlineStyleSheetsCount() {
+ return this.editors.filter(editor => !editor.styleSheet.href).length;
+ }
+
+ #getNewStyleSheetsCount() {
+ return this.editors.filter(editor => editor.isNew).length;
+ }
+
+ /**
+ * Finds the index to be shown in the Style Editor for inline or
+ * user-created style sheets, returns undefined if not of either type.
+ *
+ * @param {StyleSheet} styleSheet
+ * Object representing stylesheet
+ * @return {(Number|undefined)}
+ * Optional Integer representing the index of the current stylesheet
+ * among all stylesheets of its type (inline or user-created)
+ */
+ #getNextFriendlyIndex(styleSheet) {
+ if (styleSheet.href) {
+ return undefined;
+ }
+
+ return styleSheet.isNew
+ ? this.#getNewStyleSheetsCount()
+ : this.#getInlineStyleSheetsCount();
+ }
+
+ /**
+ * Add a new editor to the UI for a source.
+ *
+ * @param {Resource} resource
+ * The resource which is received from resource command.
+ * @return {Promise} that is resolved with the created StyleSheetEditor when
+ * the editor is fully initialized or rejected on error.
+ */
+ async #addStyleSheetEditor(resource) {
+ const editor = new StyleSheetEditor(
+ resource,
+ this.#window,
+ this.#getNextFriendlyIndex(resource)
+ );
+
+ editor.on("property-change", this.#summaryChange.bind(this, editor));
+ editor.on("at-rules-changed", this.#updateAtRulesList.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);
+ editor.on(
+ "filter-input-keyboard-shortcut",
+ this.#onFocusFilterInputKeyboardShortcut
+ );
+
+ // onAtRulesChanged fires at-rules-changed, so call the function after
+ // registering the listener in order to ensure to get at-rules-changed event.
+ editor.onAtRulesChanged(resource.atRules);
+
+ this.editors.push(editor);
+
+ try {
+ await editor.fetchSource();
+ } catch (e) {
+ // if the editor was destroyed while fetching dependencies, we don't want to go further.
+ if (!this.editors.includes(editor)) {
+ return null;
+ }
+ throw e;
+ }
+
+ this.#sourceLoaded(editor);
+
+ if (resource.fileName) {
+ this.emit("test:editor-updated", 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(file, parentWindow) {
+ const onFileSelected = selectedFile => {
+ if (!selectedFile) {
+ // nothing selected
+ return;
+ }
+ lazy.NetUtil.asyncFetch(
+ {
+ uri: lazy.NetUtil.newURI(selectedFile),
+ loadingNode: this.#window.document,
+ securityFlags:
+ Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_OTHER,
+ },
+ async (stream, status) => {
+ if (!Components.isSuccessCode(status)) {
+ this.emit("error", { key: LOAD_ERROR, level: "warning" });
+ return;
+ }
+ const source = lazy.NetUtil.readInputStreamToString(
+ stream,
+ stream.available()
+ );
+ stream.close();
+
+ const stylesheetsFront = await this.currentTarget.getFront(
+ "stylesheets"
+ );
+ stylesheetsFront.addStyleSheet(source, selectedFile.path);
+ }
+ );
+ };
+
+ showFilePicker(file, false, parentWindow, onFileSelected);
+ }
+
+ /**
+ * Forward any error from a stylesheet.
+ *
+ * @param {data} data
+ * The event data
+ */
+ #onError = data => {
+ this.emit("error", data);
+ };
+
+ /**
+ * Toggle the original sources pref.
+ */
+ #toggleOrigSources() {
+ const isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
+ Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
+ }
+
+ /**
+ * Toggle the pref for showing the at-rules sidebar (for @media, @layer, @container, …)
+ * in each editor.
+ */
+ #toggleAtRulesSidebar() {
+ const isEnabled = Services.prefs.getBoolPref(PREF_AT_RULES_SIDEBAR);
+ Services.prefs.setBoolPref(PREF_AT_RULES_SIDEBAR, !isEnabled);
+ }
+
+ /**
+ * Toggle the at-rules sidebar in each editor depending on the setting.
+ */
+ #onAtRulesSidebarPrefChanged = () => {
+ this.editors.forEach(this.#updateAtRulesList);
+ };
+
+ /**
+ * This method handles the following cases related to the context
+ * menu items "_openLinkNewTabItem" and "_copyUrlItem":
+ *
+ * 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
+ */
+ #updateContextMenuItems = async () => {
+ this.#openLinkNewTabItem.hidden = !this.#contextMenuStyleSheet;
+ this.#copyUrlItem.hidden = !this.#contextMenuStyleSheet;
+
+ if (this.#contextMenuStyleSheet) {
+ this.#openLinkNewTabItem.setAttribute(
+ "disabled",
+ !this.#contextMenuStyleSheet.href
+ );
+ this.#copyUrlItem.setAttribute(
+ "disabled",
+ !this.#contextMenuStyleSheet.href
+ );
+ }
+ };
+
+ /**
+ * Open a particular stylesheet in a new tab.
+ */
+ #openLinkNewTab = () => {
+ if (this.#contextMenuStyleSheet) {
+ lazy.openContentLink(this.#contextMenuStyleSheet.href);
+ }
+ };
+
+ /**
+ * Copies a stylesheet's URL.
+ */
+ #copyUrl = () => {
+ if (this.#contextMenuStyleSheet) {
+ lazy.copyString(this.#contextMenuStyleSheet.href);
+ }
+ };
+
+ /**
+ * Remove a particular stylesheet editor from the UI
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor to remove.
+ */
+ #removeStyleSheetEditor(editor) {
+ if (editor.summary) {
+ this.removeSplitViewItem(editor.summary);
+ } else {
+ const self = this;
+ this.on("editor-added", function onAdd(added) {
+ if (editor == added) {
+ self.off("editor-added", onAdd);
+ self.removeSplitViewItem(editor.summary);
+ }
+ });
+ }
+
+ editor.destroy();
+ this.editors.splice(this.editors.indexOf(editor), 1);
+ }
+
+ /**
+ * Clear all the editors from the UI.
+ */
+ #clearStyleSheetEditors() {
+ for (const editor of this.editors) {
+ editor.destroy();
+ }
+ this.editors = [];
+ }
+
+ /**
+ * Called when a StyleSheetEditor's source has been fetched.
+ * Add new sidebar item and editor to the UI
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor to create UI for.
+ */
+ #sourceLoaded(editor) {
+ // Create the detail and summary nodes from the templates node (declared in index.xhtml)
+ const details = this.#tplDetails.cloneNode(true);
+ details.id = "";
+ const summary = this.#tplSummary.cloneNode(true);
+ summary.id = "";
+
+ let ordinal = editor.styleSheet.styleSheetIndex;
+ ordinal = ordinal == -1 ? Number.MAX_SAFE_INTEGER : ordinal;
+ summary.style.order = ordinal;
+ summary.setAttribute("data-ordinal", ordinal);
+
+ const isSystem = !!editor.styleSheet.system;
+ if (isSystem) {
+ summary.classList.add("stylesheet-system");
+ }
+
+ this.#nav.appendChild(summary);
+ this.#side.appendChild(details);
+
+ this.#summaryDataMap.set(summary, {
+ details,
+ editor,
+ });
+
+ const createdEditor = editor;
+ createdEditor.summary = summary;
+ createdEditor.details = details;
+
+ const eventListenersConfig = { signal: this.#uiAbortController.signal };
+
+ summary.addEventListener(
+ "click",
+ event => {
+ event.stopPropagation();
+ this.setActiveSummary(summary);
+ },
+ eventListenersConfig
+ );
+
+ const stylesheetToggle = summary.querySelector(".stylesheet-toggle");
+ if (isSystem) {
+ stylesheetToggle.disabled = true;
+ this.#window.document.l10n.setAttributes(
+ stylesheetToggle,
+ "styleeditor-visibility-toggle-system"
+ );
+ } else {
+ stylesheetToggle.addEventListener(
+ "click",
+ event => {
+ event.stopPropagation();
+ event.target.blur();
+
+ createdEditor.toggleDisabled();
+ },
+ eventListenersConfig
+ );
+ }
+
+ summary.querySelector(".stylesheet-name").addEventListener(
+ "keypress",
+ event => {
+ if (event.keyCode == lazy.KeyCodes.DOM_VK_RETURN) {
+ this.setActiveSummary(summary);
+ }
+ },
+ eventListenersConfig
+ );
+
+ summary.querySelector(".stylesheet-saveButton").addEventListener(
+ "click",
+ event => {
+ event.stopPropagation();
+ event.target.blur();
+
+ createdEditor.saveToFile(createdEditor.savedFile);
+ },
+ eventListenersConfig
+ );
+
+ this.#updateSummaryForEditor(createdEditor, summary);
+
+ summary.addEventListener(
+ "contextmenu",
+ () => {
+ this.#contextMenuStyleSheet = createdEditor.styleSheet;
+ },
+ eventListenersConfig
+ );
+
+ summary.addEventListener(
+ "focus",
+ function onSummaryFocus(event) {
+ if (event.target == summary) {
+ // autofocus the stylesheet name
+ summary.querySelector(".stylesheet-name").focus();
+ }
+ },
+ eventListenersConfig
+ );
+
+ const sidebar = details.querySelector(".stylesheet-sidebar");
+ sidebar.style.width = Services.prefs.getIntPref(PREF_SIDEBAR_WIDTH) + "px";
+
+ const splitter = details.querySelector(".devtools-side-splitter");
+ splitter.addEventListener(
+ "mousemove",
+ () => {
+ const sidebarWidth = parseInt(sidebar.style.width, 10);
+ if (!isNaN(sidebarWidth)) {
+ Services.prefs.setIntPref(PREF_SIDEBAR_WIDTH, sidebarWidth);
+
+ // update all at-rules sidebars for consistency
+ const sidebars = [
+ ...this.#panelDoc.querySelectorAll(".stylesheet-sidebar"),
+ ];
+ for (const atRuleSidebar of sidebars) {
+ atRuleSidebar.style.width = sidebarWidth + "px";
+ }
+ }
+ },
+ eventListenersConfig
+ );
+
+ // 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 &&
+ !summary.classList.contains(FILTERED_CLASSNAME)
+ ) {
+ this.#selectEditor(createdEditor);
+ }
+ this.emit("editor-added", createdEditor);
+ }
+
+ /**
+ * Switch to the editor that has been marked to be selected.
+ *
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected.
+ */
+ switchToSelectedSheet() {
+ const toSelect = this.#styleSheetToSelect;
+
+ for (const 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(editor) {
+ const toSelect = this.#styleSheetToSelect;
+ if (!toSelect) {
+ return false;
+ }
+ const 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(editor, line = null, col = null) {
+ // Don't go further if the editor was destroyed in the meantime
+ if (!this.editors.includes(editor)) {
+ return null;
+ }
+
+ const editorPromise = editor.getSourceEditor().then(() => {
+ // line/col are null when the style editor is initialized and the first stylesheet
+ // editor is selected. Unfortunately, this function might be called also when the
+ // panel is opened from clicking on a CSS warning in the WebConsole panel, in which
+ // case we have specific line+col.
+ // There's no guarantee which one could be called first, and it happened that we
+ // were setting the cursor once for the correct line coming from the webconsole,
+ // and then re-setting it to the default value (which was <0,0>).
+ // To avoid the race, we simply don't explicitly set the cursor to any default value,
+ // which is not a big deal as CodeMirror does init it to <0,0> anyway.
+ // See Bug 1738124 for more information.
+ if (line !== null || col !== null) {
+ editor.setCursor(line, col);
+ }
+ this.#styleSheetBoundToSelect = null;
+ });
+
+ const summaryPromise = this.getEditorSummary(editor).then(summary => {
+ // Don't go further if the editor was destroyed in the meantime
+ if (!this.editors.includes(editor)) {
+ throw new Error("Editor was destroyed");
+ }
+ this.setActiveSummary(summary);
+ });
+
+ return Promise.all([editorPromise, summaryPromise]);
+ }
+
+ getEditorSummary(editor) {
+ const self = this;
+
+ if (editor.summary) {
+ return Promise.resolve(editor.summary);
+ }
+
+ return new Promise(resolve => {
+ this.on("editor-added", function onAdd(selected) {
+ if (selected == editor) {
+ self.off("editor-added", onAdd);
+ resolve(editor.summary);
+ }
+ });
+ });
+ }
+
+ getEditorDetails(editor) {
+ const self = this;
+
+ if (editor.details) {
+ return Promise.resolve(editor.details);
+ }
+
+ return new Promise(resolve => {
+ this.on("editor-added", function onAdd(selected) {
+ if (selected == editor) {
+ self.off("editor-added", onAdd);
+ resolve(editor.details);
+ }
+ });
+ });
+ }
+
+ /**
+ * Returns an identifier for the given style sheet.
+ *
+ * @param {StyleSheet} styleSheet
+ * The style sheet to be identified.
+ */
+ getStyleSheetIdentifier(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;
+ }
+
+ /**
+ * Get the OriginalSource object for a given original sourceId returned from
+ * the sourcemap worker service.
+ *
+ * @param {string} sourceId
+ * The ID to search for from the sourcemap worker.
+ *
+ * @return {OriginalSource | null}
+ */
+ getOriginalSourceSheet(sourceId) {
+ for (const editor of this.editors) {
+ const { styleSheet } = editor;
+ if (styleSheet.isOriginalSource && styleSheet.sourceId === sourceId) {
+ return styleSheet;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Given an URL, find a stylesheet resource with that URL, if one has been
+ * loaded into the editor.js
+ *
+ * Do not use this unless you have no other way to get a StyleSheet resource
+ * multiple sheets could share the same URL, so this will give you _one_
+ * of possibly many sheets with that URL.
+ *
+ * @param {string} url
+ * An arbitrary URL to search for.
+ *
+ * @return {StyleSheetResource|null}
+ */
+ getStylesheetResourceForGeneratedURL(url) {
+ for (const styleSheet of this.#seenSheets.keys()) {
+ const sheetURL = styleSheet.href || styleSheet.nodeHref;
+ if (!styleSheet.isOriginalSource && sheetURL === url) {
+ return styleSheet;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * selects a stylesheet and optionally moves the cursor to a selected line
+ *
+ * @param {StyleSheetResource} 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(stylesheet, line, col) {
+ this.#styleSheetToSelect = {
+ stylesheet,
+ line,
+ 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(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(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 = "-";
+ }
+
+ this.#panelDoc.l10n.setArgs(
+ summary.querySelector(".stylesheet-rule-count"),
+ {
+ ruleCount,
+ }
+ );
+
+ summary.classList.toggle("disabled", !!editor.styleSheet.disabled);
+ summary.classList.toggle("unsaved", !!editor.unsaved);
+ summary.classList.toggle("linked-file-error", !!editor.linkedCSSFileError);
+
+ const 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 = PathUtils.filename(editor.linkedCSSFile);
+ } else if (editor.styleSheet.relatedStyleSheet) {
+ // Compute a friendly name for the related generated source
+ // (relatedStyleSheet is set on original CSS to refer to the generated one)
+ linkedCSSSource = shortSource(editor.styleSheet.relatedStyleSheet);
+ try {
+ linkedCSSSource = decodeURI(linkedCSSSource);
+ } catch (e) {}
+ }
+ text(summary, ".stylesheet-linked-file", linkedCSSSource);
+ text(summary, ".stylesheet-title", editor.styleSheet.title || "");
+
+ // We may need to change the summary visibility as a result of the changes.
+ this.handleSummaryVisibility(summary);
+ }
+
+ /**
+ * Update the pretty print button.
+ * The button will be disabled if the selected file is an original file.
+ */
+ #updatePrettyPrintButton() {
+ const disable =
+ !this.selectedEditor || !!this.selectedEditor.styleSheet.isOriginalSource;
+
+ // Only update the button if its state needs it
+ if (disable !== this.#prettyPrintButton.hasAttribute("disabled")) {
+ this.#prettyPrintButton.toggleAttribute("disabled");
+ const l10nString = disable
+ ? "styleeditor-pretty-print-button-disabled"
+ : "styleeditor-pretty-print-button";
+ this.#window.document.l10n.setAttributes(
+ this.#prettyPrintButton,
+ l10nString
+ );
+ }
+ }
+
+ /**
+ * Update the at-rules sidebar for an editor. Hide if there are no rules
+ * Display a list of the at-rules (@media, @layer, @container, …) in the editor's associated style sheet.
+ * Emits a 'at-rules-list-changed' event after updating the UI.
+ *
+ * @param {StyleSheetEditor} editor
+ * Editor to update sidebar of
+ */
+ #updateAtRulesList = editor => {
+ (async function () {
+ const details = await this.getEditorDetails(editor);
+ const list = details.querySelector(".stylesheet-at-rules-list");
+
+ while (list.firstChild) {
+ list.firstChild.remove();
+ }
+
+ const rules = editor.atRules;
+ const showSidebar = Services.prefs.getBoolPref(PREF_AT_RULES_SIDEBAR);
+ const sidebar = details.querySelector(".stylesheet-sidebar");
+
+ let inSource = false;
+
+ for (const rule of rules) {
+ const { line, column } = rule;
+
+ let location = {
+ line,
+ column,
+ source: editor.styleSheet.href,
+ styleSheet: editor.styleSheet,
+ };
+ if (editor.styleSheet.isOriginalSource) {
+ const styleSheet = editor.cssSheet;
+ location = await editor.styleSheet.getOriginalLocation(
+ styleSheet,
+ line,
+ column
+ );
+ }
+
+ // this at-rule is from a different original source
+ if (location.source != editor.styleSheet.href) {
+ continue;
+ }
+ inSource = true;
+
+ const div = this.#panelDoc.createElementNS(HTML_NS, "div");
+ div.classList.add("at-rule-label", rule.type);
+ div.addEventListener(
+ "click",
+ this.#jumpToLocation.bind(this, location)
+ );
+
+ const ruleTextContainer = this.#panelDoc.createElementNS(
+ HTML_NS,
+ "div"
+ );
+ const type = this.#panelDoc.createElementNS(HTML_NS, "span");
+ type.className = "at-rule-type";
+ type.append(this.#panelDoc.createTextNode(`@${rule.type}\u00A0`));
+ if (rule.type == "layer" && rule.layerName) {
+ type.append(this.#panelDoc.createTextNode(`${rule.layerName}\u00A0`));
+ }
+
+ const cond = this.#panelDoc.createElementNS(HTML_NS, "span");
+ cond.className = "at-rule-condition";
+ if (rule.type == "media" && !rule.matches) {
+ cond.classList.add("media-condition-unmatched");
+ }
+ if (this.#commands.descriptorFront.isLocalTab) {
+ this.#setConditionContents(cond, rule.conditionText, rule.type);
+ } else {
+ cond.textContent = rule.conditionText;
+ }
+
+ const link = this.#panelDoc.createElementNS(HTML_NS, "div");
+ link.className = "at-rule-line theme-link";
+ if (location.line != -1) {
+ link.textContent = ":" + location.line;
+ }
+
+ ruleTextContainer.append(type, cond);
+ div.append(ruleTextContainer, link);
+ list.appendChild(div);
+ }
+
+ sidebar.hidden = !showSidebar || !inSource;
+
+ this.emit("at-rules-list-changed", editor);
+ })
+ .bind(this)()
+ .catch(console.error);
+ };
+
+ /**
+ * Set the condition text for the at-rule element.
+ * For media queries, it also injects links to open RDM at a specific size.
+ *
+ * @param {HTMLElement} element
+ * The element corresponding to the media sidebar condition
+ * @param {String} ruleConditionText
+ * The rule conditionText
+ * @param {String} type
+ * The type of the at-rule (e.g. "media", "layer", "supports", …)
+ */
+ #setConditionContents(element, ruleConditionText, type) {
+ if (!ruleConditionText) {
+ return;
+ }
+
+ // For non-media rules, we don't do anything more than displaying the conditionText
+ // as there are no other condition text that would justify opening RDM at a specific
+ // size (e.g. `@container` condition is relative to a container size, which varies
+ // depending the node the rule applies to).
+ if (type !== "media") {
+ const node = this.#panelDoc.createTextNode(ruleConditionText);
+ element.appendChild(node);
+ return;
+ }
+
+ const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/gi;
+
+ let match = minMaxPattern.exec(ruleConditionText);
+ let lastParsed = 0;
+ while (match && match.index != minMaxPattern.lastIndex) {
+ const matchEnd = match.index + match[0].length;
+ const node = this.#panelDoc.createTextNode(
+ ruleConditionText.substring(lastParsed, match.index)
+ );
+ element.appendChild(node);
+
+ const link = this.#panelDoc.createElementNS(HTML_NS, "a");
+ link.href = "#";
+ link.className = "media-responsive-mode-toggle";
+ link.textContent = ruleConditionText.substring(match.index, matchEnd);
+ link.addEventListener("click", this.#onMediaConditionClick.bind(this));
+ element.appendChild(link);
+
+ match = minMaxPattern.exec(ruleConditionText);
+ lastParsed = matchEnd;
+ }
+
+ const node = this.#panelDoc.createTextNode(
+ ruleConditionText.substring(lastParsed, ruleConditionText.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(e) {
+ const conditionText = e.target.textContent;
+ const isWidthCond = conditionText.toLowerCase().indexOf("width") > -1;
+ const mediaVal = parseInt(/\d+/.exec(conditionText), 10);
+
+ const 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.
+ */
+ async #launchResponsiveMode(options = {}) {
+ const tab = this.#commands.descriptorFront.localTab;
+ const win = tab.ownerDocument.defaultView;
+
+ await lazy.ResponsiveUIManager.openIfNeeded(win, tab, {
+ trigger: "style_editor",
+ });
+ this.emit("responsive-mode-opened");
+
+ lazy.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(location) {
+ const source = location.styleSheet || location.source;
+ this.selectStyleSheet(source, location.line - 1, location.column - 1);
+ }
+
+ #startLoadingStyleSheets() {
+ this.#root.classList.add("loading");
+ this.#loadingStyleSheets = [];
+ }
+
+ async #waitForLoadingStyleSheets() {
+ while (this.#loadingStyleSheets?.length > 0) {
+ const pending = this.#loadingStyleSheets;
+ this.#loadingStyleSheets = [];
+ await Promise.all(pending);
+ }
+
+ this.#loadingStyleSheets = null;
+ this.#root.classList.remove("loading");
+ }
+
+ async #handleStyleSheetResource(resource) {
+ try {
+ // The fileName is in resource means this stylesheet was imported from file by user.
+ const { fileName } = resource;
+ let file = fileName ? new lazy.FileUtils.File(fileName) : null;
+
+ // recall location of saved file for this sheet after page reload
+ if (!file) {
+ const identifier = this.getStyleSheetIdentifier(resource);
+ const savedFile = this.savedLocations[identifier];
+ if (savedFile) {
+ file = savedFile;
+ }
+ }
+ resource.file = file;
+
+ await this.#addStyleSheet(resource);
+ } catch (e) {
+ console.error(e);
+ this.emit("error", { key: LOAD_ERROR, level: "warning" });
+ }
+ }
+
+ // onAvailable is a mandatory argument for watchTargets,
+ // but we don't do anything when a new target gets created.
+ #onTargetAvailable = ({ targetFront }) => {};
+
+ #onTargetDestroyed = ({ targetFront }) => {
+ // Iterate over a copy of the list in order to prevent skipping
+ // over some items when removing items of this list
+ const editorsCopy = [...this.editors];
+ for (const editor of editorsCopy) {
+ const { styleSheet } = editor;
+ if (styleSheet.targetFront == targetFront) {
+ this.#removeStyleSheet(styleSheet, editor);
+ }
+ }
+ };
+
+ #onResourceAvailable = async resources => {
+ const promises = [];
+ for (const resource of resources) {
+ if (
+ resource.resourceType === this.#toolbox.resourceCommand.TYPES.STYLESHEET
+ ) {
+ const onStyleSheetHandled = this.#handleStyleSheetResource(resource);
+
+ if (this.#loadingStyleSheets) {
+ // In case of reloading/navigating and panel's opening
+ this.#loadingStyleSheets.push(onStyleSheetHandled);
+ }
+ promises.push(onStyleSheetHandled);
+ continue;
+ }
+
+ if (!resource.targetFront.isTopLevel) {
+ continue;
+ }
+
+ if (resource.name === "will-navigate") {
+ this.#startLoadingStyleSheets();
+ this.#clear();
+ } else if (resource.name === "dom-complete") {
+ promises.push(this.#waitForLoadingStyleSheets());
+ }
+ }
+ await Promise.all(promises);
+ };
+
+ #onResourceUpdated = async updates => {
+ for (const { resource, update } of updates) {
+ if (
+ update.resourceType === this.#toolbox.resourceCommand.TYPES.STYLESHEET
+ ) {
+ const editor = this.editors.find(
+ e => e.resourceId === update.resourceId
+ );
+
+ switch (update.updateType) {
+ case "style-applied": {
+ editor.onStyleApplied(update);
+ break;
+ }
+ case "property-change": {
+ for (const [property, value] of Object.entries(
+ update.resourceUpdates
+ )) {
+ editor.onPropertyChange(property, value);
+ }
+ break;
+ }
+ case "at-rules-changed":
+ case "matches-change": {
+ editor.onAtRulesChanged(resource.atRules);
+ break;
+ }
+ }
+ }
+ }
+ };
+
+ #onResourceDestroyed = resources => {
+ for (const resource of resources) {
+ if (
+ resource.resourceType !== this.#toolbox.resourceCommand.TYPES.STYLESHEET
+ ) {
+ continue;
+ }
+
+ const editorToRemove = this.editors.find(
+ editor => editor.styleSheet.resourceId == resource.resourceId
+ );
+
+ if (editorToRemove) {
+ const { styleSheet } = editorToRemove;
+ this.#removeStyleSheet(styleSheet, editorToRemove);
+ }
+ }
+ };
+
+ /**
+ * Set the active item's summary element.
+ *
+ * @param DOMElement summary
+ * @param {Object} options
+ * @param {String=} options.reason: Indicates why the summary was selected. It's set to
+ * "filter-auto" when the summary was automatically selected as the result
+ * of the previous active summary being filtered out.
+ */
+ setActiveSummary(summary, options = {}) {
+ if (summary == this.#activeSummary) {
+ return;
+ }
+
+ if (this.#activeSummary) {
+ const binding = this.#summaryDataMap.get(this.#activeSummary);
+
+ this.#activeSummary.classList.remove("splitview-active");
+ binding.details.classList.remove("splitview-active");
+ }
+
+ this.#activeSummary = summary;
+ if (!summary) {
+ this.selectedEditor = null;
+ return;
+ }
+
+ const { details } = this.#summaryDataMap.get(summary);
+ summary.classList.add("splitview-active");
+ details.classList.add("splitview-active");
+
+ this.showSummaryEditor(summary, options);
+ }
+
+ /**
+ * Show summary's associated editor
+ *
+ * @param DOMElement summary
+ * @param {Object} options
+ * @param {String=} options.reason: Indicates why the summary was selected. It's set to
+ * "filter-auto" when the summary was automatically selected as the result
+ * of the previous active summary being filtered out.
+ */
+ async showSummaryEditor(summary, options) {
+ const { details, editor } = this.#summaryDataMap.get(summary);
+ this.selectedEditor = editor;
+
+ try {
+ if (!editor.sourceEditor) {
+ // only initialize source editor when we switch to this view
+ const inputElement = details.querySelector(".stylesheet-editor-input");
+ await editor.load(inputElement, this.#cssProperties);
+ }
+
+ editor.onShow(options);
+
+ this.#updatePrettyPrintButton();
+
+ this.emit("editor-selected", editor);
+ } catch (e) {
+ console.error(e);
+ }
+ }
+
+ /**
+ * Remove an item from the split view.
+ *
+ * @param DOMElement summary
+ * Summary element of the item to remove.
+ */
+ removeSplitViewItem(summary) {
+ if (summary == this.#activeSummary) {
+ this.setActiveSummary(null);
+ }
+
+ const data = this.#summaryDataMap.get(summary);
+ if (!data) {
+ return;
+ }
+
+ summary.remove();
+ data.details.remove();
+ }
+
+ /**
+ * Make the passed element visible or not, depending if it matches the current filter
+ *
+ * @param {Element} summary
+ * @param {Object} options
+ * @param {Boolean} options.triggerOnFilterStateChange: Set to false to avoid calling
+ * #onFilterStateChange directly here. This can be useful when this
+ * function is called for every item of the list, like in `setFilter`.
+ */
+ handleSummaryVisibility(summary, { triggerOnFilterStateChange = true } = {}) {
+ if (!this.#filter) {
+ summary.classList.remove(FILTERED_CLASSNAME);
+ return;
+ }
+
+ const label = summary.querySelector(".stylesheet-name label");
+ const itemText = label.value.toLowerCase();
+ const matchesSearch = itemText.includes(this.#filter.toLowerCase());
+ summary.classList.toggle(FILTERED_CLASSNAME, !matchesSearch);
+
+ if (this.#activeSummary == summary && !matchesSearch) {
+ this.setActiveSummary(null);
+ }
+
+ if (triggerOnFilterStateChange) {
+ this.#onFilterStateChange();
+ }
+ }
+
+ destroy() {
+ this.#toolbox.resourceCommand.unwatchResources(
+ [
+ this.#toolbox.resourceCommand.TYPES.DOCUMENT_EVENT,
+ this.#toolbox.resourceCommand.TYPES.STYLESHEET,
+ ],
+ {
+ onAvailable: this.#onResourceAvailable,
+ onUpdated: this.#onResourceUpdated,
+ onDestroyed: this.#onResourceDestroyed,
+ }
+ );
+ this.#commands.targetCommand.unwatchTargets({
+ types: [this.#commands.targetCommand.TYPES.FRAME],
+ onAvailable: this.#onTargetAvailable,
+ onDestroyed: this.#onTargetDestroyed,
+ });
+
+ if (this.#uiAbortController) {
+ this.#uiAbortController.abort();
+ this.#uiAbortController = null;
+ }
+ this.#clearStyleSheetEditors();
+
+ this.#seenSheets = null;
+ this.#filterInput = null;
+ this.#filterInputClearButton = null;
+ this.#nav = null;
+ this.#prettyPrintButton = null;
+ this.#side = null;
+ this.#tplDetails = null;
+ this.#tplSummary = null;
+
+ const sidebar = this.#panelDoc.querySelector(".splitview-controller");
+ const sidebarWidth = parseInt(sidebar.style.width, 10);
+ if (!isNaN(sidebarWidth)) {
+ Services.prefs.setIntPref(PREF_NAV_WIDTH, sidebarWidth);
+ }
+
+ if (this.#sourceMapPrefObserver) {
+ this.#sourceMapPrefObserver.off(
+ PREF_ORIG_SOURCES,
+ this.#onOrigSourcesPrefChanged
+ );
+ this.#sourceMapPrefObserver.destroy();
+ this.#sourceMapPrefObserver = null;
+ }
+
+ if (this.#prefObserver) {
+ this.#prefObserver.off(
+ PREF_AT_RULES_SIDEBAR,
+ this.#onAtRulesSidebarPrefChanged
+ );
+ this.#prefObserver.destroy();
+ this.#prefObserver = null;
+ }
+
+ if (this.#shortcuts) {
+ this.#shortcuts.destroy();
+ this.#shortcuts = null;
+ }
+ }
+}
diff --git a/devtools/client/styleeditor/StyleEditorUtil.sys.mjs b/devtools/client/styleeditor/StyleEditorUtil.sys.mjs
new file mode 100644
index 0000000000..739dc55fc5
--- /dev/null
+++ b/devtools/client/styleeditor/StyleEditorUtil.sys.mjs
@@ -0,0 +1,213 @@
+/* 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/. */
+
+/* All top-level definitions here are exports. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+const PROPERTIES_URL = "chrome://devtools/locale/styleeditor.properties";
+
+import { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
+
+const gStringBundle = Services.strings.createBundle(PROPERTIES_URL);
+
+const lazy = {};
+
+loader.lazyRequireGetter(
+ lazy,
+ "Menu",
+ "resource://devtools/client/framework/menu.js"
+);
+loader.lazyRequireGetter(
+ lazy,
+ "MenuItem",
+ "resource://devtools/client/framework/menu-item.js"
+);
+
+const PREF_AT_RULES_SIDEBAR = "devtools.styleeditor.showAtRulesSidebar";
+const PREF_ORIG_SOURCES = "devtools.source-map.client-service.enabled";
+
+/**
+ * Returns a localized string with the given key name from the string bundle.
+ *
+ * @param name
+ * @param ...rest
+ * Optional arguments to format in the string.
+ * @return string
+ */
+export function getString(name) {
+ try {
+ if (arguments.length == 1) {
+ return gStringBundle.GetStringFromName(name);
+ }
+ const rest = Array.prototype.slice.call(arguments, 1);
+ return gStringBundle.formatStringFromName(name, rest);
+ } catch (ex) {
+ console.error(ex);
+ throw new Error(
+ "L10N error. '" + name + "' is missing from " + PROPERTIES_URL
+ );
+ }
+}
+
+/**
+ * Assert an expression is true or throw if false.
+ *
+ * @param expression
+ * @param message
+ * Optional message.
+ * @return expression
+ */
+export function assert(expression, message) {
+ if (!expression) {
+ const msg = message ? "ASSERTION FAILURE:" + message : "ASSERTION FAILURE";
+ log(msg);
+ throw new Error(msg);
+ }
+ return expression;
+}
+
+/**
+ * Retrieve or set the text content of an element.
+ *
+ * @param DOMElement root
+ * The element to use for querySelector.
+ * @param string selector
+ * Selector string for the element to get/set the text content.
+ * @param string textContent
+ * Optional text to set.
+ * @return string
+ * Text content of matching element or null if there were no element
+ * matching selector.
+ */
+export function text(root, selector, textContent) {
+ const element = root.querySelector(selector);
+ if (!element) {
+ return null;
+ }
+
+ if (textContent === undefined) {
+ return element.textContent;
+ }
+ element.textContent = textContent;
+ return textContent;
+}
+
+/**
+ * Log a message to the console.
+ *
+ * @param ...rest
+ * One or multiple arguments to log.
+ * If multiple arguments are given, they will be joined by " "
+ * in the log.
+ */
+export function log() {
+ console.logStringMessage(Array.prototype.slice.call(arguments).join(" "));
+}
+
+/**
+ * Show file picker and return the file user selected.
+ *
+ * @param mixed file
+ * Optional nsIFile or string representing the filename to auto-select.
+ * @param boolean toSave
+ * If true, the user is selecting a filename to save.
+ * @param nsIWindow parentWindow
+ * Optional parent window. If null the parent window of the file picker
+ * will be the window of the attached input element.
+ * @param callback
+ * The callback method, which will be called passing in the selected
+ * file or null if the user did not pick one.
+ * @param AString suggestedFilename
+ * The suggested filename when toSave is true.
+ */
+export function showFilePicker(
+ path,
+ toSave,
+ parentWindow,
+ callback,
+ suggestedFilename
+) {
+ if (typeof path == "string") {
+ try {
+ if (Services.io.extractScheme(path) == "file") {
+ const uri = Services.io.newURI(path);
+ const file = uri.QueryInterface(Ci.nsIFileURL).file;
+ callback(file);
+ return;
+ }
+ } catch (ex) {
+ callback(null);
+ return;
+ }
+ try {
+ const file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(path);
+ callback(file);
+ return;
+ } catch (ex) {
+ callback(null);
+ return;
+ }
+ }
+ if (path) {
+ // "path" is an nsIFile
+ callback(path);
+ return;
+ }
+
+ const fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ const mode = toSave ? fp.modeSave : fp.modeOpen;
+ const key = toSave ? "saveStyleSheet" : "importStyleSheet";
+ const fpCallback = function (result) {
+ if (result == Ci.nsIFilePicker.returnCancel) {
+ callback(null);
+ } else {
+ callback(fp.file);
+ }
+ };
+
+ if (toSave && suggestedFilename) {
+ fp.defaultString = suggestedFilename;
+ }
+
+ fp.init(parentWindow, getString(key + ".title"), mode);
+ fp.appendFilter(getString(key + ".filter"), "*.css");
+ fp.appendFilters(fp.filterAll);
+ fp.open(fpCallback);
+}
+
+/**
+ * Returns a Popup Menu for the Options ("gear") Button
+ * @param {function} toggleOrigSources
+ * To toggle the original source pref
+ * @param {function} toggleAtRulesSidebar
+ * To toggle the pref to show at-rules side bar
+ * @return {object} popupMenu
+ * A Menu object holding the MenuItems
+ */
+export function optionsPopupMenu(toggleOrigSources, toggleAtRulesSidebar) {
+ const popupMenu = new lazy.Menu();
+ popupMenu.append(
+ new lazy.MenuItem({
+ id: "options-origsources",
+ label: getString("showOriginalSources.label"),
+ accesskey: getString("showOriginalSources.accesskey"),
+ type: "checkbox",
+ checked: Services.prefs.getBoolPref(PREF_ORIG_SOURCES),
+ click: () => toggleOrigSources(),
+ })
+ );
+ popupMenu.append(
+ new lazy.MenuItem({
+ id: "options-show-at-rules",
+ label: getString("showAtRulesSidebar.label"),
+ accesskey: getString("showAtRulesSidebar.accesskey"),
+ type: "checkbox",
+ checked: Services.prefs.getBoolPref(PREF_AT_RULES_SIDEBAR),
+ click: () => toggleAtRulesSidebar(),
+ })
+ );
+
+ return popupMenu;
+}
diff --git a/devtools/client/styleeditor/StyleSheetEditor.sys.mjs b/devtools/client/styleeditor/StyleSheetEditor.sys.mjs
new file mode 100644
index 0000000000..d84854a0ae
--- /dev/null
+++ b/devtools/client/styleeditor/StyleSheetEditor.sys.mjs
@@ -0,0 +1,1052 @@
+/* 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/. */
+
+import {
+ require,
+ loader,
+} from "resource://devtools/shared/loader/Loader.sys.mjs";
+
+const Editor = require("resource://devtools/client/shared/sourceeditor/editor.js");
+const {
+ shortSource,
+ prettifyCSS,
+} = require("resource://devtools/shared/inspector/css-logic.js");
+const { throttle } = require("resource://devtools/shared/throttle.js");
+const EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+const lazy = {};
+
+loader.lazyGetter(lazy, "BufferStream", () => {
+ return Components.Constructor(
+ "@mozilla.org/io/arraybuffer-input-stream;1",
+ "nsIArrayBufferInputStream",
+ "setData"
+ );
+});
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+});
+
+import {
+ getString,
+ showFilePicker,
+} from "resource://devtools/client/styleeditor/StyleEditorUtil.sys.mjs";
+
+const LOAD_ERROR = "error-load";
+const SAVE_ERROR = "error-save";
+const SELECTOR_HIGHLIGHTER_TYPE = "SelectorHighlighter";
+
+// max update frequency in ms (avoid potential typing lag and/or flicker)
+// @see StyleEditor.updateStylesheet
+const UPDATE_STYLESHEET_DELAY = 500;
+
+// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
+const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
+
+// Pref which decides whether updates to the stylesheet use transitions
+const TRANSITION_PREF = "devtools.styleeditor.transitions";
+
+// How long to wait to update linked CSS file after original source was saved
+// to disk. Time in ms.
+const CHECK_LINKED_SHEET_DELAY = 500;
+
+// How many times to check for linked file changes
+const MAX_CHECK_COUNT = 10;
+
+// How much time should the mouse be still before the selector at that position
+// gets highlighted?
+const SELECTOR_HIGHLIGHT_TIMEOUT = 500;
+
+// Minimum delay between firing two at-rules-changed events.
+const EMIT_AT_RULES_THROTTLING = 500;
+
+const STYLE_SHEET_UPDATE_CAUSED_BY_STYLE_EDITOR = "styleeditor";
+
+/**
+ * StyleSheetEditor controls the editor linked to a particular StyleSheet
+ * object.
+ *
+ * Emits events:
+ * 'property-change': A property on the underlying stylesheet has changed
+ * 'source-editor-load': The source editor for this editor has been loaded
+ * 'error': An error has occured
+ *
+ * @param {Resource} resource
+ * The STYLESHEET resource which is received from resource command.
+ * @param {DOMWindow} win
+ * panel window for style editor
+ * @param {Number} styleSheetFriendlyIndex
+ * Optional Integer representing the index of the current stylesheet
+ * among all stylesheets of its type (inline or user-created)
+ */
+export function StyleSheetEditor(resource, win, styleSheetFriendlyIndex) {
+ EventEmitter.decorate(this);
+
+ this._resource = resource;
+ this._inputElement = null;
+ this.sourceEditor = null;
+ this._window = win;
+ this._isNew = this.styleSheet.isNew;
+ this.styleSheetFriendlyIndex = styleSheetFriendlyIndex;
+
+ // True when we've just set the editor text based on a style-applied
+ // event from the StyleSheetActor.
+ this._justSetText = false;
+
+ // state to use when inputElement attaches
+ this._state = {
+ text: "",
+ selection: {
+ start: { line: 0, ch: 0 },
+ end: { line: 0, ch: 0 },
+ },
+ };
+
+ this._styleSheetFilePath = null;
+ if (
+ this.styleSheet.href &&
+ Services.io.extractScheme(this.styleSheet.href) == "file"
+ ) {
+ this._styleSheetFilePath = this.styleSheet.href;
+ }
+
+ this.onPropertyChange = this.onPropertyChange.bind(this);
+ this.onAtRulesChanged = this.onAtRulesChanged.bind(this);
+ this.checkLinkedFileForChanges = this.checkLinkedFileForChanges.bind(this);
+ this.markLinkedFileBroken = this.markLinkedFileBroken.bind(this);
+ this.saveToFile = this.saveToFile.bind(this);
+ this.updateStyleSheet = this.updateStyleSheet.bind(this);
+ this._updateStyleSheet = this._updateStyleSheet.bind(this);
+ this._onMouseMove = this._onMouseMove.bind(this);
+
+ this._focusOnSourceEditorReady = false;
+ this.savedFile = this.styleSheet.file;
+ this.linkCSSFile();
+
+ this.emitAtRulesChanged = throttle(
+ this.emitAtRulesChanged,
+ EMIT_AT_RULES_THROTTLING,
+ this
+ );
+
+ this.atRules = [];
+}
+
+StyleSheetEditor.prototype = {
+ get resourceId() {
+ return this._resource.resourceId;
+ },
+
+ get styleSheet() {
+ return this._resource;
+ },
+
+ /**
+ * Whether there are unsaved changes in the editor
+ */
+ get unsaved() {
+ return this.sourceEditor && !this.sourceEditor.isClean();
+ },
+
+ /**
+ * Whether the editor is for a stylesheet created by the user
+ * through the style editor UI.
+ */
+ get isNew() {
+ return this._isNew;
+ },
+
+ /**
+ * The style sheet or the generated style sheet for this source if it's an
+ * original source.
+ */
+ get cssSheet() {
+ if (this.styleSheet.isOriginalSource) {
+ return this.styleSheet.relatedStyleSheet;
+ }
+ return this.styleSheet;
+ },
+
+ get savedFile() {
+ return this._savedFile;
+ },
+
+ set savedFile(name) {
+ this._savedFile = name;
+
+ this.linkCSSFile();
+ },
+
+ /**
+ * Get a user-friendly name for the style sheet.
+ *
+ * @return string
+ */
+ get friendlyName() {
+ if (this.savedFile) {
+ return this.savedFile.leafName;
+ }
+
+ if (this._isNew) {
+ const index = this.styleSheetFriendlyIndex + 1 || 0;
+ return getString("newStyleSheet", index);
+ }
+
+ if (!this.styleSheet.href) {
+ // TODO(bug 1809107): Probably a different index + string for
+ // constructable stylesheets, they can't be meaningfully edited right now
+ // because we don't have their original text.
+ const index = this.styleSheetFriendlyIndex + 1 || 0;
+ return getString("inlineStyleSheet", index);
+ }
+
+ if (!this._friendlyName) {
+ this._friendlyName = shortSource(this.styleSheet);
+ try {
+ this._friendlyName = decodeURI(this._friendlyName);
+ } catch (ex) {
+ // Ignore.
+ }
+ }
+ return this._friendlyName;
+ },
+
+ /**
+ * Check if transitions are enabled for style changes.
+ *
+ * @return Boolean
+ */
+ get transitionsEnabled() {
+ return Services.prefs.getBoolPref(TRANSITION_PREF);
+ },
+
+ /**
+ * If this is an original source, get the path of the CSS file it generated.
+ */
+ linkCSSFile() {
+ if (!this.styleSheet.isOriginalSource) {
+ return;
+ }
+
+ const relatedSheet = this.styleSheet.relatedStyleSheet;
+ if (!relatedSheet || !relatedSheet.href) {
+ return;
+ }
+
+ let path;
+ const href = removeQuery(relatedSheet.href);
+ const uri = lazy.NetUtil.newURI(href);
+
+ if (uri.scheme == "file") {
+ const file = uri.QueryInterface(Ci.nsIFileURL).file;
+ path = file.path;
+ } else if (this.savedFile) {
+ const origHref = removeQuery(this.styleSheet.href);
+ const origUri = lazy.NetUtil.newURI(origHref);
+ path = findLinkedFilePath(uri, origUri, this.savedFile);
+ } else {
+ // we can't determine path to generated file on disk
+ return;
+ }
+
+ if (this.linkedCSSFile == path) {
+ return;
+ }
+
+ this.linkedCSSFile = path;
+
+ this.linkedCSSFileError = null;
+
+ // save last file change time so we can compare when we check for changes.
+ IOUtils.stat(path).then(info => {
+ this._fileModDate = info.lastModified;
+ }, this.markLinkedFileBroken);
+
+ this.emit("linked-css-file");
+ },
+
+ /**
+ * A helper function that fetches the source text from the style
+ * sheet.
+ *
+ * This will set |this._state.text| to the new text.
+ */
+ async _fetchSourceText(options = {}) {
+ const styleSheetsFront = await this._getStyleSheetsFront();
+
+ let longStr = null;
+ if (this.styleSheet.isOriginalSource) {
+ // If the stylesheet is OriginalSource, we should get the texts from SourceMapLoader.
+ // So, for now, we use OriginalSource.getText() as it is.
+ longStr = await this.styleSheet.getText();
+ } else {
+ longStr = await styleSheetsFront.getText(this.resourceId);
+ }
+
+ this._state.text = await longStr.string();
+ },
+
+ prettifySourceText() {
+ this._prettifySourceTextIfNeeded(/* force */ true);
+ },
+
+ /**
+ * Attempt to prettify the current text if the corresponding stylesheet is not
+ * an original source. The text will be read from |this._state.text|.
+ *
+ * This will set |this._state.text| to the prettified text if needed.
+ *
+ * @param {Boolean} force: Set to true to prettify the stylesheet, no matter if it's
+ * minified or not.
+ */
+ _prettifySourceTextIfNeeded(force = false) {
+ if (this.styleSheet.isOriginalSource) {
+ return;
+ }
+
+ const { result, mappings } = prettifyCSS(
+ this._state.text,
+ // prettifyCSS will always prettify the passed text if we pass a `null` ruleCount.
+ force ? null : this.styleSheet.ruleCount
+ );
+
+ // Store the list of objects with mappings between CSS token positions from the
+ // original source to the prettified source. These will be used when requested to
+ // jump to a specific position within the editor.
+ this._mappings = mappings;
+ this._state.text = result;
+
+ if (force && this.sourceEditor) {
+ this.sourceEditor.setText(result);
+ }
+ },
+
+ /**
+ * Start fetching the full text source for this editor's sheet.
+ */
+ async fetchSource() {
+ try {
+ await this._fetchSourceText();
+ this.sourceLoaded = true;
+ } catch (e) {
+ if (this._isDestroyed) {
+ console.warn(
+ `Could not fetch the source for ${this.styleSheet.href}, the editor was destroyed`
+ );
+ console.error(e);
+ } else {
+ console.error(e);
+ this.emit("error", {
+ key: LOAD_ERROR,
+ append: this.styleSheet.href,
+ level: "warning",
+ });
+ throw e;
+ }
+ }
+ },
+
+ /**
+ * Set the cursor at the given line and column location within the code editor.
+ *
+ * @param {Number} line
+ * @param {Number} column
+ */
+ setCursor(line, column) {
+ line = line || 0;
+ column = column || 0;
+
+ const position = this.translateCursorPosition(line, column);
+ this.sourceEditor.setCursor({ line: position.line, ch: position.column });
+ },
+
+ /**
+ * If the stylesheet was automatically prettified, there should be a list of line
+ * and column mappings from the original to the generated source that can be used
+ * to translate the cursor position to the correct location in the prettified source.
+ * If no mappings exist, return the original cursor position unchanged.
+ *
+ * @param {Number} line
+ * @param {Numer} column
+ *
+ * @return {Object}
+ */
+ translateCursorPosition(line, column) {
+ if (Array.isArray(this._mappings)) {
+ for (const mapping of this._mappings) {
+ if (
+ mapping.original.line === line &&
+ mapping.original.column === column
+ ) {
+ line = mapping.generated.line;
+ column = mapping.generated.column;
+ continue;
+ }
+ }
+ }
+
+ return { line, column };
+ },
+
+ /**
+ * Forward property-change event from stylesheet.
+ *
+ * @param {string} event
+ * Event type
+ * @param {string} property
+ * Property that has changed on sheet
+ */
+ onPropertyChange(property, value) {
+ this.emit("property-change", property, value);
+ },
+
+ /**
+ * Called when the stylesheet text changes.
+ * @param {Object} update: The stylesheet resource update packet.
+ */
+ async onStyleApplied(update) {
+ const updateIsFromSyleSheetEditor =
+ update?.event?.cause === STYLE_SHEET_UPDATE_CAUSED_BY_STYLE_EDITOR;
+
+ if (updateIsFromSyleSheetEditor) {
+ // We just applied an edit in the editor, so we can drop this notification.
+ this.emit("style-applied");
+ return;
+ }
+
+ if (this.sourceEditor) {
+ await this._fetchSourceText();
+
+ // sourceEditor is already loaded, so we can prettify immediately.
+ this._prettifySourceTextIfNeeded();
+
+ // The updated stylesheet text should have been set in this._state.text by _fetchSourceText.
+ const sourceText = this._state.text;
+
+ this._justSetText = true;
+ const firstLine = this.sourceEditor.getFirstVisibleLine();
+ const pos = this.sourceEditor.getCursor();
+ this.sourceEditor.setText(sourceText);
+ this.sourceEditor.setFirstVisibleLine(firstLine);
+ this.sourceEditor.setCursor(pos);
+ this.emit("style-applied");
+ }
+ },
+
+ /**
+ * Handles changes to the list of at-rules (@media, @layer, @container, …) in the stylesheet.
+ * Emits 'at-rules-changed' if the list has changed.
+ *
+ * @param {array} rules
+ * Array of MediaRuleFronts for new media rules of sheet.
+ */
+ onAtRulesChanged(rules) {
+ if (!rules.length && !this.atRules.length) {
+ return;
+ }
+
+ this.atRules = rules;
+ this.emitAtRulesChanged();
+ },
+
+ /**
+ * Forward at-rules-changed event from stylesheet.
+ */
+ emitAtRulesChanged() {
+ this.emit("at-rules-changed", this.atRules);
+ },
+
+ /**
+ * Create source editor and load state into it.
+ * @param {DOMElement} inputElement
+ * Element to load source editor in
+ * @param {CssProperties} cssProperties
+ * A css properties database.
+ *
+ * @return {Promise}
+ * Promise that will resolve when the style editor is loaded.
+ */
+ async load(inputElement, cssProperties) {
+ if (this._isDestroyed) {
+ throw new Error(
+ "Won't load source editor as the style sheet has " +
+ "already been removed from Style Editor."
+ );
+ }
+
+ this._inputElement = inputElement;
+
+ // Attempt to prettify the source before loading the source editor.
+ this._prettifySourceTextIfNeeded();
+
+ const walker = await this.getWalker();
+ const config = {
+ value: this._state.text,
+ lineNumbers: true,
+ mode: Editor.modes.css,
+ readOnly: false,
+ autoCloseBrackets: "{}()",
+ extraKeys: this._getKeyBindings(),
+ contextMenu: "sourceEditorContextMenu",
+ autocomplete: Services.prefs.getBoolPref(AUTOCOMPLETION_PREF),
+ autocompleteOpts: { walker, cssProperties },
+ cssProperties,
+ };
+ const sourceEditor = (this._sourceEditor = new Editor(config));
+
+ sourceEditor.on("dirty-change", this.onPropertyChange);
+
+ await sourceEditor.appendTo(inputElement);
+
+ sourceEditor.on("saveRequested", this.saveToFile);
+
+ if (!this.styleSheet.isOriginalSource) {
+ sourceEditor.on("change", this.updateStyleSheet);
+ }
+
+ this.sourceEditor = sourceEditor;
+
+ if (this._focusOnSourceEditorReady) {
+ this._focusOnSourceEditorReady = false;
+ sourceEditor.focus();
+ }
+
+ sourceEditor.setSelection(
+ this._state.selection.start,
+ this._state.selection.end
+ );
+
+ const highlighter = await this.getHighlighter();
+ if (highlighter && walker && sourceEditor.container?.contentWindow) {
+ sourceEditor.container.contentWindow.addEventListener(
+ "mousemove",
+ this._onMouseMove
+ );
+ }
+
+ // Add the commands controller for the source-editor.
+ sourceEditor.insertCommandsController();
+
+ this.emit("source-editor-load");
+ },
+
+ /**
+ * Get the source editor for this editor.
+ *
+ * @return {Promise}
+ * Promise that will resolve with the editor.
+ */
+ getSourceEditor() {
+ const self = this;
+
+ if (this.sourceEditor) {
+ return Promise.resolve(this);
+ }
+
+ return new Promise(resolve => {
+ this.on("source-editor-load", () => {
+ resolve(self);
+ });
+ });
+ },
+
+ /**
+ * Focus the Style Editor input.
+ */
+ focus() {
+ if (this.sourceEditor) {
+ this.sourceEditor.focus();
+ } else {
+ this._focusOnSourceEditorReady = true;
+ }
+ },
+
+ /**
+ * Event handler for when the editor is shown.
+ *
+ * @param {Object} options
+ * @param {String} options.reason: Indicates why the editor is shown
+ */
+ onShow(options = {}) {
+ if (this.sourceEditor) {
+ // CodeMirror needs refresh to restore scroll position after hiding and
+ // showing the editor.
+ this.sourceEditor.refresh();
+ }
+
+ // We don't want to focus the editor if it was shown because of the list being filtered,
+ // as the user might still be typing in the filter input.
+ if (options.reason !== "filter-auto") {
+ this.focus();
+ }
+ },
+
+ /**
+ * Toggled the disabled state of the underlying stylesheet.
+ */
+ async toggleDisabled() {
+ const styleSheetsFront = await this._getStyleSheetsFront();
+ styleSheetsFront.toggleDisabled(this.resourceId).catch(console.error);
+ },
+
+ /**
+ * Queue a throttled task to update the live style sheet.
+ */
+ updateStyleSheet() {
+ if (this._updateTask) {
+ // cancel previous queued task not executed within throttle delay
+ this._window.clearTimeout(this._updateTask);
+ }
+
+ this._updateTask = this._window.setTimeout(
+ this._updateStyleSheet,
+ UPDATE_STYLESHEET_DELAY
+ );
+ },
+
+ /**
+ * Update live style sheet according to modifications.
+ */
+ async _updateStyleSheet() {
+ if (this.styleSheet.disabled) {
+ // TODO: do we want to do this?
+ return;
+ }
+
+ if (this._justSetText) {
+ this._justSetText = false;
+ return;
+ }
+
+ // reset only if we actually perform an update
+ // (stylesheet is enabled) so that 'missed' updates
+ // while the stylesheet is disabled can be performed
+ // when it is enabled back. @see enableStylesheet
+ this._updateTask = null;
+
+ if (this.sourceEditor) {
+ this._state.text = this.sourceEditor.getText();
+ }
+
+ try {
+ const styleSheetsFront = await this._getStyleSheetsFront();
+ await styleSheetsFront.update(
+ this.resourceId,
+ this._state.text,
+ this.transitionsEnabled,
+ STYLE_SHEET_UPDATE_CAUSED_BY_STYLE_EDITOR
+ );
+
+ // Clear any existing mappings from automatic CSS prettification
+ // because they were likely invalided by manually editing the stylesheet.
+ this._mappings = null;
+ } catch (e) {
+ console.error(e);
+ }
+ },
+
+ /**
+ * Handle mousemove events, calling _highlightSelectorAt after a delay only
+ * and reseting the delay everytime.
+ */
+ _onMouseMove(e) {
+ // As we only want to hide an existing highlighter, we can use this.highlighter directly
+ // (and not this.getHighlighter).
+ if (this.highlighter) {
+ this.highlighter.hide();
+ }
+
+ if (this.mouseMoveTimeout) {
+ this._window.clearTimeout(this.mouseMoveTimeout);
+ this.mouseMoveTimeout = null;
+ }
+
+ this.mouseMoveTimeout = this._window.setTimeout(() => {
+ this._highlightSelectorAt(e.clientX, e.clientY);
+ }, SELECTOR_HIGHLIGHT_TIMEOUT);
+ },
+
+ /**
+ * Highlight nodes matching the selector found at coordinates x,y in the
+ * editor, if any.
+ *
+ * @param {Number} x
+ * @param {Number} y
+ */
+ async _highlightSelectorAt(x, y) {
+ const pos = this.sourceEditor.getPositionFromCoords({ left: x, top: y });
+ const info = this.sourceEditor.getInfoAt(pos);
+ if (!info || info.state !== "selector") {
+ return;
+ }
+
+ const onGetHighlighter = this.getHighlighter();
+ const walker = await this.getWalker();
+ const node = await walker.getStyleSheetOwnerNode(this.resourceId);
+
+ const highlighter = await onGetHighlighter;
+ await highlighter.show(node, {
+ selector: info.selector,
+ hideInfoBar: true,
+ showOnly: "border",
+ region: "border",
+ });
+
+ this.emit("node-highlighted");
+ },
+
+ /**
+ * Returns the walker front associated with this._resource target.
+ *
+ * @returns {Promise<WalkerFront>}
+ */
+ async getWalker() {
+ if (this.walker) {
+ return this.walker;
+ }
+
+ const { targetFront } = this._resource;
+ const inspectorFront = await targetFront.getFront("inspector");
+ this.walker = inspectorFront.walker;
+ return this.walker;
+ },
+
+ /**
+ * Returns or creates the selector highlighter associated with this._resource target.
+ *
+ * @returns {CustomHighlighterFront|null}
+ */
+ async getHighlighter() {
+ if (this.highlighter) {
+ return this.highlighter;
+ }
+
+ const walker = await this.getWalker();
+ try {
+ this.highlighter = await walker.parentFront.getHighlighterByType(
+ SELECTOR_HIGHLIGHTER_TYPE
+ );
+ return this.highlighter;
+ } 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"
+ );
+ }
+ return null;
+ },
+
+ /**
+ * Save the editor contents into a file and set savedFile property.
+ * A file picker UI will open if file is not set and editor is not headless.
+ *
+ * @param mixed file
+ * Optional nsIFile or string representing the filename to save in the
+ * background, no UI will be displayed.
+ * If not specified, the original style sheet URI is used.
+ * To implement 'Save' instead of 'Save as', you can pass
+ * savedFile here.
+ * @param function(nsIFile aFile) callback
+ * Optional callback called when the operation has finished.
+ * aFile has the nsIFile object for saved file or null if the operation
+ * has failed or has been canceled by the user.
+ * @see savedFile
+ */
+ saveToFile(file, callback) {
+ const onFile = returnFile => {
+ if (!returnFile) {
+ if (callback) {
+ callback(null);
+ }
+ return;
+ }
+
+ if (this.sourceEditor) {
+ this._state.text = this.sourceEditor.getText();
+ }
+
+ const ostream = lazy.FileUtils.openSafeFileOutputStream(returnFile);
+ const buffer = new TextEncoder().encode(this._state.text).buffer;
+ const istream = new lazy.BufferStream(buffer, 0, buffer.byteLength);
+
+ lazy.NetUtil.asyncCopy(istream, ostream, status => {
+ if (!Components.isSuccessCode(status)) {
+ if (callback) {
+ callback(null);
+ }
+ this.emit("error", { key: SAVE_ERROR });
+ return;
+ }
+ lazy.FileUtils.closeSafeFileOutputStream(ostream);
+
+ this.onFileSaved(returnFile);
+
+ if (callback) {
+ callback(returnFile);
+ }
+ });
+ };
+
+ let defaultName;
+ if (this._friendlyName) {
+ defaultName = PathUtils.isAbsolute(this._friendlyName)
+ ? PathUtils.filename(this._friendlyName)
+ : this._friendlyName;
+ }
+ showFilePicker(
+ file || this._styleSheetFilePath,
+ true,
+ this._window,
+ onFile,
+ defaultName
+ );
+ },
+
+ /**
+ * Called when this source has been successfully saved to disk.
+ */
+ onFileSaved(returnFile) {
+ this._friendlyName = null;
+ this.savedFile = returnFile;
+
+ if (this.sourceEditor) {
+ this.sourceEditor.setClean();
+ }
+
+ this.emit("property-change");
+
+ // TODO: replace with file watching
+ this._modCheckCount = 0;
+ this._window.clearTimeout(this._timeout);
+
+ if (this.linkedCSSFile && !this.linkedCSSFileError) {
+ this._timeout = this._window.setTimeout(
+ this.checkLinkedFileForChanges,
+ CHECK_LINKED_SHEET_DELAY
+ );
+ }
+ },
+
+ /**
+ * Check to see if our linked CSS file has changed on disk, and
+ * if so, update the live style sheet.
+ */
+ checkLinkedFileForChanges() {
+ IOUtils.stat(this.linkedCSSFile).then(info => {
+ const lastChange = info.lastModified;
+
+ if (this._fileModDate && lastChange != this._fileModDate) {
+ this._fileModDate = lastChange;
+ this._modCheckCount = 0;
+
+ this.updateLinkedStyleSheet();
+ return;
+ }
+
+ if (++this._modCheckCount > MAX_CHECK_COUNT) {
+ this.updateLinkedStyleSheet();
+ return;
+ }
+
+ // try again in a bit
+ this._timeout = this._window.setTimeout(
+ this.checkLinkedFileForChanges,
+ CHECK_LINKED_SHEET_DELAY
+ );
+ }, this.markLinkedFileBroken);
+ },
+
+ /**
+ * Notify that the linked CSS file (if this is an original source)
+ * doesn't exist on disk in the place we think it does.
+ *
+ * @param string error
+ * The error we got when trying to access the file.
+ */
+ markLinkedFileBroken(error) {
+ this.linkedCSSFileError = error || true;
+ this.emit("linked-css-file-error");
+
+ error +=
+ " querying " +
+ this.linkedCSSFile +
+ " original source location: " +
+ this.savedFile.path;
+ console.error(error);
+ },
+
+ /**
+ * For original sources (e.g. Sass files). Fetch contents of linked CSS
+ * file from disk and live update the stylesheet object with the contents.
+ */
+ updateLinkedStyleSheet() {
+ IOUtils.read(this.linkedCSSFile).then(async array => {
+ const decoder = new TextDecoder();
+ const text = decoder.decode(array);
+
+ // Ensure we don't re-fetch the text from the original source
+ // actor when we're notified that the style sheet changed.
+ const styleSheetsFront = await this._getStyleSheetsFront();
+
+ await styleSheetsFront.update(
+ this.resourceId,
+ text,
+ this.transitionsEnabled,
+ STYLE_SHEET_UPDATE_CAUSED_BY_STYLE_EDITOR
+ );
+ }, this.markLinkedFileBroken);
+ },
+
+ /**
+ * Retrieve custom key bindings objects as expected by Editor.
+ * Editor action names are not displayed to the user.
+ *
+ * @return {array} key binding objects for the source editor
+ */
+ _getKeyBindings() {
+ const saveStyleSheetKeybind = Editor.accel(
+ getString("saveStyleSheet.commandkey")
+ );
+ const focusFilterInputKeybind = Editor.accel(
+ getString("focusFilterInput.commandkey")
+ );
+
+ return {
+ Esc: false,
+ [saveStyleSheetKeybind]: () => {
+ this.saveToFile(this.savedFile);
+ },
+ ["Shift-" + saveStyleSheetKeybind]: () => {
+ this.saveToFile();
+ },
+ // We can't simply ignore this (with `false`, or returning `CodeMirror.Pass`), as the
+ // event isn't received by the event listener in StyleSheetUI.
+ [focusFilterInputKeybind]: () => {
+ this.emit("filter-input-keyboard-shortcut");
+ },
+ };
+ },
+
+ _getStyleSheetsFront() {
+ return this._resource.targetFront.getFront("stylesheets");
+ },
+
+ /**
+ * Clean up for this editor.
+ */
+ destroy() {
+ if (this._sourceEditor) {
+ this._sourceEditor.off("dirty-change", this.onPropertyChange);
+ this._sourceEditor.off("saveRequested", this.saveToFile);
+ this._sourceEditor.off("change", this.updateStyleSheet);
+ if (this._sourceEditor.container?.contentWindow) {
+ this._sourceEditor.container.contentWindow.removeEventListener(
+ "mousemove",
+ this._onMouseMove
+ );
+ }
+ this._sourceEditor.destroy();
+ }
+ this._isDestroyed = true;
+ },
+};
+
+/**
+ * Find a path on disk for a file given it's hosted uri, the uri of the
+ * original resource that generated it (e.g. Sass file), and the location of the
+ * local file for that source.
+ *
+ * @param {nsIURI} uri
+ * The uri of the resource
+ * @param {nsIURI} origUri
+ * The uri of the original source for the resource
+ * @param {nsIFile} file
+ * The local file for the resource on disk
+ *
+ * @return {string}
+ * The path of original file on disk
+ */
+function findLinkedFilePath(uri, origUri, file) {
+ const { origBranch, branch } = findUnsharedBranches(origUri, uri);
+ const project = findProjectPath(file, origBranch);
+
+ const parts = project.concat(branch);
+ const path = PathUtils.join.apply(this, parts);
+
+ return path;
+}
+
+/**
+ * Find the path of a project given a file in the project and its branch
+ * off the root. e.g.:
+ * /Users/moz/proj/src/a.css" and "src/a.css"
+ * would yield ["Users", "moz", "proj"]
+ *
+ * @param {nsIFile} file
+ * file for that resource on disk
+ * @param {array} branch
+ * path parts for branch to chop off file path.
+ * @return {array}
+ * array of path parts
+ */
+function findProjectPath(file, branch) {
+ const path = PathUtils.split(file.path);
+
+ for (let i = 2; i <= branch.length; i++) {
+ // work backwards until we find a differing directory name
+ if (path[path.length - i] != branch[branch.length - i]) {
+ return path.slice(0, path.length - i + 1);
+ }
+ }
+
+ // if we don't find a differing directory, just chop off the branch
+ return path.slice(0, path.length - branch.length);
+}
+
+/**
+ * Find the parts of a uri past the root it shares with another uri. e.g:
+ * "http://localhost/built/a.scss" and "http://localhost/src/a.css"
+ * would yield ["built", "a.scss"] and ["src", "a.css"]
+ *
+ * @param {nsIURI} origUri
+ * uri to find unshared branch of. Usually is uri for original source.
+ * @param {nsIURI} uri
+ * uri to compare against to get a shared root
+ * @return {object}
+ * object with 'branch' and 'origBranch' array of path parts for branch
+ */
+function findUnsharedBranches(origUri, uri) {
+ origUri = PathUtils.split(origUri.pathQueryRef);
+ uri = PathUtils.split(uri.pathQueryRef);
+
+ for (let i = 0; i < uri.length - 1; i++) {
+ if (uri[i] != origUri[i]) {
+ return {
+ branch: uri.slice(i),
+ origBranch: origUri.slice(i),
+ };
+ }
+ }
+ return {
+ branch: uri,
+ origBranch: origUri,
+ };
+}
+
+/**
+ * Remove the query string from a url.
+ *
+ * @param {string} href
+ * Url to remove query string from
+ * @return {string}
+ * Url without query string
+ */
+function removeQuery(href) {
+ return href.replace(/\?.*/, "");
+}
diff --git a/devtools/client/styleeditor/index.xhtml b/devtools/client/styleeditor/index.xhtml
new file mode 100644
index 0000000000..460e0545ac
--- /dev/null
+++ b/devtools/client/styleeditor/index.xhtml
@@ -0,0 +1,256 @@
+<?xml version="1.0"?>
+<!-- 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/. -->
+<!DOCTYPE window>
+
+<window
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="style-editor-chrome-window"
+>
+ <linkset>
+ <html:link rel="stylesheet" href="chrome://global/skin/global.css" />
+ <html:link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/widgets/widgets.css"
+ />
+ <html:link
+ rel="stylesheet"
+ href="chrome://devtools/content/shared/toolbarbutton.css"
+ />
+ <html:link rel="stylesheet" href="chrome://devtools/skin/chart.css" />
+ <html:link rel="stylesheet" href="chrome://devtools/skin/widgets.css" />
+ <html:link rel="stylesheet" href="chrome://devtools/skin/splitview.css" />
+ <html:link rel="stylesheet" href="chrome://devtools/skin/styleeditor.css" />
+
+ <html:link rel="localization" href="toolkit/global/textActions.ftl" />
+ <html:link rel="localization" href="devtools/client/styleeditor.ftl" />
+ </linkset>
+
+ <script src="chrome://devtools/content/shared/theme-switching.js" />
+ <script src="chrome://global/content/globalOverlay.js" />
+ <script src="chrome://browser/content/utilityOverlay.js" />
+ <script src="chrome://global/content/editMenuOverlay.js" />
+ <script>
+ "use strict";
+ /* import-globals-from ../../../toolkit/content/globalOverlay.js */
+ /* import-globals-from ../../../toolkit/content/editMenuOverlay.js */
+ /* exported goUpdateSourceEditorMenuItems */
+ function goUpdateSourceEditorMenuItems() {
+ goUpdateGlobalEditMenuItems();
+
+ [
+ "cmd_undo",
+ "cmd_redo",
+ "cmd_cut",
+ "cmd_paste",
+ "cmd_delete",
+ "cmd_find",
+ "cmd_findAgain",
+ ].forEach(goUpdateCommand);
+ }
+ </script>
+
+ <popupset id="style-editor-popups">
+ <menupopup
+ id="sourceEditorContextMenu"
+ incontentshell="false"
+ onpopupshowing="goUpdateSourceEditorMenuItems()"
+ >
+ <menuitem
+ id="cMenu_undo"
+ data-l10n-id="text-action-undo"
+ command="cmd_undo"
+ />
+ <menuseparator />
+ <menuitem
+ id="cMenu_cut"
+ data-l10n-id="text-action-cut"
+ command="cmd_cut"
+ />
+ <menuitem
+ id="cMenu_copy"
+ data-l10n-id="text-action-copy"
+ command="cmd_copy"
+ />
+ <menuitem
+ id="cMenu_paste"
+ data-l10n-id="text-action-paste"
+ command="cmd_paste"
+ />
+ <menuitem
+ id="cMenu_delete"
+ data-l10n-id="text-action-delete"
+ command="cmd_delete"
+ />
+ <menuseparator />
+ <menuitem
+ id="cMenu_selectAll"
+ data-l10n-id="text-action-select-all"
+ command="cmd_selectAll"
+ />
+ <menuseparator />
+ <menuitem
+ id="se-menu-find"
+ data-l10n-id="styleeditor-find"
+ command="cmd_find"
+ />
+ <menuitem
+ id="cMenu_findAgain"
+ data-l10n-id="styleeditor-find-again"
+ command="cmd_findAgain"
+ />
+ <menuseparator />
+ <menuitem
+ id="se-menu-gotoLine"
+ data-l10n-id="styleeditor-go-to-line"
+ command="cmd_gotoLine"
+ />
+ </menupopup>
+ <menupopup id="sidebar-context" incontentshell="false">
+ <menuitem
+ id="context-openlinknewtab"
+ data-l10n-id="styleeditor-open-link-new-tab"
+ />
+ <menuitem id="context-copyurl" data-l10n-id="styleeditor-copy-url" />
+ </menupopup>
+ </popupset>
+
+ <commandset id="sourceEditorCommands">
+ <command id="cmd_gotoLine" oncommand="goDoCommand('cmd_gotoLine')" />
+ <command id="cmd_find" oncommand="goDoCommand('cmd_find')" />
+ <command id="cmd_findAgain" oncommand="goDoCommand('cmd_findAgain')" />
+ </commandset>
+
+ <keyset id="sourceEditorKeys" />
+
+ <box
+ id="style-editor-chrome"
+ class="devtools-responsive-container loading theme-body"
+ context="sidebar-context"
+ >
+ <box class="splitview-controller">
+ <box class="splitview-main">
+ <toolbar class="devtools-toolbar">
+ <toolbarbutton
+ class="style-editor-newButton devtools-toolbarbutton"
+ data-l10n-id="styleeditor-new-button"
+ />
+ <toolbarbutton
+ class="style-editor-importButton devtools-toolbarbutton"
+ data-l10n-id="styleeditor-import-button"
+ />
+ <toolbaritem class="devtools-searchbox" flex="1">
+ <html:input
+ class="devtools-filterinput"
+ data-l10n-id="styleeditor-filter-input"
+ />
+ <html:button
+ class="devtools-searchinput-clear"
+ tabindex="-1"
+ hidden=""
+ ></html:button>
+ </toolbaritem>
+ <toolbarbutton
+ id="style-editor-options"
+ class="devtools-toolbarbutton devtools-option-toolbarbutton"
+ data-l10n-id="styleeditor-options-button"
+ />
+ </toolbar>
+ </box>
+ <box
+ id="splitview-resizer-target"
+ class="theme-sidebar splitview-nav-container"
+ persist="height"
+ >
+ <html:ol class="splitview-nav" tabindex="0"></html:ol>
+ <html:div class="splitview-nav placeholder empty">
+ <html:p>
+ <html:strong data-l10n-id="styleeditor-no-stylesheet"></html:strong>
+ </html:p>
+ <html:p data-l10n-id="styleeditor-no-stylesheet-tip">
+ <html:a
+ class="style-editor-newButton"
+ data-l10n-name="append-new-stylesheet"
+ href="#"
+ />
+ </html:p>
+ </html:div>
+ <html:div class="splitview-nav placeholder all-filtered">
+ <html:p data-l10n-id="styleeditor-stylesheet-all-filtered"></html:p>
+ </html:div>
+ </box>
+ </box>
+ <splitter
+ class="devtools-side-splitter devtools-invisible-splitter"
+ resizebefore="sibling"
+ resizeafter="none"
+ />
+ <box>
+ <box class="splitview-side-details devtools-main-content" />
+ <html:footer
+ class="devtools-toolbar stylesheet-editor-status"
+ hidden="true"
+ >
+ <html:button
+ class="devtools-button style-editor-prettyPrintButton"
+ data-l10n-id="styleeditor-pretty-print-button"
+ />
+ </html:footer>
+ </box>
+
+ <html:div id="splitview-templates" hidden="">
+ <html:li id="splitview-tpl-summary-stylesheet" tabindex="0">
+ <label
+ class="stylesheet-toggle"
+ tabindex="0"
+ data-l10n-id="styleeditor-visibility-toggle"
+ ></label>
+ <html:hgroup class="stylesheet-info">
+ <html:h1
+ ><html:a class="stylesheet-name" tabindex="0"
+ ><label crop="center" /></html:a
+ ></html:h1>
+ <html:div class="stylesheet-more">
+ <html:h3 class="stylesheet-title"></html:h3>
+ <html:h3 class="stylesheet-linked-file"></html:h3>
+ <html:h3
+ class="stylesheet-rule-count"
+ data-l10n-id="styleeditor-stylesheet-rule-count"
+ data-l10n-args='{"ruleCount": 0}'
+ ></html:h3>
+ <spacer />
+ <html:h3
+ ><label
+ class="stylesheet-saveButton"
+ data-l10n-id="styleeditor-save-button"
+ ></label
+ ></html:h3>
+ </html:div>
+ </html:hgroup>
+ </html:li>
+
+ <box id="splitview-tpl-details-stylesheet" class="splitview-details">
+ <hbox class="stylesheet-details-container">
+ <box class="stylesheet-editor-input textbox" />
+ <splitter
+ class="devtools-side-splitter"
+ resizebefore="none"
+ resizeafter="sibling"
+ />
+ <vbox class="stylesheet-sidebar theme-sidebar" hidden="true">
+ <toolbar
+ class="devtools-toolbar"
+ data-l10n-id="styleeditor-at-rules"
+ >
+ </toolbar>
+ <vbox class="stylesheet-at-rules-container" flex="1">
+ <html:div class="stylesheet-at-rules-list" />
+ </vbox>
+ </vbox>
+ </hbox>
+ </box>
+ </html:div>
+ </box>
+</window>
diff --git a/devtools/client/styleeditor/moz.build b/devtools/client/styleeditor/moz.build
new file mode 100644
index 0000000000..9e4428f91a
--- /dev/null
+++ b/devtools/client/styleeditor/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+BROWSER_CHROME_MANIFESTS += ["test/browser.toml"]
+
+DevToolsModules(
+ "original-source.js",
+ "panel.js",
+ "StyleEditorUI.sys.mjs",
+ "StyleEditorUtil.sys.mjs",
+ "StyleSheetEditor.sys.mjs",
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("DevTools", "Style Editor")
diff --git a/devtools/client/styleeditor/original-source.js b/devtools/client/styleeditor/original-source.js
new file mode 100644
index 0000000000..1c01ae0355
--- /dev/null
+++ b/devtools/client/styleeditor/original-source.js
@@ -0,0 +1,103 @@
+/* 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";
+
+/**
+ * An object of this type represents an original source for the style
+ * editor. An "original" source is one that is mentioned in a source
+ * map.
+ *
+ * @param {String} url
+ * The URL of the original source.
+ * @param {String} sourceID
+ * The source ID of the original source, as used by the source
+ * map service.
+ * @param {SourceMapLoader} sourceMapLoader
+ * The source map loader; @see Toolbox.sourceMapLoader
+ */
+function OriginalSource(url, sourceId, sourceMapLoader) {
+ this.isOriginalSource = true;
+
+ this._url = url;
+ this._sourceId = sourceId;
+ this._sourceMapLoader = sourceMapLoader;
+}
+
+OriginalSource.prototype = {
+ get sourceId() {
+ return this._sourceId;
+ },
+
+ /** Get the original source's URL. */
+ get url() {
+ return this._url;
+ },
+
+ /** Get the original source's URL. */
+ get href() {
+ return this._url;
+ },
+
+ /**
+ * Return a promise that will resolve to the original source's full
+ * text. The return result is actually an object with a single
+ * `string` method; this method will return the source text as a
+ * string. This is done because the style editor elsewhere expects
+ * a long string actor.
+ */
+ getText() {
+ if (!this._sourcePromise) {
+ this._sourcePromise = this._sourceMapLoader
+ .getOriginalSourceText(this._sourceId)
+ .then(contents => {
+ // Make it look like a long string actor.
+ return {
+ string: () => contents.text,
+ };
+ });
+ }
+ return this._sourcePromise;
+ },
+
+ /**
+ * Given a source-mapped, generated style sheet, a line, and a
+ * column, return the corresponding original location in this style
+ * sheet.
+ *
+ * @param {StyleSheetResource} relatedSheet
+ * The generated style sheet's resource
+ * @param {Number} line
+ * Line number.
+ * @param {Number} column
+ * Column number.
+ * @return {Location}
+ * The original location, an object with at least
+ * `sourceUrl`, `source`, `styleSheet`, `line`, and `column`
+ * properties.
+ */
+ getOriginalLocation(relatedSheet, line, column) {
+ const { href, nodeHref, resourceId: sourceId } = relatedSheet;
+ const sourceUrl = href || nodeHref;
+ return this._sourceMapLoader
+ .getOriginalLocation({
+ sourceId,
+ line,
+ column,
+ sourceUrl,
+ })
+ .then(location => {
+ // Add some properties for the style editor.
+ location.source = location.sourceUrl;
+ location.styleSheet = relatedSheet;
+ return location;
+ });
+ },
+
+ // Dummy implementations, as we never emit an event.
+ on() {},
+ off() {},
+};
+
+exports.OriginalSource = OriginalSource;
diff --git a/devtools/client/styleeditor/panel.js b/devtools/client/styleeditor/panel.js
new file mode 100644
index 0000000000..5a2772d095
--- /dev/null
+++ b/devtools/client/styleeditor/panel.js
@@ -0,0 +1,172 @@
+/* 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";
+
+var EventEmitter = require("resource://devtools/shared/event-emitter.js");
+
+var { StyleEditorUI } = ChromeUtils.importESModule(
+ "resource://devtools/client/styleeditor/StyleEditorUI.sys.mjs"
+);
+var { getString } = ChromeUtils.importESModule(
+ "resource://devtools/client/styleeditor/StyleEditorUtil.sys.mjs"
+);
+
+var StyleEditorPanel = function StyleEditorPanel(panelWin, toolbox, commands) {
+ EventEmitter.decorate(this);
+
+ this._toolbox = toolbox;
+ this._commands = commands;
+ this._panelWin = panelWin;
+ this._panelDoc = panelWin.document;
+
+ this._showError = this._showError.bind(this);
+};
+
+exports.StyleEditorPanel = StyleEditorPanel;
+
+StyleEditorPanel.prototype = {
+ get panelWindow() {
+ return this._panelWin;
+ },
+
+ /**
+ * open is effectively an asynchronous constructor
+ */
+ async open(options) {
+ // Initialize the CSS properties database.
+ const { cssProperties } = await this._toolbox.target.getFront(
+ "cssProperties"
+ );
+
+ // Initialize the UI
+ this.UI = new StyleEditorUI(
+ this._toolbox,
+ this._commands,
+ this._panelDoc,
+ cssProperties
+ );
+ this.UI.on("error", this._showError);
+ await this.UI.initialize(options);
+
+ return this;
+ },
+
+ /**
+ * Show an error message from the style editor in the toolbox
+ * notification box.
+ *
+ * @param {string} data
+ * The parameters to customize the error message
+ */
+ _showError(data) {
+ if (!this._toolbox) {
+ // could get an async error after we've been destroyed
+ return;
+ }
+
+ let errorMessage = getString(data.key);
+ if (data.append) {
+ errorMessage += " " + data.append;
+ }
+
+ const notificationBox = this._toolbox.getNotificationBox();
+ const notification =
+ notificationBox.getNotificationWithValue("styleeditor-error");
+
+ let level = notificationBox.PRIORITY_CRITICAL_LOW;
+ if (data.level === "info") {
+ level = notificationBox.PRIORITY_INFO_LOW;
+ } else if (data.level === "warning") {
+ level = notificationBox.PRIORITY_WARNING_LOW;
+ }
+
+ if (!notification) {
+ notificationBox.appendNotification(
+ errorMessage,
+ "styleeditor-error",
+ "",
+ level
+ );
+ }
+ },
+
+ /**
+ * Select a stylesheet.
+ *
+ * @param {StyleSheetResource} stylesheet
+ * The resource for the stylesheet to find and select in editor.
+ * @param {number} line
+ * Line number to jump to after selecting. One-indexed
+ * @param {number} col
+ * Column number to jump to after selecting. One-indexed
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected and ready
+ * to be used.
+ */
+ selectStyleSheet(stylesheet, line, col) {
+ if (!this.UI) {
+ return null;
+ }
+
+ return this.UI.selectStyleSheet(stylesheet, line - 1, col ? col - 1 : 0);
+ },
+
+ /**
+ * Given a location in an original file, open that file in the editor.
+ *
+ * @param {string} originalId
+ * The original "sourceId" returned from the sourcemap worker.
+ * @param {number} line
+ * Line number to jump to after selecting. One-indexed
+ * @param {number} col
+ * Column number to jump to after selecting. One-indexed
+ * @return {Promise}
+ * Promise that will resolve when the editor is selected and ready
+ * to be used.
+ */
+ selectOriginalSheet(originalId, line, col) {
+ if (!this.UI) {
+ return null;
+ }
+
+ const originalSheet = this.UI.getOriginalSourceSheet(originalId);
+ return this.UI.selectStyleSheet(originalSheet, line - 1, col ? col - 1 : 0);
+ },
+
+ getStylesheetResourceForGeneratedURL(url) {
+ if (!this.UI) {
+ return null;
+ }
+
+ return this.UI.getStylesheetResourceForGeneratedURL(url);
+ },
+
+ /**
+ * Destroy the style editor.
+ */
+ destroy() {
+ if (this._destroyed) {
+ return;
+ }
+ this._destroyed = true;
+
+ this._toolbox = null;
+ this._panelWin = null;
+ this._panelDoc = null;
+
+ this.UI.destroy();
+ this.UI = null;
+ },
+};
+
+ChromeUtils.defineLazyGetter(
+ StyleEditorPanel.prototype,
+ "strings",
+ function () {
+ return Services.strings.createBundle(
+ "chrome://devtools/locale/styleeditor.properties"
+ );
+ }
+);
diff --git a/devtools/client/styleeditor/test/autocomplete.html b/devtools/client/styleeditor/test/autocomplete.html
new file mode 100644
index 0000000000..801eb4d4b9
--- /dev/null
+++ b/devtools/client/styleeditor/test/autocomplete.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for autocomplete testing</title>
+ <link rel="stylesheet" type="text/css" href="resources_inpage1.css"/>
+ <style type="text/css">
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+
+ div + button {
+ border: 2px dotted red;
+ }
+ </style>
+</head>
+<body>
+ <div>parent <span>child</span></div><button>sibling</button>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/browser.toml b/devtools/client/styleeditor/test/browser.toml
new file mode 100644
index 0000000000..34cf130826
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser.toml
@@ -0,0 +1,227 @@
+[DEFAULT]
+tags = "devtools"
+subsuite = "devtools"
+support-files = [
+ "autocomplete.html",
+ "bug_1405342_serviceworker_iframes.html",
+ "four.html",
+ "head.js",
+ "iframe_with_service_worker.html",
+ "iframe_service_worker.js",
+ "import.css",
+ "import.html",
+ "import2.css",
+ "inline-1.html",
+ "inline-2.html",
+ "longload.html",
+ "longname.html",
+ "many-media-rules-sourcemaps/index.html",
+ "many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css",
+ "many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css.map",
+ "many-media-rules-sourcemaps/sourcemap/sourcemap-sass/_partial.scss",
+ "many-media-rules-sourcemaps/sourcemap/sourcemap-sass/sourcemaps.scss",
+ "media-small.css",
+ "media.html",
+ "media-rules.html",
+ "media-rules.css",
+ "media-rules-sourcemaps.html",
+ "minified.html",
+ "missing.html",
+ "nostyle.html",
+ "pretty.css",
+ "resources_inpage.jsi",
+ "resources_inpage1.css",
+ "resources_inpage2.css",
+ "selector-highlighter.html",
+ "simple.css",
+ "simple.css.gz",
+ "simple.css.gz^headers^",
+ "simple.gz.html",
+ "simple.html",
+ "sjs_huge-css-server.sjs",
+ "sourcemap-css/contained.css",
+ "sourcemap-css/sourcemaps.css",
+ "sourcemap-css/sourcemaps_chrome.css",
+ "sourcemap-css/sourcemaps.css.map",
+ "sourcemap-css/sourcemaps.css.map^headers^", # add nosniff header to test against Bug 1330383
+ "sourcemap-css/media-rules.css",
+ "sourcemap-css/media-rules.css.map",
+ "sourcemap-css/test-bootstrap-scss.css",
+ "sourcemap-css/test-stylus.css",
+ "sourcemap-sass/sourcemaps.scss",
+ "sourcemap-sass/sourcemaps.scss^headers^", # add nosniff header to test against Bug 1330383
+ "sourcemap-sass/media-rules.scss",
+ "sourcemap-styl/test-stylus.styl",
+ "sourcemaps.html",
+ "sourcemaps-inline.html",
+ "sourcemaps-large.html",
+ "sourcemaps-watching.html",
+ "test_private.css",
+ "test_private.html",
+ "doc_empty.html",
+ "doc_fetch_from_netmonitor.html",
+ "doc_long_string.css",
+ "doc_long.css",
+ "doc_short_string.css",
+ "doc_sourcemap_chrome.html",
+ "doc_xulpage.xhtml",
+ "sync.html",
+ "sync_with_csp.css",
+ "sync_with_csp.html",
+ "utf-16.css",
+ "veryveryverylongnamethatcanbreakthestyleeditor.css",
+ "!/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js",
+ "!/devtools/client/inspector/shared/test/head.js",
+ "!/devtools/client/inspector/test/head.js",
+ "!/devtools/client/inspector/test/shared-head.js",
+ "!/devtools/client/shared/test/shared-head.js",
+ "!/devtools/client/shared/test/telemetry-test-helpers.js",
+ "!/devtools/client/shared/test/highlighter-test-actor.js",
+]
+
+["browser_styleeditor_add_stylesheet.js"]
+
+["browser_styleeditor_at_rules_sidebar.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_autocomplete-disabled.js"]
+
+["browser_styleeditor_autocomplete.js"]
+
+["browser_styleeditor_bom.js"]
+
+["browser_styleeditor_bug_740541_iframes.js"]
+
+["browser_styleeditor_bug_851132_middle_click.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_bug_870339.js"]
+
+["browser_styleeditor_bug_1247083_inline_stylesheet_numbering.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_bug_1405342_serviceworker_iframes.js"]
+skip-if = [
+ "!debug && os == 'win'",
+ "os == 'linux' && os_version == '18.04'", #bug 1424914
+]
+
+["browser_styleeditor_copyurl.js"]
+
+["browser_styleeditor_enabled.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_fetch-from-netmonitor.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_styleeditor_filesave.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_styleeditor_filter.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_fission_switch_target.js"]
+
+["browser_styleeditor_highlight-selector.js"]
+
+["browser_styleeditor_import.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_import_rule.js"]
+
+["browser_styleeditor_init.js"]
+
+["browser_styleeditor_inline_friendly_names.js"]
+
+["browser_styleeditor_loading.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+]
+
+["browser_styleeditor_loading_with_containers.js"]
+
+["browser_styleeditor_media_sidebar_links.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_media_sidebar_sourcemaps.js"]
+skip-if = ["a11y_checks"] # Bug 1858041 and 1849028 intermittent a11y_checks results (fails on Try, passes on Autoland)
+
+["browser_styleeditor_missing_stylesheet.js"]
+
+["browser_styleeditor_navigate.js"]
+
+["browser_styleeditor_new.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_nostyle.js"]
+
+["browser_styleeditor_opentab.js"]
+
+["browser_styleeditor_pretty.js"]
+
+["browser_styleeditor_private_perwindowpb.js"]
+
+["browser_styleeditor_reload.js"]
+
+["browser_styleeditor_remove_stylesheet.js"]
+
+["browser_styleeditor_resize_performance.js"]
+
+["browser_styleeditor_scroll.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_selectstylesheet.js"]
+
+["browser_styleeditor_sidebars.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_sourcemap_chrome.js"]
+
+["browser_styleeditor_sourcemap_large.js"]
+skip-if = ["a11y_checks"] # Bug 1858041 and 1849028 intermittent a11y_checks results (fails on Try, passes on Autoland)
+
+["browser_styleeditor_sourcemap_watching.js"]
+skip-if = [
+ "http3", # Bug 1829298
+ "http2",
+ "a11y_checks", # Bug 1858041 and 1849028 intermittent a11y_checks results (fails on Try, passes on Autoland)
+]
+
+["browser_styleeditor_sourcemaps.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_sourcemaps_inline.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_sv_keynav.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_sv_resize.js"]
+
+["browser_styleeditor_sync.js"]
+skip-if = ["a11y_checks && debug"] # Bugs 1849028 and 1858041 for causing intermittent test results
+
+["browser_styleeditor_syncAddProperty.js"]
+
+["browser_styleeditor_syncAddRule.js"]
+
+["browser_styleeditor_syncAlreadyOpen.js"]
+
+["browser_styleeditor_syncEditSelector.js"]
+
+["browser_styleeditor_syncIntoRuleView.js"]
+
+["browser_styleeditor_transition_rule.js"]
+fail-if = ["a11y_checks"] # Bug 1849028 clicked element may not be focusable and/or labeled
+
+["browser_styleeditor_xul.js"]
+
+["browser_toolbox_styleeditor.js"]
+skip-if = ["asan"] # Bug 1591064
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js
new file mode 100644
index 0000000000..794de2c328
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_add_stylesheet.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that a newly-added style sheet shows up in the style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "Two sheets present after load.");
+
+ // We have to wait for the length to change, because we might still
+ // be seeing events from the initial open.
+ const added = new Promise(resolve => {
+ const handler = () => {
+ if (ui.editors.length === 3) {
+ ui.off("editor-added", handler);
+ resolve();
+ }
+ };
+ ui.on("editor-added", handler);
+ });
+
+ info("Adding a style sheet");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const document = content.document;
+ const style = document.createElement("style");
+ style.appendChild(document.createTextNode("div { background: #f06; }"));
+ document.head.appendChild(style);
+ });
+ await added;
+
+ is(ui.editors.length, 3, "Three sheets present after new style sheet");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js b/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js
new file mode 100644
index 0000000000..a0a9bc93fd
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_at_rules_sidebar.js
@@ -0,0 +1,340 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
+const SIDEBAR_PREF = "devtools.styleeditor.showAtRulesSidebar";
+
+const RESIZE_W = 300;
+const RESIZE_H = 450;
+const LABELS = [
+ "not all",
+ "all",
+ "(max-width: 550px)",
+ "(min-height: 300px) and (max-height: 320px)",
+ "(max-width: 750px)",
+ "",
+ "print",
+];
+const LINE_NOS = [1, 7, 19, 25, 31, 34, 39];
+const NEW_RULE = `
+ @media (max-width: 750px) {
+ div {
+ color: blue;
+ @layer {
+ border-color: tomato;
+ }
+ }
+
+ @media print {
+ body {
+ filter: grayscale(100%);
+ }
+ }
+ }`;
+
+waitForExplicitFinish();
+
+add_task(async function () {
+ await pushPref("layout.css.container-queries.enabled", true);
+
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 4, "correct number of editors");
+
+ info("Test first plain css editor");
+ const plainEditor = ui.editors[0];
+ await openEditor(plainEditor);
+ testPlainEditor(plainEditor);
+
+ info("Test editor for inline sheet with @media rules");
+ const inlineMediaEditor = ui.editors[3];
+ await openEditor(inlineMediaEditor);
+ await testInlineMediaEditor(ui, inlineMediaEditor);
+
+ info("Test editor with @media rules");
+ const mediaEditor = ui.editors[1];
+ await openEditor(mediaEditor);
+ await testMediaEditor(ui, mediaEditor);
+
+ info("Test that sidebar hides when flipping pref");
+ await testShowHide(ui, mediaEditor);
+
+ info("Test adding a rule updates the list");
+ await testMediaRuleAdded(ui, mediaEditor);
+
+ info("Test resizing and seeing @media matching state change");
+ const originalWidth = window.outerWidth;
+ const originalHeight = window.outerHeight;
+
+ const onMatchesChange = ui.once("at-rules-list-changed");
+ window.resizeTo(RESIZE_W, RESIZE_H);
+ await onMatchesChange;
+
+ testMediaMatchChanged(mediaEditor);
+
+ window.resizeTo(originalWidth, originalHeight);
+});
+
+function testPlainEditor(editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, true, "sidebar is hidden on editor without @media");
+}
+
+async function testInlineMediaEditor(ui, editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, false, "sidebar is showing on editor with @media");
+
+ const entries = sidebar.querySelectorAll(".at-rule-label");
+ is(entries.length, 6, "6 @media rules displayed in sidebar");
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[0],
+ conditionText: "screen",
+ matches: true,
+ line: 2,
+ type: "media",
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[1],
+ conditionText: "(display: flex)",
+ line: 7,
+ type: "support",
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[2],
+ conditionText: "(1px < height < 10000px)",
+ matches: true,
+ line: 8,
+ type: "media",
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[3],
+ conditionText: "",
+ line: 16,
+ type: "layer",
+ layerName: "myLayer",
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[4],
+ conditionText: "(min-width: 1px)",
+ line: 17,
+ type: "container",
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[5],
+ conditionText: "selector(&)",
+ line: 21,
+ type: "support",
+ });
+}
+
+async function testMediaEditor(ui, editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, false, "sidebar is showing on editor with @media");
+
+ const entries = [...sidebar.querySelectorAll(".at-rule-label")];
+ is(entries.length, 4, "four @media rules displayed in sidebar");
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[0],
+ conditionText: LABELS[0],
+ matches: false,
+ line: LINE_NOS[0],
+ });
+ await testRule({
+ ui,
+ editor,
+ rule: entries[1],
+ conditionText: LABELS[1],
+ matches: true,
+ line: LINE_NOS[1],
+ });
+ await testRule({
+ ui,
+ editor,
+ rule: entries[2],
+ conditionText: LABELS[2],
+ matches: false,
+ line: LINE_NOS[2],
+ });
+ await testRule({
+ ui,
+ editor,
+ rule: entries[3],
+ conditionText: LABELS[3],
+ matches: false,
+ line: LINE_NOS[3],
+ });
+}
+
+function testMediaMatchChanged(editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+
+ const cond = sidebar.querySelectorAll(".at-rule-condition")[2];
+ is(
+ cond.textContent,
+ "(max-width: 550px)",
+ "third rule condition text is correct"
+ );
+ ok(
+ !cond.classList.contains("media-condition-unmatched"),
+ "media rule is now matched after resizing"
+ );
+}
+
+async function testShowHide(ui, editor) {
+ let sidebarChange = ui.once("at-rules-list-changed");
+ Services.prefs.setBoolPref(SIDEBAR_PREF, false);
+ await sidebarChange;
+
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, true, "sidebar is hidden after flipping pref");
+
+ sidebarChange = ui.once("at-rules-list-changed");
+ Services.prefs.clearUserPref(SIDEBAR_PREF);
+ await sidebarChange;
+
+ is(sidebar.hidden, false, "sidebar is showing after flipping pref back");
+}
+
+async function testMediaRuleAdded(ui, editor) {
+ await editor.getSourceEditor();
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(
+ sidebar.querySelectorAll(".at-rule-label").length,
+ 4,
+ "4 @media rules after changing text"
+ );
+
+ let text = editor.sourceEditor.getText();
+ text += NEW_RULE;
+
+ const listChange = ui.once("at-rules-list-changed");
+ editor.sourceEditor.setText(text);
+ await listChange;
+
+ const entries = [...sidebar.querySelectorAll(".at-rule-label")];
+ is(entries.length, 7, "7 @media rules after changing text");
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[4],
+ conditionText: LABELS[4],
+ matches: false,
+ line: LINE_NOS[4],
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[5],
+ type: "layer",
+ conditionText: LABELS[5],
+ line: LINE_NOS[5],
+ });
+
+ await testRule({
+ ui,
+ editor,
+ rule: entries[6],
+ conditionText: LABELS[6],
+ matches: false,
+ line: LINE_NOS[6],
+ });
+}
+
+/**
+ * Run assertion on given rule
+ *
+ * @param {Object} options
+ * @param {StyleEditorUI} options.ui
+ * @param {StyleSheetEditor} options.editor: The editor the rule is displayed in
+ * @param {Element} options.rule: The rule element in the media sidebar
+ * @param {String} options.conditionText: media query condition text
+ * @param {Boolean} options.matches: Whether or not the document matches the rule
+ * @param {String} options.layerName: Optional name of the @layer
+ * @param {Number} options.line: Line of the rule
+ * @param {String} options.type: The type of the rule (container, layer, media, support ).
+ * Defaults to "media".
+ */
+async function testRule({
+ ui,
+ editor,
+ rule,
+ conditionText,
+ matches,
+ layerName,
+ line,
+ type = "media",
+}) {
+ const atTypeEl = rule.querySelector(".at-rule-type");
+ is(
+ atTypeEl.textContent,
+ `@${type}\u00A0${layerName ? `${layerName}\u00A0` : ""}`,
+ "label for at-rule type is correct"
+ );
+
+ const cond = rule.querySelector(".at-rule-condition");
+ is(
+ cond.textContent,
+ conditionText,
+ "condition label is correct for " + conditionText
+ );
+
+ if (type == "media") {
+ const matched = !cond.classList.contains("media-condition-unmatched");
+ ok(
+ matches ? matched : !matched,
+ "media rule is " + (matches ? "matched" : "unmatched")
+ );
+ }
+
+ const ruleLine = rule.querySelector(".at-rule-line");
+ is(ruleLine.textContent, ":" + line, "correct line number shown");
+
+ info(
+ "Check that clicking on the rule jumps to the expected position in the stylesheet"
+ );
+ rule.click();
+ await waitFor(
+ () =>
+ ui.selectedEditor == editor &&
+ editor.sourceEditor.getCursor().line == line - 1
+ );
+ ok(true, "Jumped to the expected location");
+}
+
+/* Helpers */
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js
new file mode 100644
index 0000000000..fd7d4969d0
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete-disabled.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that autocomplete can be disabled.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
+
+// Pref which decides if CSS autocompletion is enabled in Style Editor or not.
+const AUTOCOMPLETION_PREF = "devtools.styleeditor.autocompletion-enabled";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+ const editor = await ui.editors[0].getSourceEditor();
+ editor.sourceEditor.setOption("autocomplete", false);
+
+ is(
+ editor.sourceEditor.getOption("autocomplete"),
+ false,
+ "Autocompletion option does not exist"
+ );
+ ok(
+ !editor.sourceEditor.getAutocompletionPopup(),
+ "Autocompletion popup does not exist"
+ );
+});
+
+add_task(async function () {
+ Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+ const editor = await ui.editors[0].getSourceEditor();
+
+ is(
+ editor.sourceEditor.getOption("autocomplete"),
+ false,
+ "Autocompletion option does not exist"
+ );
+});
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(AUTOCOMPLETION_PREF);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
new file mode 100644
index 0000000000..b1377c62ab
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_autocomplete.js
@@ -0,0 +1,284 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that autocompletion works as expected.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "autocomplete.html";
+const MAX_SUGGESTIONS = 15;
+
+// Test cases to test that autocompletion works correctly when enabled.
+// Format:
+// [
+// key,
+// {
+// total: Number of suggestions in the popup (-1 if popup is closed),
+// current: Index of selected suggestion,
+// inserted: 1 to check whether the selected suggestion is inserted into the
+// editor or not,
+// entered: 1 if the suggestion is inserted and finalized
+// }
+// ]
+
+function getTestCases(cssProperties) {
+ const keywords = getCSSKeywords(cssProperties);
+ const getSuggestionNumberFor = suggestionNumberGetter(keywords);
+
+ return [
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", { total: 1, current: 0 }],
+ ["VK_LEFT"],
+ ["VK_RIGHT"],
+ ["VK_DOWN"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", { total: getSuggestionNumberFor("font"), current: 0 }],
+ ["VK_END"],
+ ["VK_RETURN"],
+ ["b", { total: getSuggestionNumberFor("b"), current: 0 }],
+ ["a", { total: getSuggestionNumberFor("ba"), current: 0 }],
+ [
+ "VK_DOWN",
+ { total: getSuggestionNumberFor("ba"), current: 0, inserted: 1 },
+ ],
+ [
+ "VK_DOWN",
+ { total: getSuggestionNumberFor("ba"), current: 1, inserted: 1 },
+ ],
+ [
+ "VK_TAB",
+ { total: getSuggestionNumberFor("ba"), current: 2, inserted: 1 },
+ ],
+ ["VK_RETURN", { current: 2, inserted: 1, entered: 1 }],
+ ["b", { total: getSuggestionNumberFor("background", "b"), current: 0 }],
+ ["l", { total: getSuggestionNumberFor("background", "bl"), current: 0 }],
+ [
+ "VK_TAB",
+ {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 0,
+ inserted: 1,
+ },
+ ],
+ [
+ "VK_DOWN",
+ {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 1,
+ inserted: 1,
+ },
+ ],
+ [
+ "VK_UP",
+ {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 0,
+ inserted: 1,
+ },
+ ],
+ [
+ "VK_TAB",
+ {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 1,
+ inserted: 1,
+ },
+ ],
+ [
+ "VK_TAB",
+ {
+ total: getSuggestionNumberFor("background", "bl"),
+ current: 2,
+ inserted: 1,
+ },
+ ],
+ [";"],
+ ["VK_RETURN"],
+ ["c", { total: getSuggestionNumberFor("c"), current: 0 }],
+ ["o", { total: getSuggestionNumberFor("co"), current: 0 }],
+ ["VK_RETURN", { current: 0, inserted: 1 }],
+ ["r", { total: getSuggestionNumberFor("color", "r"), current: 0 }],
+ ["VK_RETURN", { current: 0, inserted: 1 }],
+ [";"],
+ ["VK_LEFT"],
+ ["VK_RIGHT"],
+ ["VK_DOWN"],
+ ["VK_RETURN"],
+ ["b", { total: 2, current: 0 }],
+ ["u", { total: 1, current: 0 }],
+ ["VK_RETURN", { current: 0, inserted: 1 }],
+ ["{"],
+ ["VK_HOME"],
+ ["VK_DOWN"],
+ ["VK_DOWN"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["VK_RIGHT"],
+ ["Ctrl+Space", { total: 1, current: 0 }],
+ ];
+}
+
+add_task(async function () {
+ // We try to type "background" above, so backdrop-filter enabledness affects
+ // the expectations. Instead of branching on the test set the pref to true
+ // here as that is the end state, and it doesn't interact with the test in
+ // other ways.
+ await SpecialPowers.pushPrefEnv({
+ set: [["layout.css.backdrop-filter.enabled", true]],
+ });
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+ const { cssProperties } = ui;
+ const testCases = getTestCases(cssProperties);
+
+ await ui.selectStyleSheet(ui.editors[1].styleSheet);
+ const editor = await ui.editors[1].getSourceEditor();
+
+ const sourceEditor = editor.sourceEditor;
+ const popup = sourceEditor.getAutocompletionPopup();
+
+ await SimpleTest.promiseFocus(panel.panelWindow);
+
+ for (const index in testCases) {
+ await testState(testCases, index, sourceEditor, popup, panel.panelWindow);
+ await checkState(testCases, index, sourceEditor, popup);
+ }
+});
+
+function testState(testCases, index, sourceEditor, popup, panelWindow) {
+ let [key, details] = testCases[index];
+ let entered;
+ if (details) {
+ entered = details.entered;
+ }
+ const mods = {};
+
+ info(
+ "pressing key " +
+ key +
+ " to get result: " +
+ JSON.stringify(testCases[index]) +
+ " for index " +
+ index
+ );
+
+ let evt = "after-suggest";
+
+ if (key == "Ctrl+Space") {
+ key = " ";
+ mods.ctrlKey = true;
+ } else if (key == "VK_RETURN" && entered) {
+ evt = "popup-hidden";
+ } else if (
+ /(left|right|return|home|end)/gi.test(key) ||
+ (key == "VK_DOWN" && !popup.isOpen)
+ ) {
+ evt = "cursorActivity";
+ } else if (key == "VK_TAB" || key == "VK_UP" || key == "VK_DOWN") {
+ evt = "suggestion-entered";
+ }
+
+ const ready = sourceEditor.once(evt);
+ EventUtils.synthesizeKey(key, mods, panelWindow);
+
+ return ready;
+}
+
+function checkState(testCases, index, sourceEditor, popup) {
+ return new Promise(resolve => {
+ executeSoon(() => {
+ let [, details] = testCases[index];
+ details = details || {};
+ const { total, current, inserted } = details;
+
+ if (total != undefined) {
+ ok(popup.isOpen, "Popup is open for index " + index);
+ is(
+ total,
+ popup.itemCount,
+ "Correct total suggestions for index " + index
+ );
+ is(
+ current,
+ popup.selectedIndex,
+ "Correct index is selected for index " + index
+ );
+ if (inserted) {
+ const { text } = popup.getItemAtIndex(current);
+ const { line, ch } = sourceEditor.getCursor();
+ const lineText = sourceEditor.getText(line);
+ is(
+ lineText.substring(ch - text.length, ch),
+ text,
+ "Current suggestion from the popup is inserted into the editor."
+ );
+ }
+ } else {
+ ok(!popup.isOpen, "Popup is closed for index " + index);
+ if (inserted) {
+ const { text } = popup.getItemAtIndex(current);
+ const { line, ch } = sourceEditor.getCursor();
+ const lineText = sourceEditor.getText(line);
+ is(
+ lineText.substring(ch - text.length, ch),
+ text,
+ "Current suggestion from the popup is inserted into the editor."
+ );
+ }
+ }
+ resolve();
+ });
+ });
+}
+
+/**
+ * Returns a list of all property names and a map of property name vs possible
+ * CSS values provided by the Gecko engine.
+ *
+ * @return {Object} An object with following properties:
+ * - CSSProperties {Array} Array of string containing all the possible
+ * CSS property names.
+ * - CSSValues {Object|Map} A map where key is the property name and
+ * value is an array of string containing all the possible
+ * CSS values the property can have.
+ */
+function getCSSKeywords(cssProperties) {
+ const props = {};
+ const propNames = cssProperties.getNames();
+ propNames.forEach(prop => {
+ props[prop] = cssProperties.getValues(prop).sort();
+ });
+ return {
+ CSSValues: props,
+ CSSProperties: propNames.sort(),
+ };
+}
+
+/**
+ * Returns a function that returns the number of expected suggestions for the given
+ * property and value. If the value is not null, returns the number of values starting
+ * with `value`. Returns the number of properties starting with `property` otherwise.
+ */
+function suggestionNumberGetter({ CSSProperties, CSSValues }) {
+ return (property, value) => {
+ if (value == null) {
+ return CSSProperties.filter(prop => prop.startsWith(property)).slice(
+ 0,
+ MAX_SUGGESTIONS
+ ).length;
+ }
+ return CSSValues[property]
+ .filter(val => val.startsWith(value))
+ .slice(0, MAX_SUGGESTIONS).length;
+ };
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bom.js b/devtools/client/styleeditor/test/browser_styleeditor_bom.js
new file mode 100644
index 0000000000..863351a32c
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bom.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const BOM_CSS = TEST_BASE_HTTPS + "utf-16.css";
+const DOCUMENT =
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ [
+ "<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Bug 1301854</title>",
+ ' <link rel="stylesheet" type="text/css" href="' + BOM_CSS + '">',
+ " </head>",
+ " <body>",
+ " </body>",
+ "</html>",
+ ].join("\n")
+ );
+
+const CONTENTS =
+ "// Note that this file must be utf-16 with a " +
+ "BOM for the test to make sense.\n";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(DOCUMENT);
+
+ is(ui.editors.length, 1, "correct number of editors");
+
+ const editor = ui.editors[0];
+ await editor.getSourceEditor();
+
+ const text = editor.sourceEditor.getText();
+ is(text, CONTENTS, "editor contains expected text");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_1247083_inline_stylesheet_numbering.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_1247083_inline_stylesheet_numbering.js
new file mode 100644
index 0000000000..1f40d44cc1
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_1247083_inline_stylesheet_numbering.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the displayed numbering of inline and user-created stylesheets are independent of their absolute index
+// See bug 1247083.
+
+const SIMPLE = TEST_BASE_HTTP + "simple.css";
+const LONG = TEST_BASE_HTTP + "doc_long.css";
+const DOCUMENT_WITH_LONG_SHEET =
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ [
+ "<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Style editor numbering test page</title>",
+
+ // first inline stylesheet
+ " <style>",
+ " #p-blue {",
+ " color: blue;",
+ " }",
+ " </style>",
+ // first external stylesheet
+ ' <link rel="stylesheet" type="text/css" href="' + SIMPLE + '">',
+ // second external stylesheet
+ ' <link rel="stylesheet" type="text/css" href="' + LONG + '">',
+ // second inline stylesheet
+ " <style>",
+ " #p-green {",
+ " color: green;",
+ " }",
+ " #p-red {",
+ " color: red;",
+ " }",
+ " </style>",
+
+ " </head>",
+ " <body>",
+ " </body>",
+ "</html>",
+ ].join("\n")
+ );
+
+add_task(async function () {
+ info("Test that inline stylesheets are numbered correctly");
+ const { ui } = await openStyleEditorForURL(DOCUMENT_WITH_LONG_SHEET);
+
+ is(ui.editors.length, 4, "4 editors present.");
+
+ const firstEditor = ui.editors[0];
+ is(
+ firstEditor.styleSheetFriendlyIndex,
+ 0,
+ "1st inline stylesheet's index is 0"
+ );
+
+ is(
+ firstEditor.styleSheet.styleSheetIndex,
+ 0,
+ "1st inline stylesheet is also the first stylesheet declared"
+ );
+
+ is(firstEditor.styleSheet.ruleCount, 1, "1st inline stylesheet has 1 rule");
+
+ const secondEditor = ui.editors[3];
+ is(
+ secondEditor.styleSheetFriendlyIndex,
+ 1,
+ "2nd inline stylesheet's index is 1"
+ );
+
+ is(
+ secondEditor.styleSheet.styleSheetIndex,
+ 3,
+ "2nd inline stylesheet is the last stylesheet"
+ );
+
+ is(secondEditor.styleSheet.ruleCount, 2, "2nd inline stylesheet has 2 rules");
+});
+
+add_task(async function () {
+ info("Test that user-created stylesheets are numbered correctly");
+ const { panel, ui } = await openStyleEditorForURL(DOCUMENT_WITH_LONG_SHEET);
+ await createNewStyleSheet(ui, panel.panelWindow);
+ await createNewStyleSheet(ui, panel.panelWindow);
+
+ is(ui.editors.length, 6, "6 editors present.");
+
+ ok(ui.editors[4].isNew, "2nd to last editor is user-created");
+ is(
+ ui.editors[4].styleSheetFriendlyIndex,
+ 0,
+ "2nd to last user created stylesheet's index is 0"
+ );
+
+ ok(ui.editors[5].isNew, "Last editor is user-created");
+ is(
+ ui.editors[5].styleSheetFriendlyIndex,
+ 1,
+ "Last user created stylesheet's index is 1"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_1405342_serviceworker_iframes.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_1405342_serviceworker_iframes.js
new file mode 100644
index 0000000000..cccd92fedd
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_1405342_serviceworker_iframes.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that sheets inside cross origin iframes, served from a service worker
+// are correctly fetched via the service worker in the stylesheet editor.
+
+add_task(async function () {
+ const TEST_URL =
+ "https://test1.example.com/browser/devtools/client/styleeditor/test/bug_1405342_serviceworker_iframes.html";
+ const { ui } = await openStyleEditorForURL(TEST_URL);
+
+ if (ui.editors.length != 1) {
+ info("Stylesheet isn't available immediately, waiting for it");
+ await ui.once("editor-added");
+ }
+ is(ui.editors.length, 1, "Got the iframe stylesheet");
+
+ await ui.selectStyleSheet(ui.editors[0].styleSheet);
+ const editor = await ui.editors[0].getSourceEditor();
+ const text = editor.sourceEditor.getText();
+ is(
+ text,
+ "* { color: green; }",
+ "stylesheet content is the one served by the service worker"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js
new file mode 100644
index 0000000000..9eaf0be0f2
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_740541_iframes.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that sheets inside iframes are shown in the editor.
+
+add_task(async function () {
+ function makeStylesheet(selector) {
+ return (
+ "data:text/css;charset=UTF-8," + encodeURIComponent(selector + " { }")
+ );
+ }
+
+ function makeDocument(stylesheets, framedDocuments) {
+ stylesheets = stylesheets || [];
+ framedDocuments = framedDocuments || [];
+ return (
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ Array.prototype.concat
+ .call(
+ [
+ "<!DOCTYPE html>",
+ "<html>",
+ "<head>",
+ "<title>Bug 740541</title>",
+ ],
+ stylesheets.map(function (sheet) {
+ return (
+ '<link rel="stylesheet" type="text/css" href="' + sheet + '">'
+ );
+ }),
+ ["</head>", "<body>"],
+ framedDocuments.map(function (doc) {
+ return '<iframe src="' + doc + '"></iframe>';
+ }),
+ ["</body>", "</html>"]
+ )
+ .join("\n")
+ )
+ );
+ }
+
+ const DOCUMENT_WITH_INLINE_STYLE =
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ [
+ "<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Bug 740541</title>",
+ ' <style type="text/css">',
+ " .something {",
+ " }",
+ " </style>",
+ " </head>",
+ " <body>",
+ " </body>",
+ " </html>",
+ ].join("\n")
+ );
+
+ const FOUR = TEST_BASE_HTTP + "four.html";
+
+ const SIMPLE = TEST_BASE_HTTP + "simple.css";
+
+ const SIMPLE_DOCUMENT = TEST_BASE_HTTP + "simple.html";
+
+ const TESTCASE_URI = makeDocument(
+ [makeStylesheet(".a")],
+ [
+ makeDocument([], [FOUR, DOCUMENT_WITH_INLINE_STYLE]),
+ makeDocument(
+ [makeStylesheet(".b"), SIMPLE],
+ [makeDocument([makeStylesheet(".c")], [])]
+ ),
+ makeDocument([SIMPLE], []),
+ SIMPLE_DOCUMENT,
+ ]
+ );
+
+ const EXPECTED_STYLE_SHEET_COUNT = 12;
+
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(
+ ui.editors.length,
+ EXPECTED_STYLE_SHEET_COUNT,
+ "Got the expected number of style sheets."
+ );
+
+ // Verify that stylesheets are removed when their related target is destroyed
+ if (isFissionEnabled() || isEveryFrameTargetEnabled()) {
+ info("Remove all iframes");
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const iframes = content.document.querySelectorAll("iframe");
+ for (const iframe of iframes) {
+ iframe.remove();
+ }
+ });
+
+ await waitFor(
+ () => ui.editors.length == 1,
+ "Wait until all iframe stylesheets are removed and we only have the top document one"
+ );
+ }
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
new file mode 100644
index 0000000000..ed0d5838b2
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_851132_middle_click.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that middle click on style sheet doesn't open index.xhtml in a new
+// tab (bug 851132).
+
+const TESTCASE_URI = TEST_BASE_HTTP + "four.html";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+ gBrowser.tabContainer.addEventListener("TabOpen", onTabAdded);
+
+ await ui.editors[0].getSourceEditor();
+ info("first editor selected");
+
+ await waitFor(
+ () => ui.editors[0].sourceEditor.hasFocus(),
+ "Wait until the initially selected editor grabs the focus"
+ );
+
+ info("Left-clicking on the second editor link.");
+ await clickOnStyleSheetLink(ui.editors[1], 0);
+
+ info("Waiting for the second editor to be selected.");
+ const editor = await ui.once("editor-selected");
+
+ ok(
+ editor.sourceEditor.hasFocus(),
+ "Left mouse click gave second editor focus."
+ );
+
+ // middle mouse click should not open a new tab
+ info("Middle clicking on the third editor link.");
+ await clickOnStyleSheetLink(ui.editors[2], 1);
+});
+
+/**
+ * A helper that clicks on style sheet link in the sidebar.
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor of which link should be clicked.
+ * @param {MouseEvent.button} button
+ * The button to click the link with.
+ */
+async function clickOnStyleSheetLink(editor, button) {
+ const window = editor._window;
+ const link = editor.summary.querySelector(".stylesheet-name");
+
+ info("Waiting for focus.");
+ await SimpleTest.promiseFocus(window);
+
+ info("Pressing button " + button + " on style sheet name link.");
+ EventUtils.synthesizeMouseAtCenter(link, { button }, window);
+}
+
+function onTabAdded() {
+ ok(false, "middle mouse click has opened a new tab");
+}
+
+registerCleanupFunction(function () {
+ gBrowser.tabContainer.removeEventListener("TabOpen", onTabAdded);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js b/devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js
new file mode 100644
index 0000000000..926048223d
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_bug_870339.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const SIMPLE = TEST_BASE_HTTP + "simple.css";
+const DOCUMENT_WITH_ONE_STYLESHEET =
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ [
+ "<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Bug 870339</title>",
+ ' <link rel="stylesheet" type="text/css" href="' + SIMPLE + '">',
+ " </head>",
+ " <body>",
+ " </body>",
+ "</html>",
+ ].join("\n")
+ );
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(DOCUMENT_WITH_ONE_STYLESHEET);
+
+ // Spam the "devtools.source-map.client-service.enabled" pref observer callback (#onOrigSourcesPrefChanged)
+ // multiple times before the StyleEditorActor has a chance to respond to the first one.
+ const SPAM_COUNT = 2;
+ let prefValue = false;
+ for (let i = 0; i < SPAM_COUNT; ++i) {
+ pushPref("devtools.source-map.client-service.enabled", prefValue);
+ prefValue = !prefValue;
+ }
+
+ // Wait for the StyleEditorActor to respond to each pref changes.
+ await new Promise(resolve => {
+ let loadCount = 0;
+ ui.on("stylesheets-refreshed", function onReset() {
+ ++loadCount;
+ if (loadCount == SPAM_COUNT) {
+ ui.off("stylesheets-refreshed", onReset);
+ // No matter how large SPAM_COUNT is, the number of style
+ // sheets should never be more than the number of style sheets
+ // in the document.
+ is(ui.editors.length, 1, "correct style sheet count");
+ resolve();
+ }
+ });
+ });
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_copyurl.js b/devtools/client/styleeditor/test/browser_styleeditor_copyurl.js
new file mode 100644
index 0000000000..8b2919a173
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_copyurl.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test to check the 'Copy URL' functionality in the context menu item for stylesheets.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ const doc = panel.panelWindow.document;
+ const contextMenu = getContextMenuElement(panel);
+ const copyUrlItem = doc.getElementById("context-copyurl");
+
+ const onContextMenuShown = new Promise(resolve => {
+ contextMenu.addEventListener("popupshown", resolve, { once: true });
+ });
+
+ info("Right-click the first stylesheet editor.");
+ const editor = ui.editors[0];
+
+ is(editor.friendlyName, "simple.css", "editor is the expected one");
+
+ const stylesheetEl = editor.summary.querySelector(".stylesheet-name");
+ await EventUtils.synthesizeMouseAtCenter(
+ stylesheetEl,
+ { button: 2, type: "contextmenu" },
+ panel.panelWindow
+ );
+ await onContextMenuShown;
+
+ ok(!copyUrlItem.hidden, "Copy URL menu item should be showing.");
+
+ info(
+ "Click on Copy URL menu item and wait for the URL to be copied to the clipboard."
+ );
+ await waitForClipboardPromise(
+ () => contextMenu.activateItem(copyUrlItem),
+ `${TEST_BASE_HTTPS}simple.css`
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_enabled.js b/devtools/client/styleeditor/test/browser_styleeditor_enabled.js
new file mode 100644
index 0000000000..7a902d2634
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_enabled.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that style sheets can be disabled and enabled.
+
+// https rather than chrome to improve coverage
+const SIMPLE_URI = TEST_BASE_HTTPS + "simple.html";
+const LONGNAME_URI = TEST_BASE_HTTPS + "longname.html";
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(SIMPLE_URI);
+ const editor = await ui.editors[0].getSourceEditor();
+
+ const summary = editor.summary;
+ const stylesheetToggle = summary.querySelector(".stylesheet-toggle");
+ ok(stylesheetToggle, "stylesheet toggle button exists");
+
+ is(
+ editor.styleSheet.disabled,
+ false,
+ "first stylesheet is initially enabled"
+ );
+
+ is(
+ summary.classList.contains("disabled"),
+ false,
+ "first stylesheet is initially enabled, UI does not have DISABLED class"
+ );
+
+ info("Disabling the first stylesheet.");
+ await toggleEnabled(editor, stylesheetToggle, panel.panelWindow);
+
+ is(editor.styleSheet.disabled, true, "first stylesheet is now disabled");
+ is(
+ summary.classList.contains("disabled"),
+ true,
+ "first stylesheet is now disabled, UI has DISABLED class"
+ );
+
+ info("Enabling the first stylesheet again.");
+ await toggleEnabled(editor, stylesheetToggle, panel.panelWindow);
+
+ is(
+ editor.styleSheet.disabled,
+ false,
+ "first stylesheet is now enabled again"
+ );
+ is(
+ summary.classList.contains("disabled"),
+ false,
+ "first stylesheet is now enabled again, UI does not have DISABLED class"
+ );
+});
+
+// Check that stylesheets with long names do not prevent the toggle button
+// from being visible.
+add_task(async function testLongNameStylesheet() {
+ const { ui } = await openStyleEditorForURL(LONGNAME_URI);
+
+ is(ui.editors.length, 2, "Expected 2 stylesheet editors");
+
+ // Test that the first editor, which should have a stylesheet with a short
+ // name.
+ let editor = ui.editors[0];
+ let stylesheetToggle = editor.summary.querySelector(".stylesheet-toggle");
+ is(editor.friendlyName, "simple.css");
+ ok(stylesheetToggle, "stylesheet toggle button exists");
+ Assert.greater(stylesheetToggle.getBoundingClientRect().width, 0);
+ Assert.greater(stylesheetToggle.getBoundingClientRect().height, 0);
+
+ const expectedWidth = stylesheetToggle.getBoundingClientRect().width;
+ const expectedHeight = stylesheetToggle.getBoundingClientRect().height;
+
+ // Test that the second editor, which should have a stylesheet with a long
+ // name.
+ editor = ui.editors[1];
+ stylesheetToggle = editor.summary.querySelector(".stylesheet-toggle");
+ is(editor.friendlyName, "veryveryverylongnamethatcanbreakthestyleeditor.css");
+ ok(stylesheetToggle, "stylesheet toggle button exists");
+ is(stylesheetToggle.getBoundingClientRect().width, expectedWidth);
+ is(stylesheetToggle.getBoundingClientRect().height, expectedHeight);
+});
+
+add_task(async function testSystemStylesheet() {
+ const { ui } = await openStyleEditorForURL("about:support");
+
+ const aboutSupportEditor = ui.editors.find(
+ editor => editor.friendlyName === "aboutSupport.css"
+ );
+ ok(!!aboutSupportEditor, "Found the editor for aboutSupport.css");
+ const aboutSupportToggle =
+ aboutSupportEditor.summary.querySelector(".stylesheet-toggle");
+ ok(aboutSupportToggle, "enabled toggle button exists");
+ ok(!aboutSupportToggle.disabled, "enabled toggle button is not disabled");
+ is(
+ aboutSupportToggle.getAttribute("tooltiptext"),
+ "Toggle style sheet visibility"
+ );
+
+ const formsEditor = ui.editors.find(
+ editor => editor.friendlyName === "forms.css"
+ );
+ ok(!!formsEditor, "Found the editor for forms.css");
+ const formsToggle = formsEditor.summary.querySelector(".stylesheet-toggle");
+ ok(formsToggle, "enabled toggle button exists");
+ ok(formsToggle.disabled, "enabled toggle button is disabled");
+ // For some unexplained reason, this is updated asynchronously
+ await waitFor(
+ () =>
+ formsToggle.getAttribute("tooltiptext") ==
+ "System style sheets can’t be disabled"
+ );
+ is(
+ formsToggle.getAttribute("tooltiptext"),
+ "System style sheets can’t be disabled"
+ );
+});
+
+async function toggleEnabled(editor, stylesheetToggle, panelWindow) {
+ const changed = editor.once("property-change");
+
+ info("Waiting for focus.");
+ await SimpleTest.promiseFocus(panelWindow);
+
+ info("Clicking on the toggle.");
+ EventUtils.synthesizeMouseAtCenter(stylesheetToggle, {}, panelWindow);
+
+ info("Waiting for stylesheet to be disabled.");
+ let property = await changed;
+ while (property !== "disabled") {
+ info("Ignoring property-change for '" + property + "'.");
+ property = await editor.once("property-change");
+ }
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-netmonitor.js b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-netmonitor.js
new file mode 100644
index 0000000000..7169de8bf5
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fetch-from-netmonitor.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// A test to ensure Style Editor only issues 1 request for each stylesheet (instead of 2)
+// by using the cache on the platform.
+
+const EMPTY_TEST_URL = TEST_BASE_HTTPS + "doc_empty.html";
+const TEST_URL = TEST_BASE_HTTPS + "doc_fetch_from_netmonitor.html";
+
+add_task(async function () {
+ info("Opening netmonitor");
+ // Navigate first to an empty document in order to:
+ // * avoid introducing a cross process navigation when calling navigateTo()
+ // * properly wait for request updates when calling navigateTo, while showToolbox
+ // won't necessarily wait for all pending requests. (If we were loading TEST_URL
+ // in the tab, we might have pending updates in the netmonitor which won't be
+ // awaited for by showToolbox)
+ const tab = await addTab(EMPTY_TEST_URL);
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "netmonitor",
+ });
+ const monitor = toolbox.getPanel("netmonitor");
+ const { store, windowRequire } = monitor.panelWin;
+ const Actions = windowRequire("devtools/client/netmonitor/src/actions/index");
+ const { getSortedRequests } = windowRequire(
+ "devtools/client/netmonitor/src/selectors/index"
+ );
+
+ store.dispatch(Actions.batchEnable(false));
+
+ info("Navigating to test page");
+ await navigateTo(TEST_URL);
+
+ info("Opening Style Editor");
+ const styleeditor = await toolbox.selectTool("styleeditor");
+ const ui = styleeditor.UI;
+
+ info("Waiting for the sources to be loaded.");
+ await ui.editors[0].getSourceEditor();
+ await ui.selectStyleSheet(ui.editors[1].styleSheet);
+ await ui.editors[1].getSourceEditor();
+
+ // Wait till there is 4 requests in Netmonitor store.
+ await waitUntil(() => getSortedRequests(store.getState()).length == 4);
+
+ info("Checking Netmonitor contents.");
+ const shortRequests = [];
+ const longRequests = [];
+ const hugeRequests = [];
+ for (const item of getSortedRequests(store.getState())) {
+ if (item.url.endsWith("doc_short_string.css")) {
+ shortRequests.push(item);
+ }
+ if (item.url.endsWith("doc_long_string.css")) {
+ longRequests.push(item);
+ }
+ if (item.url.endsWith("sjs_huge-css-server.sjs")) {
+ hugeRequests.push(item);
+ }
+ }
+
+ is(
+ shortRequests.length,
+ 1,
+ "Got one request for doc_short_string.css after Style Editor was loaded."
+ );
+ is(
+ longRequests.length,
+ 1,
+ "Got one request for doc_long_string.css after Style Editor was loaded."
+ );
+
+ is(
+ hugeRequests.length,
+ 1,
+ "Got one requests for sjs_huge-css-server.sjs after Style Editor was loaded."
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_filesave.js b/devtools/client/styleeditor/test/browser_styleeditor_filesave.js
new file mode 100644
index 0000000000..d59137af25
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_filesave.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that 'Save' function works.
+
+const TESTCASE_URI_HTML = TEST_BASE_HTTP + "simple.html";
+const TESTCASE_URI_CSS = TEST_BASE_HTTP + "simple.css";
+
+add_task(async function () {
+ const htmlFile = await copy(TESTCASE_URI_HTML, "simple.html");
+ await copy(TESTCASE_URI_CSS, "simple.css");
+ const uri = Services.io.newFileURI(htmlFile);
+ const filePath = uri.resolve("");
+
+ const { ui } = await openStyleEditorForURL(filePath);
+
+ const editor = ui.editors[0];
+ await editor.getSourceEditor();
+
+ info("Editing the style sheet.");
+ let dirty = editor.sourceEditor.once("dirty-change");
+ const beginCursor = { line: 0, ch: 0 };
+ editor.sourceEditor.replaceText("DIRTY TEXT", beginCursor, beginCursor);
+
+ await dirty;
+
+ is(editor.sourceEditor.isClean(), false, "Editor is dirty.");
+ ok(
+ editor.summary.classList.contains("unsaved"),
+ "Star icon is present in the corresponding summary."
+ );
+
+ info("Saving the changes.");
+ dirty = editor.sourceEditor.once("dirty-change");
+
+ editor.saveToFile(null, function (file) {
+ ok(file, "file should get saved directly when using a file:// URI");
+ });
+
+ await dirty;
+
+ is(editor.sourceEditor.isClean(), true, "Editor is clean.");
+ ok(
+ !editor.summary.classList.contains("unsaved"),
+ "Star icon is not present in the corresponding summary."
+ );
+});
+
+function copy(srcChromeURL, destFileName) {
+ return new Promise(resolve => {
+ const destFile = new FileUtils.File(
+ PathUtils.join(PathUtils.profileDir, destFileName)
+ );
+ write(read(srcChromeURL), destFile, resolve);
+ });
+}
+
+function read(srcChromeURL) {
+ const scriptableStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].getService(Ci.nsIScriptableInputStream);
+
+ const channel = NetUtil.newChannel({
+ uri: srcChromeURL,
+ loadUsingSystemPrincipal: true,
+ });
+ const input = channel.open();
+ scriptableStream.init(input);
+
+ let data = "";
+ while (input.available()) {
+ data = data.concat(scriptableStream.read(input.available()));
+ }
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+function write(data, file, callback) {
+ const istream = getInputStream(data);
+ const ostream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(istream, ostream, function (status) {
+ if (!Components.isSuccessCode(status)) {
+ info("Couldn't write to " + file.path);
+ return;
+ }
+
+ callback(file);
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_filter.js b/devtools/client/styleeditor/test/browser_styleeditor_filter.js
new file mode 100644
index 0000000000..c8472b5f40
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_filter.js
@@ -0,0 +1,343 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+requestLongerTimeout(2);
+
+// Check that the stylesheets list can be filtered
+
+const INITIAL_INLINE_STYLE_SHEETS_COUNT = 100;
+
+const TEST_URI =
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ `
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <title>Test filter</title>
+ <link rel="stylesheet" type="text/css" href="${TEST_BASE_HTTPS}simple.css">
+ ${Array.from({ length: INITIAL_INLINE_STYLE_SHEETS_COUNT })
+ .map((_, i) => `<style>/* inline ${i} */</style>`)
+ .join("\n")}
+ <link rel="stylesheet" type="text/css" href="${TEST_BASE_HTTPS}pretty.css">
+ </head>
+ <body>
+ </body>
+ </html>
+ `
+ );
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(TEST_URI);
+ const { panelWindow } = panel;
+ is(
+ ui.editors.length,
+ INITIAL_INLINE_STYLE_SHEETS_COUNT + 2,
+ "correct number of editors"
+ );
+
+ const doc = panel.panelWindow.document;
+
+ const filterInput = doc.querySelector(".devtools-filterinput");
+ const filterInputClearButton = doc.querySelector(
+ ".devtools-searchinput-clear"
+ );
+ ok(filterInput, "There's a filter input");
+ ok(filterInputClearButton, "There's a clear button next to the filter input");
+ ok(
+ filterInputClearButton.hasAttribute("hidden"),
+ "The clear button is hidden by default"
+ );
+
+ const setFilterInputValue = value => {
+ // The keyboard shortcut focuses the input and select its content, so we should
+ // be able to type right-away.
+ synthesizeKeyShortcut("CmdOrCtrl+P");
+ EventUtils.sendString(value);
+ };
+
+ info(
+ "Check that the list can be filtered with the stylesheet name, regardless of the casing"
+ );
+ let onEditorSelected = ui.once("editor-selected");
+ setFilterInputValue("PREttY");
+ ok(
+ !filterInputClearButton.hasAttribute("hidden"),
+ "The clear button is visible when the input isn't empty"
+ );
+ Assert.deepEqual(
+ getVisibleStyleSheetsNames(doc),
+ ["pretty.css"],
+ "Only pretty.css is now displayed"
+ );
+
+ await onEditorSelected;
+ is(
+ ui.selectedEditor,
+ ui.editors.at(-1),
+ "When the selected stylesheet is filtered out, the first visible one gets selected"
+ );
+ is(
+ filterInput.ownerGlobal.document.activeElement,
+ filterInput,
+ "Even when a stylesheet was automatically opened, the filter input is still focused"
+ );
+ ok(!ui.selectedEditor.sourceEditor.hasFocus(), "Editor doesn't have focus.");
+
+ info(
+ "Clicking on the clear button should clear the input and unfilter the list"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ filterInputClearButton,
+ {},
+ panel.panelWindow
+ );
+ is(filterInput.value, "", "input was cleared");
+ ok(!isListFiltered(doc), "List isn't filtered anymore");
+ ok(
+ filterInputClearButton.hasAttribute("hidden"),
+ "The clear button is hidden after clicking on it"
+ );
+
+ info("Check that the list can be filtered with name-less stylesheets");
+ onEditorSelected = ui.once("editor-selected");
+ setFilterInputValue("#1");
+ Assert.deepEqual(
+ getVisibleStyleSheetsNames(doc),
+ [
+ "<inline style sheet #1>",
+ "<inline style sheet #10>",
+ "<inline style sheet #11>",
+ "<inline style sheet #12>",
+ "<inline style sheet #13>",
+ "<inline style sheet #14>",
+ "<inline style sheet #15>",
+ "<inline style sheet #16>",
+ "<inline style sheet #17>",
+ "<inline style sheet #18>",
+ "<inline style sheet #19>",
+ "<inline style sheet #100>",
+ ],
+ `List is showing inline stylesheets whose index start with "1"`
+ );
+ await onEditorSelected;
+ is(
+ ui.selectedEditor,
+ ui.editors[1],
+ "The first visible stylesheet got selected"
+ );
+
+ info("Check that keyboard navigation still works when the list is filtered");
+ // Move focus out of the input
+ EventUtils.synthesizeKey("VK_TAB", {}, panelWindow);
+ EventUtils.synthesizeKey("VK_DOWN", {}, panelWindow);
+
+ is(
+ panelWindow.document.activeElement.childNodes[0].value,
+ "<inline style sheet #1>",
+ "focus is on first inline stylesheet"
+ );
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, panelWindow);
+ is(
+ panelWindow.document.activeElement.childNodes[0].value,
+ "<inline style sheet #10>",
+ "focus is on inline stylesheet #10"
+ );
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, panelWindow);
+ is(
+ panelWindow.document.activeElement.childNodes[0].value,
+ "<inline style sheet #11>",
+ "focus is on inline stylesheet #11"
+ );
+
+ info(
+ "Check that when stylesheets are added in the page, they respect the filter state"
+ );
+ let onEditorAdded = ui.once("editor-added");
+ // Adding an inline stylesheet that will match the search
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ const document = content.document;
+ const style = document.createElement("style");
+ style.appendChild(document.createTextNode(`/* inline 101 */`));
+ document.head.appendChild(style);
+ });
+ await onEditorAdded;
+ ok(
+ getVisibleStyleSheetsNames(doc).includes("<inline style sheet #101>"),
+ "New inline stylesheet is visible as it matches the search"
+ );
+
+ // Adding a stylesheet that won't match the search
+ onEditorAdded = ui.once("editor-added");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [TEST_BASE_HTTPS],
+ baseUrl => {
+ const document = content.document;
+ const link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("type", "text/css");
+ link.setAttribute("href", `${baseUrl}doc_short_string.css`);
+ document.head.appendChild(link);
+ }
+ );
+ await onEditorAdded;
+
+ ok(
+ !getVisibleStyleSheetsNames(doc).includes("doc_short_string.css"),
+ "doc_short_string.css is not visible as its name does not match the search"
+ );
+
+ info(
+ "Check that clicking on the Add New Stylesheet button clears the list and show the stylesheet"
+ );
+ onEditorAdded = ui.once("editor-added");
+ await createNewStyleSheet(ui, panel.panelWindow);
+ is(filterInput.value, "", "Filter input was cleared");
+
+ ok(!isListFiltered(doc), "List is not filtered anymore");
+ is(ui.selectedEditor, ui.editors.at(-1), "The new stylesheet got selected");
+
+ info(
+ "Check that when no stylesheet matches the search, a class is added to the nav"
+ );
+ setFilterInputValue("sync_with_csp");
+ ok(navHasAllFilteredClass(panel), `"splitview-all-filtered" was added`);
+ ok(
+ filterInput
+ .closest(".devtools-searchbox")
+ .classList.contains("devtools-searchbox-no-match"),
+ `The searchbox has the "devtools-searchbox-no-match" class`
+ );
+
+ info(
+ "Check that adding a stylesheet matching the search remove the splitview-all-filtered class"
+ );
+ onEditorAdded = ui.once("editor-added");
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [TEST_BASE_HTTPS],
+ baseUrl => {
+ const document = content.document;
+ const link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("type", "text/css");
+ link.setAttribute("href", `${baseUrl}sync_with_csp.css`);
+ document.head.appendChild(link);
+ }
+ );
+ await onEditorAdded;
+ ok(!navHasAllFilteredClass(panel), `"splitview-all-filtered" was removed`);
+ ok(
+ !filterInput
+ .closest(".devtools-searchbox")
+ .classList.contains("devtools-searchbox-no-match"),
+ `The searchbox does not have the "devtools-searchbox-no-match" class anymore`
+ );
+
+ info(
+ "Check that reloading the page when the filter don't match anything won't select anything"
+ );
+ setFilterInputValue("XXXDONTMATCHANYTHING");
+ ok(navHasAllFilteredClass(panel), `"splitview-all-filtered" was added`);
+ await reloadPageAndWaitForStyleSheets(
+ ui,
+ INITIAL_INLINE_STYLE_SHEETS_COUNT + 2
+ );
+ ok(
+ navHasAllFilteredClass(panel),
+ `"splitview-all-filtered" is still applied`
+ );
+ is(getVisibleStyleSheets(doc).length, 0, "No stylesheets are displayed");
+ is(ui.selectedEditor, null, "No editor was selected");
+
+ info(
+ "Check that reloading the page when the filter was matching elements keep the same state"
+ );
+ onEditorSelected = ui.once("editor-selected");
+ setFilterInputValue("pretty");
+ await onEditorSelected;
+ Assert.deepEqual(
+ getVisibleStyleSheetsNames(doc),
+ ["pretty.css"],
+ "Only pretty.css is now displayed"
+ );
+
+ onEditorSelected = ui.once("editor-selected");
+ await reloadPageAndWaitForStyleSheets(
+ ui,
+ INITIAL_INLINE_STYLE_SHEETS_COUNT + 2
+ );
+ await onEditorSelected;
+ Assert.deepEqual(
+ getVisibleStyleSheetsNames(doc),
+ ["pretty.css"],
+ "pretty.css is still the only stylesheet displayed"
+ );
+ is(
+ ui.selectedEditor.friendlyName,
+ "pretty.css",
+ "pretty.css editor is active"
+ );
+
+ info("Check that clearing the input does show all the stylesheets");
+ EventUtils.synthesizeMouseAtCenter(
+ filterInputClearButton,
+ {},
+ panel.panelWindow
+ );
+ ok(!isListFiltered(doc), "List is not filtered anymore");
+ is(
+ ui.selectedEditor.friendlyName,
+ "pretty.css",
+ "pretty.css editor is still active"
+ );
+});
+
+/**
+ * @param {StyleEditorPanel} panel
+ * @returns Boolean
+ */
+function navHasAllFilteredClass(panel) {
+ return panel.panelWindow.document
+ .querySelector(".splitview-nav")
+ .classList.contains("splitview-all-filtered");
+}
+
+/**
+ * Returns true if there's at least one stylesheet filtered out
+ *
+ * @param {Document} doc: StyleEditor document
+ * @returns Boolean
+ */
+function isListFiltered(doc) {
+ return !!doc.querySelectorAll("ol > li.splitview-filtered").length;
+}
+
+/**
+ * Returns the list of stylesheet list elements.
+ *
+ * @param {Document} doc: StyleEditor document
+ * @returns Array<Element>
+ */
+function getVisibleStyleSheets(doc) {
+ return Array.from(
+ doc.querySelectorAll(
+ "ol > li:not(.splitview-filtered) .stylesheet-name label"
+ )
+ );
+}
+
+/**
+ * Returns the list of stylesheet names visible in the style editor list.
+ *
+ * @param {Document} doc: StyleEditor document
+ * @returns Array<String>
+ */
+function getVisibleStyleSheetsNames(doc) {
+ return getVisibleStyleSheets(doc).map(label => label.value);
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_fission_switch_target.js b/devtools/client/styleeditor/test/browser_styleeditor_fission_switch_target.js
new file mode 100644
index 0000000000..3f5bf34fd1
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_fission_switch_target.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test switching for the top-level target.
+
+const PARENT_PROCESS_URI = "about:robots";
+const CONTENT_PROCESS_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(async function () {
+ // We use about:robots, because this page will run in the parent process.
+ // Navigating from about:robots to a regular content page will always trigger a target
+ // switch, with or without fission.
+
+ info("Open a page that runs in the parent process");
+ const { ui } = await openStyleEditorForURL(PARENT_PROCESS_URI);
+ await waitUntil(() => ui.editors.length === 7);
+ ok(true, `Seven style sheets for ${PARENT_PROCESS_URI}`);
+
+ info("Navigate to a page that runs in the child process");
+ await navigateToAndWaitForStyleSheets(CONTENT_PROCESS_URI, ui, 2);
+ // We also have to wait for the toolbox to complete the target switching
+ // in order to avoid pending requests during test teardown.
+ ok(
+ ui.editors.every(
+ editor => editor._resource.nodeHref == CONTENT_PROCESS_URI
+ ),
+ `Two sheets present for ${CONTENT_PROCESS_URI}`
+ );
+
+ info("Wait until the editor is ready");
+ await waitFor(() => ui.selectedEditor?.sourceEditor);
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js b/devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js
new file mode 100644
index 0000000000..a5ee01e438
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_highlight-selector.js
@@ -0,0 +1,223 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that hovering over a simple selector in the style-editor requests the
+// highlighting of the corresponding nodes, even in remote iframes.
+
+const REMOTE_IFRAME_URL = `https://example.org/document-builder.sjs?html=
+ <style>h2{color:cyan}</style>
+ <h2>highlighter test</h2>`;
+const TOP_LEVEL_URL = `https://example.com/document-builder.sjs?html=
+ <style>h1{color:red}</style>
+ <h1>highlighter test</h1>
+ <iframe src='${REMOTE_IFRAME_URL}'></iframe>`;
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TOP_LEVEL_URL);
+
+ info(
+ "Wait until both stylesheet are loaded and ready to handle mouse events"
+ );
+ await waitFor(() => ui.editors.length == 2);
+ const topLevelStylesheetEditor = ui.editors.find(e =>
+ e._resource.nodeHref.startsWith("https://example.com")
+ );
+ const iframeStylesheetEditor = ui.editors.find(e =>
+ e._resource.nodeHref.startsWith("https://example.org")
+ );
+
+ await ui.selectStyleSheet(topLevelStylesheetEditor.styleSheet);
+ await waitFor(() => topLevelStylesheetEditor.highlighter);
+
+ info("Check that highlighting works on the top-level document");
+ const topLevelHighlighterTestFront =
+ await topLevelStylesheetEditor._resource.targetFront.getFront(
+ "highlighterTest"
+ );
+ topLevelHighlighterTestFront.highlighter =
+ topLevelStylesheetEditor.highlighter;
+
+ info("Expecting a node-highlighted event");
+ let onHighlighted = topLevelStylesheetEditor.once("node-highlighted");
+
+ info("Simulate a mousemove event on the h1 selector");
+ // mousemove event listeners is set on editor.sourceEditor, which is not defined right away.
+ await waitFor(() => !!topLevelStylesheetEditor.sourceEditor);
+ let selectorEl = querySelectorCodeMirrorCssRuleSelectorToken(
+ topLevelStylesheetEditor
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ selectorEl,
+ { type: "mousemove" },
+ selectorEl.ownerDocument.defaultView
+ );
+ await onHighlighted;
+ ok(
+ await topLevelHighlighterTestFront.isNodeRectHighlighted(
+ await getElementNodeRectWithinTarget(["h1"])
+ ),
+ "The highlighter's outline corresponds to the h1 node"
+ );
+
+ info(
+ "Simulate a mousemove event on the property name to hide the highlighter"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ querySelectorCodeMirrorCssPropertyNameToken(topLevelStylesheetEditor),
+ { type: "mousemove" },
+ selectorEl.ownerDocument.defaultView
+ );
+
+ await waitFor(async () => !topLevelStylesheetEditor.highlighter.isShown());
+ let isVisible = await topLevelHighlighterTestFront.isHighlighting();
+ is(isVisible, false, "The highlighter is now hidden");
+
+ // It looks like we need to show the same highlighter again to trigger Bug 1847747
+ info("Show and hide the highlighter again");
+ onHighlighted = topLevelStylesheetEditor.once("node-highlighted");
+ EventUtils.synthesizeMouseAtCenter(
+ selectorEl,
+ { type: "mousemove" },
+ selectorEl.ownerDocument.defaultView
+ );
+ await onHighlighted;
+ EventUtils.synthesizeMouseAtCenter(
+ querySelectorCodeMirrorCssPropertyNameToken(topLevelStylesheetEditor),
+ { type: "mousemove" },
+ selectorEl.ownerDocument.defaultView
+ );
+
+ await waitFor(async () => !topLevelStylesheetEditor.highlighter.isShown());
+ // wait for a bit so the style editor would have had the time to process
+ // any unwanted stylesheets
+ await wait(1000);
+ ok(
+ !ui.editors.find(e => e._resource.href?.includes("highlighters.css")),
+ "highlighters.css isn't displayed in StyleEditor"
+ );
+ is(ui.editors.length, 2, "No other stylesheet was displayed");
+
+ info("Check that highlighting works on the iframe document");
+ await ui.selectStyleSheet(iframeStylesheetEditor.styleSheet);
+ await waitFor(() => iframeStylesheetEditor.highlighter);
+
+ const iframeHighlighterTestFront =
+ await iframeStylesheetEditor._resource.targetFront.getFront(
+ "highlighterTest"
+ );
+ iframeHighlighterTestFront.highlighter = iframeStylesheetEditor.highlighter;
+
+ info("Expecting a node-highlighted event");
+ onHighlighted = iframeStylesheetEditor.once("node-highlighted");
+
+ info("Simulate a mousemove event on the h2 selector");
+ // mousemove event listeners is set on editor.sourceEditor, which is not defined right away.
+ await waitFor(() => !!iframeStylesheetEditor.sourceEditor);
+ selectorEl = querySelectorCodeMirrorCssRuleSelectorToken(
+ iframeStylesheetEditor
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ selectorEl,
+ { type: "mousemove" },
+ selectorEl.ownerDocument.defaultView
+ );
+ await onHighlighted;
+
+ isVisible = await iframeHighlighterTestFront.isHighlighting();
+ ok(isVisible, "The highlighter is shown");
+ ok(
+ await iframeHighlighterTestFront.isNodeRectHighlighted(
+ await getElementNodeRectWithinTarget(["iframe", "h2"])
+ ),
+ "The highlighter's outline corresponds to the h2 node"
+ );
+
+ info("Simulate a mousemove event elsewhere in the editor");
+ EventUtils.synthesizeMouseAtCenter(
+ querySelectorCodeMirrorCssPropertyNameToken(iframeStylesheetEditor),
+ { type: "mousemove" },
+ selectorEl.ownerDocument.defaultView
+ );
+
+ await waitFor(async () => !topLevelStylesheetEditor.highlighter.isShown());
+
+ isVisible = await iframeHighlighterTestFront.isHighlighting();
+ is(isVisible, false, "The highlighter is now hidden");
+});
+
+function querySelectorCodeMirrorCssRuleSelectorToken(stylesheetEditor) {
+ // CSS Rules selector (e.g. `h1`) are displayed in a .cm-tag span
+ return querySelectorCodeMirror(stylesheetEditor, ".cm-tag");
+}
+
+function querySelectorCodeMirrorCssPropertyNameToken(stylesheetEditor) {
+ // properties name (e.g. `color`) are displayed in a .cm-property span
+ return querySelectorCodeMirror(stylesheetEditor, ".cm-property");
+}
+
+function querySelectorCodeMirror(stylesheetEditor, selector) {
+ return stylesheetEditor.sourceEditor.codeMirror
+ .getWrapperElement()
+ .querySelector(selector);
+}
+
+/**
+ * Return the bounds of the element matching the selector, relatively to the target bounds
+ * (e.g. if Fission is enabled, it's related to the iframe bound, if Fission is disabled,
+ * it's related to the top level document).
+ *
+ * @param {Array<string>} selectors: Arrays of CSS selectors from the root document to the node.
+ * The last CSS selector of the array is for the node in its frame doc.
+ * The before-last CSS selector is for the frame in its parent frame, etc...
+ * Ex: ["frame.first-frame", ..., "frame.last-frame", ".target-node"]
+ * @returns {Object} with left/top/width/height properties representing the node bounds
+ */
+async function getElementNodeRectWithinTarget(selectors) {
+ // Retrieve the browsing context in which the element is
+ const inBCSelector = selectors.pop();
+ const frameSelectors = selectors;
+ const bc = frameSelectors.length
+ ? await getBrowsingContextInFrames(
+ gBrowser.selectedBrowser.browsingContext,
+ frameSelectors
+ )
+ : gBrowser.selectedBrowser.browsingContext;
+
+ // Get the element bounds within the Firefox window
+ const elementBounds = await SpecialPowers.spawn(
+ bc,
+ [inBCSelector],
+ _selector => {
+ const el = content.document.querySelector(_selector);
+ const { left, top, width, height } = el
+ .getBoxQuadsFromWindowOrigin()[0]
+ .getBounds();
+ return { left, top, width, height };
+ }
+ );
+
+ // Then we need to offset the element bounds from a frame bounds
+ // When fission/EFT is enabled, the highlighter is only shown within the iframe bounds.
+ // So we only need to retrieve the element bounds within the iframe.
+ // Otherwise, we retrieve the top frame bounds
+ const relativeBrowsingContext =
+ isFissionEnabled() || isEveryFrameTargetEnabled()
+ ? bc
+ : gBrowser.selectedBrowser.browsingContext;
+ const relativeDocumentBounds = await SpecialPowers.spawn(
+ relativeBrowsingContext,
+ [],
+ () =>
+ content.document.documentElement
+ .getBoxQuadsFromWindowOrigin()[0]
+ .getBounds()
+ );
+
+ // Adjust the element bounds based on the relative document bounds
+ elementBounds.left = elementBounds.left - relativeDocumentBounds.left;
+ elementBounds.top = elementBounds.top - relativeDocumentBounds.top;
+
+ return elementBounds;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_import.js b/devtools/client/styleeditor/test/browser_styleeditor_import.js
new file mode 100644
index 0000000000..c630dcd508
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_import.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the import button in the UI works.
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+const FILENAME = "styleeditor-import-test.css";
+const SOURCE = "body{background:red;}";
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ const added = ui.once("test:editor-updated");
+ importSheet(ui, panel.panelWindow);
+
+ info("Waiting for editor to be added for the imported sheet.");
+ const editor = await added;
+
+ is(
+ editor.savedFile.leafName,
+ FILENAME,
+ "imported stylesheet will be saved directly into the same file"
+ );
+ is(
+ editor.friendlyName,
+ FILENAME,
+ "imported stylesheet has the same name as the filename"
+ );
+});
+
+function importSheet(ui, panelWindow) {
+ // create file to import first
+ const file = new FileUtils.File(
+ PathUtils.join(PathUtils.profileDir, FILENAME)
+ );
+ const ostream = FileUtils.openSafeFileOutputStream(file);
+ const istream = getInputStream(SOURCE);
+
+ NetUtil.asyncCopy(istream, ostream, function () {
+ FileUtils.closeSafeFileOutputStream(ostream);
+
+ // click the import button now that the file to import is ready
+ ui._mockImportFile = file;
+
+ waitForFocus(function () {
+ const document = panelWindow.document;
+ const importButton = document.querySelector(".style-editor-importButton");
+ ok(importButton, "import button exists");
+
+ EventUtils.synthesizeMouseAtCenter(importButton, {}, panelWindow);
+ }, panelWindow);
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_import_rule.js b/devtools/client/styleeditor/test/browser_styleeditor_import_rule.js
new file mode 100644
index 0000000000..1aba71634c
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_import_rule.js
@@ -0,0 +1,32 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that style editor shows sheets loaded with @import rules.
+
+// http rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "import.html";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 3, "there are 3 stylesheets after loading @imports");
+
+ is(
+ ui.editors[0].styleSheet.href,
+ TEST_BASE_HTTPS + "simple.css",
+ "stylesheet 1 is simple.css"
+ );
+
+ is(
+ ui.editors[1].styleSheet.href,
+ TEST_BASE_HTTPS + "import.css",
+ "stylesheet 2 is import.css"
+ );
+
+ is(
+ ui.editors[2].styleSheet.href,
+ TEST_BASE_HTTPS + "import2.css",
+ "stylesheet 3 is import2.css"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_init.js b/devtools/client/styleeditor/test/browser_styleeditor_init.js
new file mode 100644
index 0000000000..2183495441
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_init.js
@@ -0,0 +1,52 @@
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that style editor contains correct stylesheets after initialization.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+const EXPECTED_SHEETS = [
+ {
+ sheetIndex: 0,
+ name: /^simple.css$/,
+ rules: 1,
+ active: true,
+ },
+ {
+ sheetIndex: 1,
+ name: /^<.*>$/,
+ rules: 3,
+ active: false,
+ },
+];
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "The UI contains two style sheets.");
+ checkSheet(ui.editors[0], EXPECTED_SHEETS[0]);
+ checkSheet(ui.editors[1], EXPECTED_SHEETS[1]);
+});
+
+function checkSheet(editor, expected) {
+ is(
+ editor.styleSheet.styleSheetIndex,
+ expected.sheetIndex,
+ "Style sheet has correct index."
+ );
+
+ const summary = editor.summary;
+ const name = summary
+ .querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+ ok(expected.name.test(name), "The name '" + name + "' is correct.");
+
+ const ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount, 10), expected.rules, "the rule count is correct");
+
+ is(
+ summary.classList.contains("splitview-active"),
+ expected.active,
+ "The active status for this sheet is correct."
+ );
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js b/devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js
new file mode 100644
index 0000000000..0296801701
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_inline_friendly_names.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that inline style sheets get correct names if they are saved to disk and
+// that those names survice a reload but not navigation to another page.
+
+const FIRST_TEST_PAGE = TEST_BASE_HTTPS + "inline-1.html";
+const SECOND_TEST_PAGE = TEST_BASE_HTTPS + "inline-2.html";
+const SAVE_PATH = "test.css";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(FIRST_TEST_PAGE);
+
+ testIndentifierGeneration(ui);
+
+ await saveFirstInlineStyleSheet(ui);
+ await testFriendlyNamesAfterSave(ui);
+ await reloadPageAndWaitForStyleSheets(ui, 2);
+ await testFriendlyNamesAfterSave(ui);
+ await navigateToAndWaitForStyleSheets(SECOND_TEST_PAGE, ui, 2);
+ await testFriendlyNamesAfterNavigation(ui);
+});
+
+function testIndentifierGeneration(ui) {
+ const fakeStyleSheetFile = {
+ href: "http://example.com/test.css",
+ nodeHref: "http://example.com/",
+ styleSheetIndex: 1,
+ };
+
+ const fakeInlineStyleSheet = {
+ href: null,
+ nodeHref: "http://example.com/",
+ styleSheetIndex: 2,
+ };
+
+ is(
+ ui.getStyleSheetIdentifier(fakeStyleSheetFile),
+ "http://example.com/test.css",
+ "URI is the identifier of style sheet file."
+ );
+
+ is(
+ ui.getStyleSheetIdentifier(fakeInlineStyleSheet),
+ "inline-2-at-http://example.com/",
+ "Inline sheets are identified by their page and position in the page."
+ );
+}
+
+function saveFirstInlineStyleSheet(ui) {
+ return new Promise(resolve => {
+ const editor = ui.editors[0];
+ const destFile = new FileUtils.File(
+ PathUtils.join(PathUtils.profileDir, SAVE_PATH)
+ );
+
+ editor.saveToFile(destFile, function (file) {
+ ok(file, "File was correctly saved.");
+ resolve();
+ });
+ });
+}
+
+function testFriendlyNamesAfterSave(ui) {
+ const firstEditor = ui.editors[0];
+ const secondEditor = ui.editors[1];
+
+ // The friendly name of first sheet should've been remembered, the second
+ // should not be the same (bug 969900).
+ is(
+ firstEditor.friendlyName,
+ SAVE_PATH,
+ "Friendly name is correct for the saved inline style sheet."
+ );
+ isnot(
+ secondEditor.friendlyName,
+ SAVE_PATH,
+ "Friendly name for the second inline sheet isn't the same as the first."
+ );
+
+ return Promise.resolve(null);
+}
+
+function testFriendlyNamesAfterNavigation(ui) {
+ const firstEditor = ui.editors[0];
+ const secondEditor = ui.editors[1];
+
+ // Inline style sheets shouldn't have the name of previously saved file as the
+ // page is different.
+ isnot(
+ firstEditor.friendlyName,
+ SAVE_PATH,
+ "The first editor doesn't have the save path as a friendly name."
+ );
+ isnot(
+ secondEditor.friendlyName,
+ SAVE_PATH,
+ "The second editor doesn't have the save path as a friendly name."
+ );
+
+ return Promise.resolve(null);
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_loading.js b/devtools/client/styleeditor/test/browser_styleeditor_loading.js
new file mode 100644
index 0000000000..1321d071be
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_loading.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that style editor loads correctly.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "longload.html";
+
+add_task(async function () {
+ // launch Style Editor right when the tab is created (before load)
+ // this checks that the Style Editor still launches correctly when it is
+ // opened *while* the page is still loading. The Style Editor should not
+ // signal that it is loaded until the accompanying content page is loaded.
+ const tabAdded = addTab(TESTCASE_URI);
+ const tab = gBrowser.selectedTab;
+ const styleEditorLoaded = gDevTools.showToolboxForTab(tab, {
+ toolId: "styleeditor",
+ });
+
+ await Promise.all([tabAdded, styleEditorLoaded]);
+
+ const toolbox = gDevTools.getToolboxForTab(tab);
+ const panel = toolbox.getPanel("styleeditor");
+ const { panelWindow } = panel;
+
+ ok(
+ !getRootElement(panel).classList.contains("loading"),
+ "style editor root element does not have 'loading' class name anymore"
+ );
+
+ let button = panelWindow.document.querySelector(".style-editor-newButton");
+ ok(!button.hasAttribute("disabled"), "new style sheet button is enabled");
+
+ button = panelWindow.document.querySelector(".style-editor-importButton");
+ ok(!button.hasAttribute("disabled"), "import button is enabled");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js b/devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js
new file mode 100644
index 0000000000..7cb0d04c64
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_loading_with_containers.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that the stylesheets can be loaded correctly with containers
+// (bug 1282660).
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+const EXPECTED_SHEETS = [
+ {
+ sheetIndex: 0,
+ name: /^simple.css$/,
+ rules: 1,
+ active: true,
+ },
+ {
+ sheetIndex: 1,
+ name: /^<.*>$/,
+ rules: 3,
+ active: false,
+ },
+];
+
+add_task(async function () {
+ // Using the personal container.
+ const userContextId = 1;
+ const { tab } = await openTabInUserContext(TESTCASE_URI, userContextId);
+ const { ui } = await openStyleEditor(tab);
+
+ is(ui.editors.length, 2, "The UI contains two style sheets.");
+ await checkSheet(ui.editors[0], EXPECTED_SHEETS[0]);
+ await checkSheet(ui.editors[1], EXPECTED_SHEETS[1]);
+});
+
+async function openTabInUserContext(uri, userContextId) {
+ // Open the tab in the correct userContextId.
+ const tab = BrowserTestUtils.addTab(gBrowser, uri, { userContextId });
+
+ // Select tab and make sure its browser is focused.
+ gBrowser.selectedTab = tab;
+ tab.ownerDocument.defaultView.focus();
+
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return { tab, browser };
+}
+
+async function checkSheet(editor, expected) {
+ is(
+ editor.styleSheet.styleSheetIndex,
+ expected.sheetIndex,
+ "Style sheet has correct index."
+ );
+
+ const summary = editor.summary;
+ const name = summary
+ .querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+ ok(expected.name.test(name), "The name '" + name + "' is correct.");
+
+ // The rule count is displayed via l10n.setArgs which only applies the value
+ // asynchronously, so wait for it to be applied.
+ await waitFor(() => {
+ const count = summary.querySelector(".stylesheet-rule-count").textContent;
+ return parseInt(count, 10) === expected.rules;
+ });
+ const ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount, 10), expected.rules, "the rule count is correct");
+
+ is(
+ summary.classList.contains("splitview-active"),
+ expected.active,
+ "The active status for this sheet is correct."
+ );
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
new file mode 100644
index 0000000000..850c9bf686
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_links.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* Tests responsive mode links for
+ * @media sidebar width and height related conditions */
+
+loader.lazyRequireGetter(
+ this,
+ "ResponsiveUIManager",
+ "resource://devtools/client/responsive/manager.js"
+);
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
+const responsiveModeToggleClass = ".media-responsive-mode-toggle";
+
+add_task(async function () {
+ // ensure that original RDM size is big enough so it doesn't match the
+ // media queries by default.
+ await pushPref("devtools.responsive.viewport.width", 1000);
+ await pushPref("devtools.responsive.viewport.height", 1000);
+
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ const editor = ui.editors[1];
+ await openEditor(editor);
+
+ const tab = gBrowser.selectedTab;
+ testNumberOfLinks(editor);
+ await testMediaLink(editor, tab, ui, 2, "width", 550);
+ await testMediaLink(editor, tab, ui, 3, "height", 300);
+
+ const onMediaChange = waitForManyEvents(ui, 1000);
+ await closeRDM(tab);
+
+ info("Wait for at-rules-list-changed events to settle on StyleEditorUI");
+ await onMediaChange;
+ doFinalChecks(editor);
+});
+
+function testNumberOfLinks(editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ const conditions = sidebar.querySelectorAll(".at-rule-condition");
+
+ info("Testing if media rules have the appropriate number of links");
+ ok(
+ !conditions[0].querySelector(responsiveModeToggleClass),
+ "There should be no links in the first media rule."
+ );
+ ok(
+ !conditions[1].querySelector(responsiveModeToggleClass),
+ "There should be no links in the second media rule."
+ );
+ ok(
+ conditions[2].querySelector(responsiveModeToggleClass),
+ "There should be 1 responsive mode link in the media rule"
+ );
+ is(
+ conditions[3].querySelectorAll(responsiveModeToggleClass).length,
+ 2,
+ "There should be 2 responsive mode links in the media rule"
+ );
+}
+
+async function testMediaLink(editor, tab, ui, itemIndex, type, value) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ const conditions = sidebar.querySelectorAll(".at-rule-condition");
+ const onRDMOpened = once(ui, "responsive-mode-opened");
+
+ ok(
+ sidebar
+ .querySelectorAll(".at-rule-condition")
+ [itemIndex].classList.contains("media-condition-unmatched"),
+ `The ${type} condition is not matched when not in responsive mode`
+ );
+
+ info("Launching responsive mode");
+ conditions[itemIndex].querySelector(responsiveModeToggleClass).click();
+ await onRDMOpened;
+ const rdmUI = ResponsiveUIManager.getResponsiveUIForTab(tab);
+ await waitForResizeTo(rdmUI, type, value);
+ rdmUI.transitionsEnabled = false;
+
+ info("Wait for RDM ui to be fully loaded");
+ await waitForRDMLoaded(rdmUI);
+
+ // Ensure that the content has reflowed, which will ensure that all the
+ // element classes are reported correctly.
+ await promiseContentReflow(rdmUI);
+
+ ok(
+ ResponsiveUIManager.isActiveForTab(tab),
+ "Responsive mode should be active."
+ );
+ await waitFor(() => {
+ const el = sidebar.querySelectorAll(".at-rule-condition")[itemIndex];
+ return !el.classList.contains("media-condition-unmatched");
+ });
+ ok(true, "media rule should now be matched after responsive mode is active");
+
+ const dimension = (await getSizing(rdmUI))[type];
+ is(dimension, value, `${type} should be properly set.`);
+}
+
+function doFinalChecks(editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ let conditions = sidebar.querySelectorAll(".at-rule-condition");
+ conditions = sidebar.querySelectorAll(".at-rule-condition");
+ ok(
+ conditions[2].classList.contains("media-condition-unmatched"),
+ "The width condition should now be unmatched"
+ );
+ ok(
+ conditions[3].classList.contains("media-condition-unmatched"),
+ "The height condition should now be unmatched"
+ );
+}
+
+/* Helpers */
+function waitForResizeTo(rdmUI, type, value) {
+ return new Promise(resolve => {
+ const onResize = data => {
+ if (data[type] != value) {
+ return;
+ }
+ rdmUI.off("content-resize", onResize);
+ info(`Got content-resize to a ${type} of ${value}`);
+ resolve();
+ };
+ info(`Waiting for content-resize to a ${type} of ${value}`);
+ rdmUI.on("content-resize", onResize);
+ });
+}
+
+function promiseContentReflow(ui) {
+ return SpecialPowers.spawn(ui.getViewportBrowser(), [], async function () {
+ return new Promise(resolve => {
+ content.window.requestAnimationFrame(() => {
+ content.window.requestAnimationFrame(resolve);
+ });
+ });
+ });
+}
+
+async function getSizing(rdmUI) {
+ const browser = rdmUI.getViewportBrowser();
+ const sizing = await SpecialPowers.spawn(browser, [], async function () {
+ return {
+ width: content.innerWidth,
+ height: content.innerHeight,
+ };
+ });
+ return sizing;
+}
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
new file mode 100644
index 0000000000..5086058402
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_media_sidebar_sourcemaps.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules-sourcemaps.html";
+const MAP_PREF = "devtools.source-map.client-service.enabled";
+
+const LABELS = [
+ "screen and (max-width: 320px)",
+ "screen and (min-width: 1200px)",
+];
+const LINE_NOS = [5, 8];
+
+waitForExplicitFinish();
+
+add_task(async function () {
+ Services.prefs.setBoolPref(MAP_PREF, true);
+
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 1, "correct number of editors");
+
+ // Test editor with @media rules
+ const mediaEditor = ui.editors[0];
+ await openEditor(mediaEditor);
+ testAtRulesEditor(mediaEditor);
+
+ Services.prefs.clearUserPref(MAP_PREF);
+});
+
+function testAtRulesEditor(editor) {
+ const sidebar = editor.details.querySelector(".stylesheet-sidebar");
+ is(sidebar.hidden, false, "sidebar is showing on editor with @media");
+
+ const entries = [...sidebar.querySelectorAll(".at-rule-label")];
+ is(entries.length, 2, "two @media rules displayed in sidebar");
+
+ testRule(entries[0], LABELS[0], LINE_NOS[0]);
+ testRule(entries[1], LABELS[1], LINE_NOS[1]);
+}
+
+function testRule(rule, text, lineno) {
+ const cond = rule.querySelector(".at-rule-condition");
+ is(cond.textContent, text, "media label is correct for " + text);
+
+ const line = rule.querySelector(".at-rule-line");
+ is(line.textContent, ":" + lineno, "correct line number shown");
+}
+
+/* Helpers */
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js
new file mode 100644
index 0000000000..97e6b78b49
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_missing_stylesheet.js
@@ -0,0 +1,37 @@
+"use strict";
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Checks that the style editor manages to finalize its stylesheet loading phase
+// even if one stylesheet is missing, and that an error message is displayed.
+
+const TESTCASE_URI = TEST_BASE + "missing.html";
+
+add_task(async function () {
+ const { ui, toolbox, panel } = await openStyleEditorForURL(TESTCASE_URI);
+
+ // Note that we're not testing for a specific number of stylesheet editors
+ // below because the test-page is loaded with chrome:// URL and, right now,
+ // that means UA stylesheets are shown. So we avoid hardcoding the number of
+ // stylesheets here.
+ await waitUntil(() => ui.editors.length);
+ ok(true, "The UI contains style sheets.");
+
+ const rootEl = panel.panelWindow.document.getElementById(
+ "style-editor-chrome"
+ );
+ ok(!rootEl.classList.contains("loading"), "The loading indicator is hidden");
+
+ const notifBox = toolbox.getNotificationBox();
+ await waitUntil(() => notifBox.getCurrentNotification());
+ const notif = notifBox.getCurrentNotification();
+ ok(notif, "The notification box contains a message");
+ ok(
+ notif.label.includes("Style sheet could not be loaded"),
+ "The error message is the correct one"
+ );
+ ok(
+ notif.label.includes("missing-stylesheet.css"),
+ "The error message contains the missing stylesheet's URL"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_navigate.js b/devtools/client/styleeditor/test/browser_styleeditor_navigate.js
new file mode 100644
index 0000000000..68f76915e4
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_navigate.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that selected sheet and cursor position is reset during navigation.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+const NEW_URI = TEST_BASE_HTTPS + "media.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "Two sheets present after load.");
+
+ info("Selecting the second editor");
+ await ui.selectStyleSheet(ui.editors[1].styleSheet, LINE_NO, COL_NO);
+
+ await navigateToAndWaitForStyleSheets(NEW_URI, ui, 2);
+
+ info("Waiting for source editor to be ready.");
+ await ui.editors[0].getSourceEditor();
+
+ is(ui.selectedEditor, ui.editors[0], "first editor is selected");
+
+ const { line, ch } = ui.selectedEditor.sourceEditor.getCursor();
+ is(line, 0, "first line is selected");
+ is(ch, 0, "first column is selected");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_new.js b/devtools/client/styleeditor/test/browser_styleeditor_new.js
new file mode 100644
index 0000000000..f583759c00
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_new.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that new sheets can be added and edited.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+const TESTCASE_CSS_SOURCE = "body{background-color:red;";
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ const editor = await createNewStyleSheet(ui, panel.panelWindow);
+ await testInitialState(editor);
+
+ const originalHref = editor.styleSheet.href;
+ const waitForPropertyChange = onPropertyChange(editor);
+
+ await typeInEditor(editor, panel.panelWindow);
+
+ await waitForPropertyChange;
+
+ await testUpdated(editor, originalHref);
+});
+
+function onPropertyChange(editor) {
+ return new Promise(resolve => {
+ editor.on("property-change", function onProp(property) {
+ // wait for text to be entered fully
+ const text = editor.sourceEditor.getText();
+ if (property == "ruleCount" && text == TESTCASE_CSS_SOURCE + "}") {
+ editor.off("property-change", onProp);
+ resolve();
+ }
+ });
+ });
+}
+
+async function testInitialState(editor) {
+ info("Testing the initial state of the new editor");
+
+ let summary = editor.summary;
+
+ ok(editor.sourceLoaded, "new editor is loaded when attached");
+ ok(editor.isNew, "new editor has isNew flag");
+
+ if (!editor.sourceEditor.hasFocus()) {
+ info("Waiting for stylesheet editor to gain focus");
+ await editor.sourceEditor.once("focus");
+ }
+ ok(editor.sourceEditor.hasFocus(), "new editor has focus");
+
+ summary = editor.summary;
+ const ruleCount = summary.querySelector(".stylesheet-rule-count").textContent;
+ is(parseInt(ruleCount, 10), 0, "new editor initially shows 0 rules");
+
+ const color = await getComputedStyleProperty({
+ selector: "body",
+ name: "background-color",
+ });
+ is(
+ color,
+ "rgb(255, 255, 255)",
+ "content's background color is initially white"
+ );
+}
+
+function typeInEditor(editor, panelWindow) {
+ return new Promise(resolve => {
+ waitForFocus(function () {
+ for (const c of TESTCASE_CSS_SOURCE) {
+ EventUtils.synthesizeKey(c, {}, panelWindow);
+ }
+ ok(editor.unsaved, "new editor has unsaved flag");
+
+ resolve();
+ }, panelWindow);
+ });
+}
+
+async function testUpdated(editor, originalHref) {
+ info("Testing the state of the new editor after editing it");
+
+ is(
+ editor.sourceEditor.getText(),
+ TESTCASE_CSS_SOURCE + "}",
+ "rule bracket has been auto-closed"
+ );
+
+ const count = await waitFor(() => {
+ const int = parseInt(
+ editor.summary.querySelector(".stylesheet-rule-count").textContent,
+ 10
+ );
+ if (int == 0) {
+ return false;
+ }
+
+ return int;
+ });
+
+ is(count, 1, "new editor shows 1 rule after modification");
+
+ is(editor.styleSheet.href, originalHref, "style sheet href did not change");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js b/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js
new file mode 100644
index 0000000000..c8a2134135
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_nostyle.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that 'no styles' indicator is shown if a page doesn't contain any style
+// sheets.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "nostyle.html";
+
+add_task(async function () {
+ // Make enough room for the "append style sheet" link to not wrap,
+ // as it messes up with EvenEventUtils.synthesizeMouse
+ await pushPref("devtools.styleeditor.navSidebarWidth", 500);
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+ const { panelWindow } = panel;
+
+ ok(
+ !getRootElement(panel).classList.contains("loading"),
+ "style editor root element does not have 'loading' class name anymore"
+ );
+
+ const newButton = panelWindow.document.querySelector(
+ "toolbarbutton.style-editor-newButton"
+ );
+ ok(!newButton.hasAttribute("disabled"), "new style sheet button is enabled");
+
+ const importButton = panelWindow.document.querySelector(
+ ".style-editor-importButton"
+ );
+ ok(!importButton.hasAttribute("disabled"), "import button is enabled");
+
+ const emptyPlaceHolderEl =
+ getRootElement(panel).querySelector(".empty.placeholder");
+ isnot(
+ emptyPlaceHolderEl.ownerGlobal.getComputedStyle(emptyPlaceHolderEl).display,
+ "none",
+ "showing 'no style' indicator"
+ );
+
+ info(
+ "Check that clicking on the append new stylesheet link do add a stylesheet"
+ );
+ const onEditorAdded = ui.once("editor-added");
+ const newLink = emptyPlaceHolderEl.querySelector("a.style-editor-newButton");
+
+ // Use synthesizeMouse to also check that the element is visible
+ EventUtils.synthesizeMouseAtCenter(newLink, {}, newLink.ownerGlobal);
+ await onEditorAdded;
+
+ ok(true, "A stylesheet was added");
+ is(
+ emptyPlaceHolderEl.ownerGlobal.getComputedStyle(emptyPlaceHolderEl).display,
+ "none",
+ "The empty placeholder element is now hidden"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_opentab.js b/devtools/client/styleeditor/test/browser_styleeditor_opentab.js
new file mode 100644
index 0000000000..55494f4166
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_opentab.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// A test to check the 'Open Link in new tab' functionality in the
+// context menu item for stylesheets (bug 992947).
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ const openLinkNewTabItem = panel.panelWindow.document.getElementById(
+ "context-openlinknewtab"
+ );
+
+ let menu = await rightClickStyleSheet(panel, ui.editors[0]);
+ is(
+ openLinkNewTabItem.getAttribute("disabled"),
+ "false",
+ "The menu item is not disabled"
+ );
+ ok(!openLinkNewTabItem.hidden, "The menu item is not hidden");
+
+ const url = TEST_BASE_HTTPS + "simple.css";
+
+ const browserWindow = Services.wm.getMostRecentWindow(
+ gDevTools.chromeWindowType
+ );
+ const originalOpenWebLinkIn = browserWindow.openWebLinkIn;
+ const tabOpenedDefer = new Promise(resolve => {
+ browserWindow.openWebLinkIn = newUrl => {
+ // Reset the actual openWebLinkIn function before proceeding.
+ browserWindow.openWebLinkIn = originalOpenWebLinkIn;
+
+ is(newUrl, url, "The correct tab has been opened");
+ resolve();
+ };
+ });
+
+ const hidden = onPopupHide(menu);
+
+ menu.activateItem(openLinkNewTabItem);
+
+ info(`Waiting for a tab to open - ${url}`);
+ await tabOpenedDefer;
+
+ await hidden;
+
+ menu = await rightClickInlineStyleSheet(panel, ui.editors[1]);
+ is(
+ openLinkNewTabItem.getAttribute("disabled"),
+ "true",
+ "The menu item is disabled"
+ );
+ ok(!openLinkNewTabItem.hidden, "The menu item should not be hidden");
+ menu.hidePopup();
+
+ menu = await rightClickNoStyleSheet(panel);
+ ok(openLinkNewTabItem.hidden, "The menu item should be hidden");
+ menu.hidePopup();
+});
+
+function onPopupShow(contextMenu) {
+ return new Promise(resolve => {
+ contextMenu.addEventListener(
+ "popupshown",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function onPopupHide(contextMenu) {
+ return new Promise(resolve => {
+ contextMenu.addEventListener(
+ "popuphidden",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function rightClickStyleSheet(panel, editor) {
+ const contextMenu = getContextMenuElement(panel);
+ return new Promise(resolve => {
+ onPopupShow(contextMenu).then(() => {
+ resolve(contextMenu);
+ });
+
+ EventUtils.synthesizeMouseAtCenter(
+ editor.summary.querySelector(".stylesheet-name"),
+ { button: 2, type: "contextmenu" },
+ panel.panelWindow
+ );
+ });
+}
+
+function rightClickInlineStyleSheet(panel, editor) {
+ const contextMenu = getContextMenuElement(panel);
+ return new Promise(resolve => {
+ onPopupShow(contextMenu).then(() => {
+ resolve(contextMenu);
+ });
+
+ EventUtils.synthesizeMouseAtCenter(
+ editor.summary.querySelector(".stylesheet-name"),
+ { button: 2, type: "contextmenu" },
+ panel.panelWindow
+ );
+ });
+}
+
+function rightClickNoStyleSheet(panel) {
+ const contextMenu = getContextMenuElement(panel);
+ return new Promise(resolve => {
+ onPopupShow(contextMenu).then(() => {
+ resolve(contextMenu);
+ });
+
+ EventUtils.synthesizeMouseAtCenter(
+ panel.panelWindow.document.querySelector(
+ "#splitview-tpl-summary-stylesheet"
+ ),
+ { button: 2, type: "contextmenu" },
+ panel.panelWindow
+ );
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_pretty.js b/devtools/client/styleeditor/test/browser_styleeditor_pretty.js
new file mode 100644
index 0000000000..e8208cb7d6
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_pretty.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that only minified sheets are automatically prettified,
+// and that the pretty print button behaves as expected.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "minified.html";
+
+const PRETTIFIED_CSS_TEXT = `
+body {
+ background:white;
+}
+div {
+ font-size:4em;
+ color:red
+}
+span {
+ color:green;
+ @media screen {
+ background: blue;
+ &>.myClass {
+ padding: 1em
+ }
+ }
+}
+`.trimStart();
+
+const INLINE_STYLESHEET_ORIGINAL_CSS_TEXT = `
+body { background: red; }
+div {
+font-size: 5em;
+color: red;
+}`.trimStart();
+
+const INLINE_STYLESHEET_PRETTIFIED_CSS_TEXT = `
+body {
+ background: red;
+}
+div {
+ font-size: 5em;
+ color: red;
+}
+`.trimStart();
+
+add_task(async function () {
+ // Use 2 spaces for indent
+ await pushPref("devtools.editor.expandtab", true);
+ await pushPref("devtools.editor.tabsize", 2);
+
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+ is(ui.editors.length, 3, "Three sheets present.");
+
+ info("Testing minified style sheet.");
+ const minifiedEditor = await ui.editors[0].getSourceEditor();
+
+ is(
+ minifiedEditor.sourceEditor.getText(),
+ PRETTIFIED_CSS_TEXT,
+ "minified source has been prettified automatically"
+ );
+
+ info("Selecting second, non-minified style sheet.");
+ await ui.selectStyleSheet(ui.editors[1].styleSheet);
+
+ const inlineEditor = ui.editors[1];
+ is(
+ inlineEditor.sourceEditor.getText(),
+ INLINE_STYLESHEET_ORIGINAL_CSS_TEXT,
+ "non-minified source has been left untouched"
+ );
+
+ const prettyPrintButton = panel.panelWindow.document.querySelector(
+ ".style-editor-prettyPrintButton"
+ );
+ ok(prettyPrintButton, "Pretty print button is displayed");
+ ok(
+ !prettyPrintButton.hasAttribute("disabled"),
+ "Pretty print button is enabled"
+ );
+ is(
+ prettyPrintButton.getAttribute("title"),
+ "Pretty print style sheet",
+ "Pretty print button has the expected title attribute"
+ );
+
+ const onEditorChange = inlineEditor.sourceEditor.once("changes");
+ EventUtils.synthesizeMouseAtCenter(prettyPrintButton, {}, panel.panelWindow);
+ await onEditorChange;
+
+ is(
+ inlineEditor.sourceEditor.getText(),
+ INLINE_STYLESHEET_PRETTIFIED_CSS_TEXT,
+ "inline stylesheet was prettified as expected when clicking on pretty print button"
+ );
+
+ info("Selecting original style sheet.");
+ await ui.selectStyleSheet(ui.editors[2].styleSheet);
+ ok(
+ prettyPrintButton.hasAttribute("disabled"),
+ "Pretty print button is disabled when selecting an original file"
+ );
+ await waitFor(
+ () =>
+ prettyPrintButton.getAttribute("title") ===
+ "Can only pretty print CSS files"
+ );
+ ok(
+ true,
+ "Pretty print button has the expected title attribute when it's disabled"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js b/devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js
new file mode 100644
index 0000000000..e372f54ba2
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_private_perwindowpb.js
@@ -0,0 +1,76 @@
+/* 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 test makes sure that the style editor does not store any
+// content CSS files in the permanent cache when opened from a private window tab.
+
+const TEST_URL = `http://${TEST_HOST}/browser/devtools/client/styleeditor/test/test_private.html`;
+
+add_task(async function () {
+ info("Opening a new private window");
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ info("Clearing the browser cache");
+ Services.cache2.clear();
+
+ const { toolbox, ui } = await openStyleEditorForURL(TEST_URL, win);
+
+ is(ui.editors.length, 1, "The style editor contains one sheet.");
+ const editor = ui.editors[0];
+ is(
+ editor.friendlyName,
+ "test_private.css",
+ "The style editor contains the expected stylesheet"
+ );
+
+ await editor.getSourceEditor();
+
+ await checkDiskCacheFor(editor.friendlyName);
+
+ await toolbox.destroy();
+
+ const onUnload = new Promise(done => {
+ win.addEventListener("unload", function listener(event) {
+ if (event.target == win.document) {
+ win.removeEventListener("unload", listener);
+ done();
+ }
+ });
+ });
+ win.close();
+ await onUnload;
+});
+
+function checkDiskCacheFor(fileName) {
+ let foundPrivateData = false;
+
+ return new Promise(resolve => {
+ Visitor.prototype = {
+ onCacheStorageInfo(num) {
+ info("disk storage contains " + num + " entries");
+ },
+ onCacheEntryInfo(uri) {
+ const urispec = uri.asciiSpec;
+ info(urispec);
+ foundPrivateData = foundPrivateData || urispec.includes(fileName);
+ },
+ onCacheEntryVisitCompleted() {
+ is(foundPrivateData, false, "web content present in disk cache");
+ resolve();
+ },
+ };
+ function Visitor() {}
+
+ const storage = Services.cache2.diskCacheStorage(
+ Services.loadContextInfo.default
+ );
+ storage.asyncVisitStorage(
+ new Visitor(),
+ /* Do walk entries */
+ true
+ );
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_reload.js b/devtools/client/styleeditor/test/browser_styleeditor_reload.js
new file mode 100644
index 0000000000..f568b3f40f
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_reload.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that selected sheet and cursor position persists during reload.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+const LINE_NO = 5;
+const COL_NO = 3;
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "Two sheets present after load.");
+
+ info("Selecting the second editor");
+ await ui.selectStyleSheet(ui.editors[1].styleSheet, LINE_NO, COL_NO);
+ const selectedStyleSheetIndex = ui.editors[1].styleSheet.styleSheetIndex;
+
+ await reloadPageAndWaitForStyleSheets(ui, 2);
+
+ info("Waiting for source editor to be ready.");
+ const newEditor = findEditor(ui, selectedStyleSheetIndex);
+ await newEditor.getSourceEditor();
+
+ is(
+ ui.selectedEditor,
+ newEditor,
+ "Editor of stylesheet that has styleSheetIndex we selected is selected after reload"
+ );
+
+ const { line, ch } = ui.selectedEditor.sourceEditor.getCursor();
+ is(line, LINE_NO, "correct line selected");
+ is(ch, COL_NO, "correct column selected");
+});
+
+function findEditor(ui, styleSheetIndex) {
+ return ui.editors.find(
+ editor => editor.styleSheet.styleSheetIndex === styleSheetIndex
+ );
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_remove_stylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_remove_stylesheet.js
new file mode 100644
index 0000000000..69a23c0947
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_remove_stylesheet.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that a removed style sheet don't show up in the style editor anymore.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "Two sheets present after load.");
+
+ info("Removing the <style> node");
+ const onStyleRemoved = waitFor(() => ui.editors.length == 1);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.querySelector("style").remove();
+ });
+ await onStyleRemoved;
+ is(ui.editors.length, 1, "There's only one stylesheet remaining");
+
+ info("Removing the <link> node");
+ const onLinkRemoved = waitFor(() => !ui.editors.length);
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ content.document.querySelector("link").remove();
+ });
+ await onLinkRemoved;
+ is(ui.editors.length, 0, "There's no stylesheet displayed anymore");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_resize_performance.js b/devtools/client/styleeditor/test/browser_styleeditor_resize_performance.js
new file mode 100644
index 0000000000..28026b7390
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_resize_performance.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This is a performance test designed to check we are not redrawing the UI too many times
+ * after resizing the window, when the styleeditor displays mediaqueries which are source
+ * mapped.
+ * See Bug 1453044 for more details.
+ */
+
+const TESTCASE_URI = TEST_BASE_HTTP + "many-media-rules-sourcemaps/index.html";
+
+// Maximum delay allowed between two at-rules-list-changed events.
+const EVENTS_DELAY = 2000;
+
+// The window resize will still trigger several resize events which will lead to several
+// UI updates. Arbitrary maximum number of events allowed to be fired for a single resize.
+// This used to be > 100 events for this test case.
+const MAX_EVENTS = 10;
+
+add_task(async function () {
+ const { toolbox, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ const win = toolbox.win.parent;
+ const originalWidth = win.outerWidth;
+ const originalHeight = win.outerHeight;
+
+ // Ensure the window is above 500px wide for @media (min-width: 500px)
+ if (originalWidth < 500) {
+ info("Window is too small for the test, resize it to > 800px width");
+ const onMediaListChanged = waitForManyEvents(ui, EVENTS_DELAY);
+ await resizeWindow(800, ui, win);
+ info("Wait for at-rules-list-changed events to settle");
+ await onMediaListChanged;
+ }
+
+ info(
+ "Resize the window to stop matching media queries, and trigger the UI updates"
+ );
+ const onMediaListChanged = waitForManyEvents(ui, win, EVENTS_DELAY);
+ await resizeWindow(400, ui, win);
+ const eventsCount = await onMediaListChanged;
+
+ Assert.less(
+ eventsCount,
+ MAX_EVENTS,
+ `Too many events fired (expected less than ${MAX_EVENTS}, got ${eventsCount})`
+ );
+
+ win.resizeTo(originalWidth, originalHeight);
+});
+
+/**
+ * Resize the window to the provided width.
+ */
+async function resizeWindow(width, ui, win) {
+ const onResize = once(win, "resize");
+ win.resizeTo(width, win.outerHeight);
+ info("Wait for window resize event");
+ await onResize;
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_scroll.js b/devtools/client/styleeditor/test/browser_styleeditor_scroll.js
new file mode 100644
index 0000000000..bba98bf01c
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_scroll.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that editor scrolls to correct line if it's selected with
+// * selectStyleSheet (specified line)
+// * click on the sidebar item (line before the editor was unselected)
+// See bug 1148086.
+
+const SIMPLE = TEST_BASE_HTTP + "simple.css";
+const LONG = TEST_BASE_HTTP + "doc_long.css";
+const DOCUMENT_WITH_LONG_SHEET =
+ "data:text/html;charset=UTF-8," +
+ encodeURIComponent(
+ [
+ "<!DOCTYPE html>",
+ "<html>",
+ " <head>",
+ " <title>Editor scroll test page</title>",
+ ' <link rel="stylesheet" type="text/css" href="' + SIMPLE + '">',
+ ' <link rel="stylesheet" type="text/css" href="' + LONG + '">',
+ " </head>",
+ " <body>Editor scroll test page</body>",
+ "</html>",
+ ].join("\n")
+ );
+const LINE_TO_SELECT = 201;
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(DOCUMENT_WITH_LONG_SHEET);
+
+ is(ui.editors.length, 2, "Two editors present.");
+
+ const simpleEditor = ui.editors[0];
+ const longEditor = ui.editors[1];
+
+ info(`Selecting doc_long.css and scrolling to line ${LINE_TO_SELECT}`);
+
+ // We need to wait for editor-selected if we want to check the scroll
+ // position as scrolling occurs after selectStyleSheet resolves but before the
+ // event is emitted.
+ let selectEventPromise = waitForEditorToBeSelected(longEditor, ui);
+ await ui.selectStyleSheet(longEditor.styleSheet, LINE_TO_SELECT);
+ await selectEventPromise;
+
+ info("Checking that the correct line is visible after initial load");
+
+ const { from, to } = longEditor.sourceEditor.getViewport();
+ info(`Lines ${from}-${to} are visible (expected ${LINE_TO_SELECT}).`);
+
+ Assert.lessOrEqual(from, LINE_TO_SELECT, "The editor scrolled too much.");
+ Assert.greaterOrEqual(to, LINE_TO_SELECT, "The editor scrolled too little.");
+
+ const initialScrollTop = longEditor.sourceEditor.getScrollInfo().top;
+ info(`Storing scrollTop = ${initialScrollTop} for later comparison.`);
+
+ info("Selecting the first editor (simple.css)");
+ await ui.selectStyleSheet(simpleEditor.styleSheet);
+
+ info("Selecting doc_long.css again.");
+ selectEventPromise = waitForEditorToBeSelected(longEditor, ui);
+
+ // Can't use ui.selectStyleSheet here as it will scroll the editor back to top
+ // and we want to check that the previous scroll position is restored.
+ const summary = await ui.getEditorSummary(longEditor);
+ summary.click();
+
+ info("Waiting for doc_long.css to be selected.");
+ await selectEventPromise;
+
+ const scrollTop = longEditor.sourceEditor.getScrollInfo().top;
+ is(
+ scrollTop,
+ initialScrollTop,
+ "Scroll top was restored after the sheet was selected again."
+ );
+});
+
+/**
+ * A helper that waits "editor-selected" event for given editor.
+ *
+ * @param {StyleSheetEditor} editor
+ * The editor to wait for.
+ * @param {StyleEditorUI} ui
+ * The StyleEditorUI the editor belongs to.
+ */
+var waitForEditorToBeSelected = async function (editor, ui) {
+ info(`Waiting for ${editor.friendlyName} to be selected.`);
+ let selected = await ui.once("editor-selected");
+ while (selected != editor) {
+ info(`Ignored editor-selected for editor ${editor.friendlyName}.`);
+ selected = await ui.once("editor-selected");
+ }
+
+ info(`Got editor-selected for ${editor.friendlyName}.`);
+};
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js b/devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js
new file mode 100644
index 0000000000..b455ec3c7f
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_selectstylesheet.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that StyleEditorUI.selectStyleSheet selects the correct sheet, line and
+// column.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+const LINE_NO = 5;
+const COL_NO = 0;
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+ const editor = ui.editors[1];
+
+ info("Selecting style sheet #1.");
+ await ui.selectStyleSheet(editor.styleSheet, LINE_NO);
+
+ is(ui.selectedEditor, ui.editors[1], "Second editor is selected.");
+ const { line, ch } = ui.selectedEditor.sourceEditor.getCursor();
+
+ is(line, LINE_NO, "correct line selected");
+ is(ch, COL_NO, "correct column selected");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sidebars.js b/devtools/client/styleeditor/test/browser_styleeditor_sidebars.js
new file mode 100644
index 0000000000..d0cdb4acfe
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sidebars.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "media-rules.html";
+
+const PREF_SHOW_AT_RULES_SIDEBAR = "devtools.styleeditor.showAtRulesSidebar";
+const PREF_SIDEBAR_WIDTH = "devtools.styleeditor.atRulesSidebarWidth";
+const PREF_NAV_WIDTH = "devtools.styleeditor.navSidebarWidth";
+
+// Initial widths for the navigation and media sidebars, which will be set via
+// the corresponding preferences.
+// The widths should remain between the current min-width and max-width for the
+// styleeditor sidebars (currently 100px and 400px).
+const NAV_WIDTH = 210;
+const MEDIA_WIDTH = 250;
+
+// Test that sidebar in the styleeditor can be resized.
+add_task(async function () {
+ await pushPref(PREF_SHOW_AT_RULES_SIDEBAR, true);
+ await pushPref(PREF_NAV_WIDTH, NAV_WIDTH);
+ await pushPref(PREF_SIDEBAR_WIDTH, MEDIA_WIDTH);
+
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+ const doc = panel.panelWindow.document;
+
+ info("Open editor for inline sheet with @media rules to have both splitters");
+ const inlineMediaEditor = ui.editors[3];
+ inlineMediaEditor.summary.querySelector(".stylesheet-name").click();
+ await inlineMediaEditor.getSourceEditor();
+
+ info("Check the initial widths of side panels match the preferences values");
+ const navSidebar = doc.querySelector(".splitview-controller");
+ is(navSidebar.clientWidth, NAV_WIDTH);
+
+ const mediaSidebar = doc.querySelector(
+ ".splitview-active .stylesheet-sidebar"
+ );
+ is(mediaSidebar.clientWidth, MEDIA_WIDTH);
+
+ info(
+ "Resize the navigation splitter and check the navigation sidebar is updated"
+ );
+ const navSplitter = doc.querySelector(".devtools-side-splitter");
+ dragElement(navSplitter, { startX: 1, startY: 10, deltaX: 50, deltaY: 0 });
+ is(navSidebar.clientWidth, NAV_WIDTH + 50);
+
+ info("Resize the media splitter and check the media sidebar is updated");
+ const mediaSplitter = doc.querySelector(
+ ".splitview-active .devtools-side-splitter"
+ );
+ dragElement(mediaSplitter, { startX: 1, startY: 10, deltaX: -50, deltaY: 0 });
+ is(mediaSidebar.clientWidth, MEDIA_WIDTH + 50);
+});
+
+/* Helpers */
+
+function dragElement(el, { startX, startY, deltaX, deltaY }) {
+ const win = el.ownerGlobal;
+ const endX = startX + deltaX;
+ const endY = startY + deltaY;
+
+ EventUtils.synthesizeMouse(el, startX, startY, { type: "mousedown" }, win);
+ EventUtils.synthesizeMouse(el, endX, endY, { type: "mousemove" }, win);
+ EventUtils.synthesizeMouse(el, endX, endY, { type: "mouseup" }, win);
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_chrome.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_chrome.js
new file mode 100644
index 0000000000..997171e30d
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_chrome.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+const TEST_URI = URL_ROOT_SSL + "doc_sourcemap_chrome.html";
+const CHROME_TEST_URI = CHROME_URL_ROOT + "doc_sourcemap_chrome.html";
+const GENERATED_NAME = "sourcemaps_chrome.css";
+const ORIGINAL_NAME = "sourcemaps.scss";
+
+/**
+ * Test that a sourcemap served by a chrome URL for a http document will not be resolved.
+ */
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TEST_URI);
+ ok(
+ findStylesheetByName(ui, GENERATED_NAME),
+ "Sourcemap not resolved: generated source is listed"
+ );
+ ok(
+ !findStylesheetByName(ui, ORIGINAL_NAME),
+ "Sourcemap not resolved: original source is not listed"
+ );
+});
+
+/**
+ * Test that a sourcemap served by a chrome URL for a chrome document is resolved.
+ */
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(CHROME_TEST_URI);
+ ok(
+ findStylesheetByName(ui, ORIGINAL_NAME),
+ "Sourcemap resolved: original source is listed"
+ );
+ ok(
+ !findStylesheetByName(ui, GENERATED_NAME),
+ "Sourcemap resolved: generated source is not listed"
+ );
+});
+
+function findStylesheetByName(ui, name) {
+ return ui.editors.some(
+ editor =>
+ editor.summary
+ .querySelector(".stylesheet-name > label")
+ .getAttribute("value") === name
+ );
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js
new file mode 100644
index 0000000000..b03cef01c1
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_large.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Covers the case from Bug 1128747, where loading a sourcemapped
+// file prevents the correct editor from being selected on load,
+// and causes a second iframe to be appended when the user clicks
+// editor in the list.
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps-large.html";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ await openEditor(ui.editors[0]);
+ const iframes = ui.selectedEditor.details.querySelectorAll("iframe");
+
+ is(iframes.length, 1, "There is only one editor iframe");
+ ok(
+ ui.selectedEditor.summary.classList.contains("splitview-active"),
+ "The editor is selected"
+ );
+});
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js
new file mode 100644
index 0000000000..582f9f0a43
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemap_watching.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+const TESTCASE_URI_HTML = TEST_BASE_HTTP + "sourcemaps-watching.html";
+const TESTCASE_URI_CSS = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css";
+const TESTCASE_URI_REG_CSS = TEST_BASE_HTTP + "simple.css";
+const TESTCASE_URI_SCSS = TEST_BASE_HTTP + "sourcemap-sass/sourcemaps.scss";
+const TESTCASE_URI_MAP = TEST_BASE_HTTP + "sourcemap-css/sourcemaps.css.map";
+const TESTCASE_SCSS_NAME = "sourcemaps.scss";
+
+const TRANSITIONS_PREF = "devtools.styleeditor.transitions";
+
+const CSS_TEXT = "* { color: blue }";
+
+add_task(async function () {
+ await new Promise(resolve => {
+ SpecialPowers.pushPrefEnv({ set: [[TRANSITIONS_PREF, false]] }, resolve);
+ });
+
+ // copy all our files over so we don't screw them up for other tests
+ const HTMLFile = await copy(TESTCASE_URI_HTML, ["sourcemaps.html"]);
+ const CSSFile = await copy(TESTCASE_URI_CSS, [
+ "sourcemap-css",
+ "sourcemaps.css",
+ ]);
+ await copy(TESTCASE_URI_SCSS, ["sourcemap-sass", "sourcemaps.scss"]);
+ await copy(TESTCASE_URI_MAP, ["sourcemap-css", "sourcemaps.css.map"]);
+ await copy(TESTCASE_URI_REG_CSS, ["simple.css"]);
+
+ const uri = Services.io.newFileURI(HTMLFile);
+ const testcaseURI = uri.resolve("");
+
+ const { ui } = await openStyleEditorForURL(testcaseURI);
+
+ let editor = ui.editors[1];
+ if (getStylesheetNameFor(editor) != TESTCASE_SCSS_NAME) {
+ editor = ui.editors[2];
+ }
+
+ is(getStylesheetNameFor(editor), TESTCASE_SCSS_NAME, "found scss editor");
+
+ const link = getLinkFor(editor);
+ link.click();
+
+ await editor.getSourceEditor();
+
+ let color = await getComputedStyleProperty({
+ selector: "div",
+ name: "color",
+ });
+ is(color, "rgb(255, 0, 102)", "div is red before saving file");
+
+ const styleApplied = editor.once("style-applied");
+
+ await pauseForTimeChange();
+
+ // Edit and save Sass in the editor. This will start off a file-watching
+ // process waiting for the CSS file to change.
+ await editSCSS(editor);
+
+ // We can't run Sass or another compiler, so we fake it by just
+ // directly changing the CSS file.
+ await editCSSFile(CSSFile);
+
+ info("wrote to CSS file, waiting for style-applied event");
+
+ await styleApplied;
+
+ color = await getComputedStyleProperty({ selector: "div", name: "color" });
+ is(color, "rgb(0, 0, 255)", "div is blue after saving file");
+
+ // Ensure that the editor didn't revert. Bug 1346662.
+ is(editor.sourceEditor.getText(), CSS_TEXT, "edits remain applied");
+});
+
+function editSCSS(editor) {
+ return new Promise(resolve => {
+ editor.sourceEditor.setText(CSS_TEXT);
+
+ editor.saveToFile(null, function (file) {
+ ok(file, "Scss file should be saved");
+ resolve();
+ });
+ });
+}
+
+function editCSSFile(CSSFile) {
+ return write(CSS_TEXT, CSSFile);
+}
+
+function pauseForTimeChange() {
+ return new Promise(resolve => {
+ // We have to wait for the system time to turn over > 1000 ms so that
+ // our file's last change time will show a change. This reflects what
+ // would happen in real life with a user manually saving the file.
+ setTimeout(resolve, 2000);
+ });
+}
+
+/* Helpers */
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+ return editor.summary
+ .querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+}
+
+function copy(srcChromeURL, destFilePath) {
+ const destFile = new FileUtils.File(
+ PathUtils.join(PathUtils.profileDir, ...destFilePath)
+ );
+ return write(read(srcChromeURL), destFile);
+}
+
+function read(srcChromeURL) {
+ const scriptableStream = Cc[
+ "@mozilla.org/scriptableinputstream;1"
+ ].getService(Ci.nsIScriptableInputStream);
+
+ const channel = NetUtil.newChannel({
+ uri: srcChromeURL,
+ loadUsingSystemPrincipal: true,
+ });
+ const input = channel.open();
+ scriptableStream.init(input);
+
+ let data = "";
+ while (input.available()) {
+ data = data.concat(scriptableStream.read(input.available()));
+ }
+ scriptableStream.close();
+ input.close();
+
+ return data;
+}
+
+function write(data, file) {
+ return new Promise(resolve => {
+ const istream = getInputStream(data);
+ const ostream = FileUtils.openSafeFileOutputStream(file);
+
+ NetUtil.asyncCopy(istream, ostream, function (status) {
+ if (!Components.isSuccessCode(status)) {
+ info("Coudln't write to " + file.path);
+ return;
+ }
+ resolve(file);
+ });
+ });
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js
new file mode 100644
index 0000000000..452988eb68
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps.js
@@ -0,0 +1,152 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps.html";
+const PREF = "devtools.source-map.client-service.enabled";
+
+const contents = {
+ "sourcemaps.scss": [
+ "",
+ "$paulrougetpink: #f06;",
+ "",
+ "div {",
+ " color: $paulrougetpink;",
+ "}",
+ "",
+ "span {",
+ " background-color: #EEE;",
+ "}",
+ ].join("\n"),
+ "contained.scss": [
+ "$pink: #f06;",
+ "",
+ "#header {",
+ " color: $pink;",
+ "}",
+ ].join("\n"),
+ "sourcemaps.css": [
+ "div {",
+ " color: #ff0066; }",
+ "",
+ "span {",
+ " background-color: #EEE; }",
+ "",
+ "/*# sourceMappingURL=sourcemaps.css.map */",
+ ].join("\n"),
+ "contained.css": [
+ "#header {",
+ " color: #f06; }",
+ "",
+ "/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJma" +
+ "WxlIjoiIiwic291cmNlcyI6WyJzYXNzL2NvbnRhaW5lZC5zY3NzIl0sIm5hbWVzIjpbXSwi" +
+ "bWFwcGluZ3MiOiJBQUVBO0VBQ0UsT0FISyIsInNvdXJjZXNDb250ZW50IjpbIiRwaW5rOiA" +
+ "jZjA2O1xuXG4jaGVhZGVyIHtcbiAgY29sb3I6ICRwaW5rO1xufSJdfQ==*/",
+ ].join("\n"),
+ "test-stylus.styl": [
+ "paulrougetpink = #f06;",
+ "",
+ "div",
+ " color: paulrougetpink",
+ "",
+ "span",
+ " background-color: #EEE",
+ "",
+ ].join("\n"),
+ "test-stylus.css": [
+ "div {",
+ " color: #f06;",
+ "}",
+ "span {",
+ " background-color: #eee;",
+ "}",
+ "/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb" +
+ "3VyY2VzIjpbInRlc3Qtc3R5bHVzLnN0eWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFB" +
+ "RUE7RUFDRSxPQUFPLEtBQVA7O0FBRUY7RUFDRSxrQkFBa0IsS0FBbEIiLCJmaWxlIjoidGV" +
+ "zdC1zdHlsdXMuY3NzIiwic291cmNlc0NvbnRlbnQiOlsicGF1bHJvdWdldHBpbmsgPSAjZj" +
+ "A2O1xuXG5kaXZcbiAgY29sb3I6IHBhdWxyb3VnZXRwaW5rXG5cbnNwYW5cbiAgYmFja2dyb" +
+ "3VuZC1jb2xvcjogI0VFRVxuIl19 */",
+ ].join("\n"),
+};
+
+const cssNames = ["sourcemaps.css", "contained.css", "test-stylus.css"];
+const origNames = ["sourcemaps.scss", "contained.scss", "test-stylus.styl"];
+
+waitForExplicitFinish();
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(
+ ui.editors.length,
+ 4,
+ "correct number of editors with source maps enabled"
+ );
+
+ // Test first plain css editor
+ testFirstEditor(ui.editors[0]);
+
+ // Test Scss editors
+ await testEditor(ui.editors[1], origNames);
+ await testEditor(ui.editors[2], origNames);
+ await testEditor(ui.editors[3], origNames);
+
+ // Test disabling original sources
+ await togglePref(ui);
+
+ is(ui.editors.length, 4, "correct number of editors after pref toggled");
+
+ // Test CSS editors
+ await testEditor(ui.editors[1], cssNames);
+ await testEditor(ui.editors[2], cssNames);
+ await testEditor(ui.editors[3], cssNames);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+function testFirstEditor(editor) {
+ const name = getStylesheetNameFor(editor);
+ is(name, "simple.css", "First style sheet display name is correct");
+}
+
+function testEditor(editor, possibleNames) {
+ const name = getStylesheetNameFor(editor);
+ ok(possibleNames.includes(name), name + " editor name is correct");
+
+ return openEditor(editor).then(() => {
+ const expectedText = contents[name];
+
+ const text = editor.sourceEditor.getText();
+
+ is(text, expectedText, name + " editor contains expected text");
+ });
+}
+
+/* Helpers */
+
+function togglePref(UI) {
+ const editorsPromise = UI.once("stylesheets-refreshed");
+ const selectedPromise = UI.once("editor-selected");
+
+ Services.prefs.setBoolPref(PREF, false);
+
+ return Promise.all([editorsPromise, selectedPromise]);
+}
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+ return editor.summary
+ .querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js
new file mode 100644
index 0000000000..933a128a74
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sourcemaps_inline.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// https rather than chrome to improve coverage
+const TESTCASE_URI = TEST_BASE_HTTPS + "sourcemaps-inline.html";
+const PREF = "devtools.source-map.client-service.enabled";
+
+const sassContent = `body {
+ background-color: black;
+ & > h1 {
+ color: white;
+ }
+}
+`;
+
+const cssContent =
+ `body {
+ background-color: black;
+}
+body > h1 {
+ color: white;
+}
+` +
+ "/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtY" +
+ "XBwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLO0VBQ3ZCLFNBQU87SUFD" +
+ "TCxLQUFLLEVBQUUsS0FBSyIsCiJzb3VyY2VzIjogWyJ0ZXN0LnNjc3MiXSwKInNvdXJjZXNDb25" +
+ "0ZW50IjogWyJib2R5IHtcbiAgYmFja2dyb3VuZC1jb2xvcjogYmxhY2s7XG4gICYgPiBoMSB7XG" +
+ "4gICAgY29sb3I6IHdoaXRlO1xuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc" +
+ "3QuY3NzIgp9Cg== */";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(
+ ui.editors.length,
+ 1,
+ "correct number of editors with source maps enabled"
+ );
+
+ await testEditor(ui.editors[0], "test.scss", sassContent);
+
+ // Test disabling original sources
+ await togglePref(ui);
+
+ is(ui.editors.length, 1, "correct number of editors after pref toggled");
+
+ // Test CSS editors
+ await testEditor(ui.editors[0], "<inline style sheet #1>", cssContent);
+
+ Services.prefs.clearUserPref(PREF);
+});
+
+async function testEditor(editor, expectedName, expectedText) {
+ const name = getStylesheetNameFor(editor);
+ is(expectedName, name, name + " editor name is correct");
+
+ await openEditor(editor);
+ const text = editor.sourceEditor.getText();
+ is(text, expectedText, name + " editor contains expected text");
+}
+
+/* Helpers */
+
+function togglePref(UI) {
+ const editorsPromise = UI.once("stylesheets-refreshed");
+ const selectedPromise = UI.once("editor-selected");
+
+ Services.prefs.setBoolPref(PREF, false);
+
+ return Promise.all([editorsPromise, selectedPromise]);
+}
+
+function openEditor(editor) {
+ getLinkFor(editor).click();
+
+ return editor.getSourceEditor();
+}
+
+function getLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function getStylesheetNameFor(editor) {
+ return editor.summary
+ .querySelector(".stylesheet-name > label")
+ .getAttribute("value");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js b/devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js
new file mode 100644
index 0000000000..5967eb40b5
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sv_keynav.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that the style sheet list can be navigated with keyboard.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "four.html";
+
+add_task(async function () {
+ const { panel, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ info("Waiting for source editor to load.");
+ await ui.editors[0].getSourceEditor();
+
+ const onEditorSelected = new Promise(resolve => {
+ const off = ui.on("editor-selected", editor => {
+ if (editor == ui.editors[2]) {
+ resolve();
+ off();
+ }
+ });
+ });
+
+ info("Testing keyboard navigation on the sheet list.");
+ testKeyboardNavigation(ui.editors[0], panel);
+
+ info("Waiting for editor #2 to be selected due to keyboard navigation.");
+ await onEditorSelected;
+ ok(ui.editors[2].sourceEditor.hasFocus(), "Editor #2 has focus.");
+});
+
+function getStylesheetNameLinkFor(editor) {
+ return editor.summary.querySelector(".stylesheet-name");
+}
+
+function testKeyboardNavigation(editor, panel) {
+ const panelWindow = panel.panelWindow;
+ const ui = panel.UI;
+ waitForFocus(function () {
+ const summary = editor.summary;
+ EventUtils.synthesizeMouseAtCenter(summary, {}, panelWindow);
+
+ let item = getStylesheetNameLinkFor(ui.editors[0]);
+ is(
+ panelWindow.document.activeElement,
+ item,
+ "editor 0 item is the active element"
+ );
+
+ EventUtils.synthesizeKey("VK_DOWN", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[1]);
+ is(
+ panelWindow.document.activeElement,
+ item,
+ "editor 1 item is the active element"
+ );
+
+ EventUtils.synthesizeKey("VK_HOME", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[0]);
+ is(
+ panelWindow.document.activeElement,
+ item,
+ "fist editor item is the active element"
+ );
+
+ EventUtils.synthesizeKey("VK_END", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[3]);
+ is(
+ panelWindow.document.activeElement,
+ item,
+ "last editor item is the active element"
+ );
+
+ EventUtils.synthesizeKey("VK_UP", {}, panelWindow);
+ item = getStylesheetNameLinkFor(ui.editors[2]);
+ is(
+ panelWindow.document.activeElement,
+ item,
+ "editor 2 item is the active element"
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, panelWindow);
+ // this will attach and give focus editor 2
+ }, panelWindow);
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js b/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js
new file mode 100644
index 0000000000..76098145bc
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sv_resize.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test that resizing the source editor container doesn't move the caret.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "simple.html";
+
+const { Toolbox } = require("resource://devtools/client/framework/toolbox.js");
+
+add_task(async function () {
+ const { toolbox, ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "There are 2 style sheets initially");
+
+ info("Changing toolbox host to a window.");
+ await toolbox.switchHost(Toolbox.HostType.WINDOW);
+
+ const editor = await ui.editors[0].getSourceEditor();
+ const originalSourceEditor = editor.sourceEditor;
+
+ const hostWindow = toolbox.win.parent;
+ const originalWidth = hostWindow.outerWidth;
+ const originalHeight = hostWindow.outerHeight;
+
+ // to check the caret is preserved
+ originalSourceEditor.setCursor(originalSourceEditor.getPosition(4));
+
+ info("Resizing window.");
+ hostWindow.resizeTo(120, 480);
+
+ const sourceEditor = ui.editors[0].sourceEditor;
+ is(
+ sourceEditor,
+ originalSourceEditor,
+ "the editor still references the same Editor instance"
+ );
+
+ is(
+ sourceEditor.getOffset(sourceEditor.getCursor()),
+ 4,
+ "the caret position has been preserved"
+ );
+
+ info("Restoring window to original size.");
+ hostWindow.resizeTo(originalWidth, originalHeight);
+});
+
+registerCleanupFunction(() => {
+ // Restore the host type for other tests.
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_sync.js b/devtools/client/styleeditor/test/browser_styleeditor_sync.js
new file mode 100644
index 0000000000..6b97e1cae0
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_sync.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ /*! color: red; */
+ }
+
+ #testid {
+ /*! font-size: 4em; */
+ }
+ `;
+
+async function closeAndReopenToolbox() {
+ await gDevTools.closeToolboxForTab(gBrowser.selectedTab);
+ const { ui: newui } = await openStyleEditor();
+ return newui;
+}
+
+add_task(async function () {
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ let ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ // Disable the "font-size" property.
+ let propEditor = ruleEditor.rule.textProps[0].editor;
+ let onModification = view.once("ruleview-changed");
+ propEditor.enable.click();
+ await onModification;
+
+ // Disable the "color" property. Note that this property is in a
+ // rule that also contains a non-inherited property -- so this test
+ // is also testing that property editing works properly in this
+ // situation.
+ ruleEditor = getRuleViewRuleEditor(view, 3);
+ propEditor = ruleEditor.rule.textProps[1].editor;
+ onModification = view.once("ruleview-changed");
+ propEditor.enable.click();
+ await onModification;
+
+ let { ui } = await openStyleEditor();
+
+ let editor = await ui.editors[0].getSourceEditor();
+ let text = editor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+
+ // Close and reopen the toolbox, to see that the edited text remains
+ // available.
+ ui = await closeAndReopenToolbox();
+ editor = await ui.editors[0].getSourceEditor();
+ text = editor.sourceEditor.getText();
+ is(text, expectedText, "changes remain after close and reopen");
+
+ // For the time being, the actor does not update the style's owning
+ // node's textContent. See bug 1205380.
+ const textContent = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [],
+ async function () {
+ return content.document.querySelector("style").textContent;
+ }
+ );
+
+ isnot(textContent, expectedText, "changes not written back to style node");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js b/devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js
new file mode 100644
index 0000000000..51546eaba6
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddProperty.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that adding a new rule is synced to the style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+const TESTCASE_URI_WITH_CSP = TEST_BASE_HTTP + "sync_with_csp.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ font-size: 4em;
+ /*! background-color: yellow; */
+ }
+ `;
+
+add_task(async function () {
+ const URIs = [TESTCASE_URI, TESTCASE_URI_WITH_CSP];
+
+ for (const URI of URIs) {
+ await addTab(URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Focusing a new property name in the rule-view on " + URI);
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+ const editor = await focusEditableField(view, ruleEditor.closeBrace);
+ is(
+ inplaceEditor(ruleEditor.newPropSpan),
+ editor,
+ "The new property editor has focus"
+ );
+
+ const input = editor.input;
+ input.value = "/* background-color: yellow; */";
+
+ info("Pressing return to commit and focus the new value field");
+ const onModifications = view.once("ruleview-changed");
+ EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
+ await onModifications;
+
+ const { ui } = await openStyleEditor();
+ const sourceEditor = await ui.editors[0].getSourceEditor();
+ const text = sourceEditor.sourceEditor.getText();
+ is(text, expectedText, "selector edits are synced");
+ }
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
new file mode 100644
index 0000000000..84fe0f2575
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAddRule.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that adding a new rule is synced to the style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+#testid {
+}`;
+
+add_task(async function () {
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ const onNewRuleAdded = once(view, "new-rule-added");
+ view.addRuleButton.click();
+ await onNewRuleAdded;
+
+ const { ui } = await openStyleEditor();
+
+ info("Selecting the second editor");
+ await ui.selectStyleSheet(ui.editors[1].styleSheet);
+
+ const editor = ui.editors[1];
+ const text = editor.sourceEditor.getText();
+ is(text, expectedText, "selector edits are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
new file mode 100644
index 0000000000..2709a86cf8
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncAlreadyOpen.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ /*! font-size: 4em; */
+ }
+ `;
+
+add_task(async function () {
+ await addTab(TESTCASE_URI);
+
+ const { inspector, view, toolbox } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ // In this test, make sure the style editor is open before making
+ // changes in the inspector.
+ const { ui } = await openStyleEditor();
+ const editor = await ui.editors[0].getSourceEditor();
+
+ const onEditorChange = new Promise(resolve => {
+ editor.sourceEditor.on("change", resolve);
+ });
+
+ await toolbox.selectTool("inspector");
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ // Disable the "font-size" property.
+ const propEditor = ruleEditor.rule.textProps[0].editor;
+ const onModification = view.once("ruleview-changed");
+ propEditor.enable.click();
+ await onModification;
+
+ await openStyleEditor();
+ await onEditorChange;
+
+ const text = editor.sourceEditor.getText();
+ is(text, expectedText, "style inspector changes are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
new file mode 100644
index 0000000000..c82000aada
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncEditSelector.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style inspector are synchronized into the
+// style editor.
+
+const TESTCASE_URI = TEST_BASE_HTTP + "sync.html";
+
+const expectedText = `
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid, span {
+ font-size: 4em;
+ }
+ `;
+
+add_task(async function () {
+ await addTab(TESTCASE_URI);
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+ const ruleEditor = getRuleViewRuleEditor(view, 1);
+
+ let editor = await focusEditableField(view, ruleEditor.selectorText);
+ editor.input.value = "#testid, span";
+ const onRuleViewChanged = once(view, "ruleview-changed");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await onRuleViewChanged;
+
+ const { ui } = await openStyleEditor();
+
+ editor = await ui.editors[0].getSourceEditor();
+ const text = editor.sourceEditor.getText();
+ is(text, expectedText, "selector edits are synced");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
new file mode 100644
index 0000000000..143e4e92c3
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_syncIntoRuleView.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+// Test that changes in the style editor are synchronized into the
+// style inspector.
+
+const TEST_URI = `
+ <style type='text/css'>
+ </style>
+ <div id='testid' class='testclass'>Styled Node</div>
+`;
+
+const TESTCASE_CSS_SOURCE = "#testid { color: chartreuse; }";
+
+add_task(async function () {
+ await addTab("data:text/html;charset=utf-8," + encodeURIComponent(TEST_URI));
+
+ info("Open the inspector and select the node we want to add style to");
+ const { inspector, view } = await openRuleView();
+ await selectNode("#testid", inspector);
+
+ info("Open the StyleEditor");
+ const { panel, ui } = await openStyleEditor();
+
+ const editor = await ui.editors[0].getSourceEditor();
+ await new Promise(res => waitForFocus(res, panel.panelWindow));
+
+ info("Type new rule in stylesheet");
+ editor.focus();
+ EventUtils.sendString(TESTCASE_CSS_SOURCE, panel.panelWindow);
+ ok(editor.unsaved, "new editor has unsaved flag");
+
+ info("Wait for ruleview to update");
+ await inspector.toolbox.selectTool("inspector");
+ await waitFor(() => getRuleViewRule(view, "#testid"));
+
+ info("Check that edits were synced to rule view");
+ const value = getRuleViewPropertyValue(view, "#testid", "color");
+ is(value, "chartreuse", "Got the expected color property");
+});
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js b/devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js
new file mode 100644
index 0000000000..7fd1c0bd1b
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_transition_rule.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TESTCASE_URI = TEST_BASE_HTTPS + "simple.html";
+
+const NEW_RULE = "body { background-color: purple; }";
+
+add_task(async function () {
+ const { ui } = await openStyleEditorForURL(TESTCASE_URI);
+
+ is(ui.editors.length, 2, "correct number of editors");
+
+ const editor = ui.editors[0];
+ await openEditor(editor);
+
+ // Set text twice in a row
+ const styleChanges = listenForStyleChange(editor);
+
+ editor.sourceEditor.setText(NEW_RULE);
+ editor.sourceEditor.setText(NEW_RULE + " ");
+
+ await styleChanges;
+
+ const rules = await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [0],
+ async function (index) {
+ const sheet = content.document.styleSheets[index];
+ return [...sheet.cssRules].map(rule => rule.cssText);
+ }
+ );
+
+ // Test that we removed the transition rule, but kept the rule we added
+ is(rules.length, 1, "only one rule in stylesheet");
+ is(rules[0], NEW_RULE, "stylesheet only contains rule we added");
+});
+
+/* Helpers */
+
+function openEditor(editor) {
+ const link = editor.summary.querySelector(".stylesheet-name");
+ link.click();
+
+ return editor.getSourceEditor();
+}
+
+function listenForStyleChange(editor) {
+ return editor.once("style-applied");
+}
diff --git a/devtools/client/styleeditor/test/browser_styleeditor_xul.js b/devtools/client/styleeditor/test/browser_styleeditor_xul.js
new file mode 100644
index 0000000000..e5843b9e79
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_styleeditor_xul.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that the style-editor initializes correctly for XUL windows.
+
+"use strict";
+
+waitForExplicitFinish();
+
+const TEST_URL = TEST_BASE + "doc_xulpage.xhtml";
+
+add_task(async function () {
+ const tab = await addTab(TEST_URL);
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "styleeditor",
+ });
+ const panel = toolbox.getCurrentPanel();
+
+ ok(
+ panel,
+ "The style-editor panel did initialize correctly for the XUL window"
+ );
+});
diff --git a/devtools/client/styleeditor/test/browser_toolbox_styleeditor.js b/devtools/client/styleeditor/test/browser_toolbox_styleeditor.js
new file mode 100644
index 0000000000..716946b67d
--- /dev/null
+++ b/devtools/client/styleeditor/test/browser_toolbox_styleeditor.js
@@ -0,0 +1,104 @@
+/* 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/. */
+
+// Test that stylesheets from parent and content processes are displayed in the styleeditor.
+
+"use strict";
+
+requestLongerTimeout(4);
+
+const TEST_URI = `data:text/html,<!DOCTYPE html>
+ <head>
+ <meta charset=utf8>
+ <link rel="stylesheet" type="text/css" href="${TEST_BASE_HTTP}simple.css">
+ <head>
+ <body>Test browser toolbox</body>`;
+
+/* global gToolbox */
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/framework/browser-toolbox/test/helpers-browser-toolbox.js",
+ this
+);
+
+add_task(async function () {
+ await pushPref("devtools.browsertoolbox.scope", "everything");
+ await pushPref("devtools.styleeditor.transitions", false);
+ await addTab(TEST_URI);
+ const ToolboxTask = await initBrowserToolboxTask();
+
+ await ToolboxTask.importFunctions({
+ waitUntil,
+ });
+
+ await ToolboxTask.spawn(null, async () => {
+ await gToolbox.selectTool("styleeditor");
+ const panel = gToolbox.getCurrentPanel();
+
+ function getStyleEditorItems() {
+ return Array.from(
+ panel.panelWindow.document.querySelectorAll(".splitview-nav li")
+ );
+ }
+
+ info(`check if "parent process" stylesheets are displayed`);
+ const isUAStyleSheet = el =>
+ el.querySelector(".stylesheet-name label").value == "ua.css";
+ await waitUntil(() => getStyleEditorItems().find(isUAStyleSheet));
+ ok(true, "Found ua.css stylesheet");
+
+ info("check if content page stylesheets are displayed");
+ const isTabStyleSheet = el =>
+ el.querySelector(".stylesheet-name label").value == "simple.css";
+ await waitUntil(() => getStyleEditorItems().find(isTabStyleSheet));
+ ok(true, "Found simple.css tab stylesheet");
+
+ info("Select the stylesheet and update its content");
+ const contentStylesheetSummaryEl =
+ getStyleEditorItems().find(isTabStyleSheet);
+
+ let tabStyleSheetEditor;
+ if (panel.UI.selectedEditor.friendlyName === "simple.css") {
+ // simple.css might be selected by default, depending on the order in
+ // which the stylesheets have been loaded in the style editor.
+ tabStyleSheetEditor = panel.UI.selectedEditor;
+ } else {
+ // We might get events for the initial, default selected stylesheet, so wait until
+ // we get the one for the simple.css stylesheet.
+ const onTabStyleSheetEditorSelected = new Promise(resolve => {
+ const onEditorSelected = editor => {
+ if (editor.summary == contentStylesheetSummaryEl) {
+ resolve(editor);
+ panel.UI.off("editor-selected", onEditorSelected);
+ }
+ };
+ panel.UI.on("editor-selected", onEditorSelected);
+ });
+ panel.UI.setActiveSummary(contentStylesheetSummaryEl);
+ tabStyleSheetEditor = await onTabStyleSheetEditorSelected;
+ }
+
+ info("Wait for sourceEditor to be available");
+ await waitUntil(() => tabStyleSheetEditor.sourceEditor);
+
+ const onStyleApplied = tabStyleSheetEditor.once("style-applied");
+ tabStyleSheetEditor.sourceEditor.setText(
+ tabStyleSheetEditor.sourceEditor.getText() + "\n body {color: red;}"
+ );
+ await onStyleApplied;
+ });
+
+ info("Check that the edit done in the style editor were applied to the page");
+ const bodyColorStyle = await getComputedStyleProperty({
+ selector: "body",
+ name: "color",
+ });
+
+ is(
+ bodyColorStyle,
+ "rgb(255, 0, 0)",
+ "Changes made to simple.css were applied to the page"
+ );
+
+ await ToolboxTask.destroy();
+});
diff --git a/devtools/client/styleeditor/test/bug_1405342_serviceworker_iframes.html b/devtools/client/styleeditor/test/bug_1405342_serviceworker_iframes.html
new file mode 100644
index 0000000000..7bcbcf875c
--- /dev/null
+++ b/devtools/client/styleeditor/test/bug_1405342_serviceworker_iframes.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Bug 1405342</title>
+</head>
+<body>
+ <iframe src="https://test2.example.com/browser/devtools/client/styleeditor/test/iframe_with_service_worker.html"><iframe>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/doc_empty.html b/devtools/client/styleeditor/test/doc_empty.html
new file mode 100644
index 0000000000..825c61e6b9
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_empty.html
@@ -0,0 +1,3 @@
+<!doctype html>
+<html>
+</html>
diff --git a/devtools/client/styleeditor/test/doc_fetch_from_netmonitor.html b/devtools/client/styleeditor/test/doc_fetch_from_netmonitor.html
new file mode 100644
index 0000000000..ff943978a0
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_fetch_from_netmonitor.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+<head>
+ <title>Fetch from netmonitor testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="doc_short_string.css"/>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="doc_long_string.css"/>
+ <!-- This last CSS is generated by a SJS server to avoid adding a 300,000 lines stylesheet to the codebase. -->
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="sjs_huge-css-server.sjs"/>
+</head>
+<body>
+ <div>Fetch from netmonitor</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/doc_long.css b/devtools/client/styleeditor/test/doc_long.css
new file mode 100644
index 0000000000..801a6e276f
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_long.css
@@ -0,0 +1,402 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+div {
+ z-index: 1;
+}
+
+div {
+ z-index: 2;
+}
+
+div {
+ z-index: 3;
+}
+
+div {
+ z-index: 4;
+}
+
+div {
+ z-index: 5;
+}
+
+div {
+ z-index: 6;
+}
+
+div {
+ z-index: 7;
+}
+
+div {
+ z-index: 8;
+}
+
+div {
+ z-index: 9;
+}
+
+div {
+ z-index: 10;
+}
+
+div {
+ z-index: 11;
+}
+
+div {
+ z-index: 12;
+}
+
+div {
+ z-index: 13;
+}
+
+div {
+ z-index: 14;
+}
+
+div {
+ z-index: 15;
+}
+
+div {
+ z-index: 16;
+}
+
+div {
+ z-index: 17;
+}
+
+div {
+ z-index: 18;
+}
+
+div {
+ z-index: 19;
+}
+
+div {
+ z-index: 20;
+}
+
+div {
+ z-index: 21;
+}
+
+div {
+ z-index: 22;
+}
+
+div {
+ z-index: 23;
+}
+
+div {
+ z-index: 24;
+}
+
+div {
+ z-index: 25;
+}
+
+div {
+ z-index: 26;
+}
+
+div {
+ z-index: 27;
+}
+
+div {
+ z-index: 28;
+}
+
+div {
+ z-index: 29;
+}
+
+div {
+ z-index: 30;
+}
+
+div {
+ z-index: 31;
+}
+
+div {
+ z-index: 32;
+}
+
+div {
+ z-index: 33;
+}
+
+div {
+ z-index: 34;
+}
+
+div {
+ z-index: 35;
+}
+
+div {
+ z-index: 36;
+}
+
+div {
+ z-index: 37;
+}
+
+div {
+ z-index: 38;
+}
+
+div {
+ z-index: 39;
+}
+
+div {
+ z-index: 40;
+}
+
+div {
+ z-index: 41;
+}
+
+div {
+ z-index: 42;
+}
+
+div {
+ z-index: 43;
+}
+
+div {
+ z-index: 44;
+}
+
+div {
+ z-index: 45;
+}
+
+div {
+ z-index: 46;
+}
+
+div {
+ z-index: 47;
+}
+
+div {
+ z-index: 48;
+}
+
+div {
+ z-index: 49;
+}
+
+div {
+ z-index: 50;
+}
+
+div {
+ z-index: 51;
+}
+
+div {
+ z-index: 52;
+}
+
+div {
+ z-index: 53;
+}
+
+div {
+ z-index: 54;
+}
+
+div {
+ z-index: 55;
+}
+
+div {
+ z-index: 56;
+}
+
+div {
+ z-index: 57;
+}
+
+div {
+ z-index: 58;
+}
+
+div {
+ z-index: 59;
+}
+
+div {
+ z-index: 60;
+}
+
+div {
+ z-index: 61;
+}
+
+div {
+ z-index: 62;
+}
+
+div {
+ z-index: 63;
+}
+
+div {
+ z-index: 64;
+}
+
+div {
+ z-index: 65;
+}
+
+div {
+ z-index: 66;
+}
+
+div {
+ z-index: 67;
+}
+
+div {
+ z-index: 68;
+}
+
+div {
+ z-index: 69;
+}
+
+div {
+ z-index: 70;
+}
+
+div {
+ z-index: 71;
+}
+
+div {
+ z-index: 72;
+}
+
+div {
+ z-index: 73;
+}
+
+div {
+ z-index: 74;
+}
+
+div {
+ z-index: 75;
+}
+
+div {
+ z-index: 76;
+}
+
+div {
+ z-index: 77;
+}
+
+div {
+ z-index: 78;
+}
+
+div {
+ z-index: 79;
+}
+
+div {
+ z-index: 80;
+}
+
+div {
+ z-index: 81;
+}
+
+div {
+ z-index: 82;
+}
+
+div {
+ z-index: 83;
+}
+
+div {
+ z-index: 84;
+}
+
+div {
+ z-index: 85;
+}
+
+div {
+ z-index: 86;
+}
+
+div {
+ z-index: 87;
+}
+
+div {
+ z-index: 88;
+}
+
+div {
+ z-index: 89;
+}
+
+div {
+ z-index: 90;
+}
+
+div {
+ z-index: 91;
+}
+
+div {
+ z-index: 92;
+}
+
+div {
+ z-index: 93;
+}
+
+div {
+ z-index: 94;
+}
+
+div {
+ z-index: 95;
+}
+
+div {
+ z-index: 96;
+}
+
+div {
+ z-index: 97;
+}
+
+div {
+ z-index: 98;
+}
+
+div {
+ z-index: 99;
+}
+
+div {
+ z-index: 100;
+}
diff --git a/devtools/client/styleeditor/test/doc_long_string.css b/devtools/client/styleeditor/test/doc_long_string.css
new file mode 100644
index 0000000000..39a856314b
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_long_string.css
@@ -0,0 +1,43 @@
+/* CSS file longer than the server's long string limit of 10000 bytes */
+
+/*
+Copyright (c) 2011, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.9.0
+*/
+.yui-calcontainer{position:relative;float:left;_overflow:hidden}.yui-calcontainer iframe{position:absolute;border:0;margin:0;padding:0;z-index:0;width:100%;height:100%;left:0;top:0}.yui-calcontainer iframe.fixedsize{width:50em;height:50em;top:-1px;left:-1px}.yui-calcontainer.multi .groupcal{z-index:1;float:left;position:relative}.yui-calcontainer .title{position:relative;z-index:1}.yui-calcontainer .close-icon{position:absolute;z-index:1;text-indent:-10000em;overflow:hidden}.yui-calendar{position:relative}.yui-calendar .calnavleft{position:absolute;z-index:1;text-indent:-10000em;overflow:hidden}.yui-calendar .calnavright{position:absolute;z-index:1;text-indent:-10000em;overflow:hidden}.yui-calendar .calheader{position:relative;width:100%;text-align:center}.yui-calcontainer .yui-cal-nav-mask{position:absolute;z-index:2;margin:0;padding:0;width:100%;height:100%;_width:0;_height:0;left:0;top:0;display:none}.yui-calcontainer .yui-cal-nav{position:absolute;z-index:3;top:0;display:none}.yui-calcontainer .yui-cal-nav .yui-cal-nav-btn{display:-moz-inline-box;display:inline-block}.yui-calcontainer .yui-cal-nav .yui-cal-nav-btn button{display:block;*display:inline-block;*overflow:visible;border:0;background-color:transparent;cursor:pointer}.yui-calendar .calbody a:hover{background:inherit}p#clear{clear:left;padding-top:10px}.yui-skin-sam .yui-calcontainer{background-color:#f2f2f2;border:1px solid #808080;padding:10px}.yui-skin-sam .yui-calcontainer.multi{padding:0 5px 0 5px}.yui-skin-sam .yui-calcontainer.multi .groupcal{background-color:transparent;border:0;padding:10px 5px 10px 5px;margin:0}.yui-skin-sam .yui-calcontainer .title{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 0;border-bottom:1px solid #ccc;font:100% sans-serif;color:#000;font-weight:bold;height:auto;padding:.4em;margin:0 -10px 10px -10px;top:0;left:0;text-align:left}.yui-skin-sam .yui-calcontainer.multi .title{margin:0 -5px 0 -5px}.yui-skin-sam .yui-calcontainer.withtitle{padding-top:0}.yui-skin-sam .yui-calcontainer .calclose{background:url(../js/yui/assets/skins/sam/sprite.png) no-repeat 0 -300px;width:25px;height:15px;top:.4em;right:.4em;cursor:pointer}.yui-skin-sam .yui-calendar{border-spacing:0;border-collapse:collapse;font:100% sans-serif;text-align:center;margin:0}.yui-skin-sam .yui-calendar .calhead{background:transparent;border:0;vertical-align:middle;padding:0}.yui-skin-sam .yui-calendar .calheader{background:transparent;font-weight:bold;padding:0 0 .6em 0;text-align:center}.yui-skin-sam .yui-calendar .calheader img{border:0}.yui-skin-sam .yui-calendar .calnavleft{background:url(../js/yui/assets/skins/sam/sprite.png) no-repeat 0 -450px;width:25px;height:15px;top:0;bottom:0;left:-10px;margin-left:.4em;cursor:pointer}.yui-skin-sam .yui-calendar .calnavright{background:url(../js/yui/assets/skins/sam/sprite.png) no-repeat 0 -500px;width:25px;height:15px;top:0;bottom:0;right:-10px;margin-right:.4em;cursor:pointer}.yui-skin-sam .yui-calendar .calweekdayrow{height:2em}.yui-skin-sam .yui-calendar .calweekdayrow th{padding:0;border:0}.yui-skin-sam .yui-calendar .calweekdaycell{color:#000;font-weight:bold;text-align:center;width:2em}.yui-skin-sam .yui-calendar .calfoot{background-color:#f2f2f2}.yui-skin-sam .yui-calendar .calrowhead,.yui-skin-sam .yui-calendar .calrowfoot{color:#a6a6a6;font-size:85%;font-style:normal;font-weight:normal;border:0}.yui-skin-sam .yui-calendar .calrowhead{text-align:right;padding:0 2px 0 0}.yui-skin-sam .yui-calendar .calrowfoot{text-align:left;padding:0 0 0 2px}.yui-skin-sam .yui-calendar td.calcell{border:1px solid #ccc;background:#fff;padding:1px;height:1.6em;line-height:1.6em;text-align:center;white-space:nowrap}.yui-skin-sam .yui-calendar td.calcell a{color:#06c;display:block;height:100%;text-decoration:none}.yui-skin-sam .yui-calendar td.calcell.today{background-color:#000}.yui-skin-sam .yui-calendar td.calcell.today a{background-color:#fff}.yui-skin-sam .yui-calendar td.calcell.oom{background-color:#ccc;color:#a6a6a6;cursor:default}.yui-skin-sam .yui-calendar td.calcell.oom a{color:#a6a6a6}.yui-skin-sam .yui-calendar td.calcell.selected{background-color:#fff;color:#000}.yui-skin-sam .yui-calendar td.calcell.selected a{background-color:#b3d4ff;color:#000}.yui-skin-sam .yui-calendar td.calcell.calcellhover{background-color:#426fd9;color:#fff;cursor:pointer}.yui-skin-sam .yui-calendar td.calcell.calcellhover a{background-color:#426fd9;color:#fff}.yui-skin-sam .yui-calendar td.calcell.previous{color:#e0e0e0}.yui-skin-sam .yui-calendar td.calcell.restricted{text-decoration:line-through}.yui-skin-sam .yui-calendar td.calcell.highlight1{background-color:#cf9}.yui-skin-sam .yui-calendar td.calcell.highlight2{background-color:#9cf}.yui-skin-sam .yui-calendar td.calcell.highlight3{background-color:#fcc}.yui-skin-sam .yui-calendar td.calcell.highlight4{background-color:#cf9}.yui-skin-sam .yui-calendar a.calnav{border:1px solid #f2f2f2;padding:0 4px;text-decoration:none;color:#000;zoom:1}.yui-skin-sam .yui-calendar a.calnav:hover{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 0;border-color:#a0a0a0;cursor:pointer}.yui-skin-sam .yui-calcontainer .yui-cal-nav-mask{background-color:#000;opacity:.25;filter:alpha(opacity=25)}.yui-skin-sam .yui-calcontainer .yui-cal-nav{font-family:arial,helvetica,clean,sans-serif;font-size:93%;border:1px solid #808080;left:50%;margin-left:-7em;width:14em;padding:0;top:2.5em;background-color:#f2f2f2}.yui-skin-sam .yui-calcontainer.withtitle .yui-cal-nav{top:4.5em}.yui-skin-sam .yui-calcontainer.multi .yui-cal-nav{width:16em;margin-left:-8em}.yui-skin-sam .yui-calcontainer .yui-cal-nav-y,.yui-skin-sam .yui-calcontainer .yui-cal-nav-m,.yui-skin-sam .yui-calcontainer .yui-cal-nav-b{padding:5px 10px 5px 10px}.yui-skin-sam .yui-calcontainer .yui-cal-nav-b{text-align:center}.yui-skin-sam .yui-calcontainer .yui-cal-nav-e{margin-top:5px;padding:5px;background-color:#edf5ff;border-top:1px solid black;display:none}.yui-skin-sam .yui-calcontainer .yui-cal-nav label{display:block;font-weight:bold}
+.yui-skin-sam .yui-calcontainer .yui-cal-nav-mc{width:100%;_width:auto}.yui-skin-sam .yui-calcontainer .yui-cal-nav-y input.yui-invalid{background-color:#ffee69;border:1px solid #000}.yui-skin-sam .yui-calcontainer .yui-cal-nav-yc{width:4em}.yui-skin-sam .yui-calcontainer .yui-cal-nav .yui-cal-nav-btn{border:1px solid #808080;background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 0;background-color:#ccc;margin:auto .15em}.yui-skin-sam .yui-calcontainer .yui-cal-nav .yui-cal-nav-btn button{padding:0 8px;font-size:93%;line-height:2;*line-height:1.7;min-height:2em;*min-height:auto;color:#000}.yui-skin-sam .yui-calcontainer .yui-cal-nav .yui-cal-nav-btn.yui-default{border:1px solid #304369;background-color:#426fd9;background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 -1400px}.yui-skin-sam .yui-calcontainer .yui-cal-nav .yui-cal-nav-btn.yui-default button{color:#fff}
+
+/*
+Copyright (c) 2011, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.9.0
+*/
+.yui-overlay,.yui-panel-container{visibility:hidden;position:absolute;z-index:2}.yui-panel{position:relative}.yui-panel-container form{margin:0}.mask{z-index:1;display:none;position:absolute;top:0;left:0;right:0;bottom:0}.mask.block-scrollbars{overflow:auto}.masked select,.drag select,.hide-select select{_visibility:hidden}.yui-panel-container select{_visibility:inherit}.hide-scrollbars,.hide-scrollbars *{overflow:hidden}.hide-scrollbars select{display:none}.show-scrollbars{overflow:auto}.yui-panel-container.show-scrollbars,.yui-tt.show-scrollbars{overflow:visible}.yui-panel-container.show-scrollbars .underlay,.yui-tt.show-scrollbars .yui-tt-shadow{overflow:auto}.yui-panel-container.shadow .underlay.yui-force-redraw{padding-bottom:1px}.yui-effect-fade .underlay,.yui-effect-fade .yui-tt-shadow{display:none}.yui-tt-shadow{position:absolute}.yui-override-padding{padding:0!important}.yui-panel-container .container-close{overflow:hidden;text-indent:-10000em;text-decoration:none}.yui-overlay.yui-force-redraw,.yui-panel-container.yui-force-redraw{margin-bottom:1px}.yui-skin-sam .mask{background-color:#000;opacity:.25;filter:alpha(opacity=25)}.yui-skin-sam .yui-panel-container{padding:0 1px;*padding:2px}.yui-skin-sam .yui-panel{position:relative;left:0;top:0;border-style:solid;border-width:1px 0;border-color:#808080;z-index:1;*border-width:1px;*zoom:1;_zoom:normal}.yui-skin-sam .yui-panel .hd,.yui-skin-sam .yui-panel .bd,.yui-skin-sam .yui-panel .ft{border-style:solid;border-width:0 1px;border-color:#808080;margin:0 -1px;*margin:0;*border:0}.yui-skin-sam .yui-panel .hd{border-bottom:solid 1px #ccc}.yui-skin-sam .yui-panel .bd,.yui-skin-sam .yui-panel .ft{background-color:#f2f2f2}.yui-skin-sam .yui-panel .hd{padding:0 10px;font-size:93%;line-height:2;*line-height:1.9;font-weight:bold;color:#000;background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 -200px}.yui-skin-sam .yui-panel .bd{padding:10px}.yui-skin-sam .yui-panel .ft{border-top:solid 1px #808080;padding:5px 10px;font-size:77%}.yui-skin-sam .container-close{position:absolute;top:5px;right:6px;width:25px;height:15px;background:url(../js/yui/assets/skins/sam/sprite.png) no-repeat 0 -300px;cursor:pointer}.yui-skin-sam .yui-panel-container .underlay{right:-1px;left:-1px}.yui-skin-sam .yui-panel-container.matte{padding:9px 10px;background-color:#fff}.yui-skin-sam .yui-panel-container.shadow{_padding:2px 4px 0 2px}.yui-skin-sam .yui-panel-container.shadow .underlay{position:absolute;top:2px;left:-3px;right:-3px;bottom:-3px;*top:4px;*left:-1px;*right:-1px;*bottom:-1px;_top:0;_left:0;_right:0;_bottom:0;_margin-top:3px;_margin-left:-1px;background-color:#000;opacity:.12;filter:alpha(opacity=12)}.yui-skin-sam .yui-dialog .ft{border-top:0;padding:0 10px 10px 10px;font-size:100%}.yui-skin-sam .yui-dialog .ft .button-group{display:block;text-align:right}.yui-skin-sam .yui-dialog .ft button.default{font-weight:bold}.yui-skin-sam .yui-dialog .ft span.default{border-color:#304369;background-position:0 -1400px}.yui-skin-sam .yui-dialog .ft span.default .first-child{border-color:#304369}.yui-skin-sam .yui-dialog .ft span.default button{color:#fff}.yui-skin-sam .yui-dialog .ft span.yui-button-disabled{background-position:0 -1500px;border-color:#ccc}.yui-skin-sam .yui-dialog .ft span.yui-button-disabled .first-child{border-color:#ccc}.yui-skin-sam .yui-dialog .ft span.yui-button-disabled button{color:#a6a6a6}.yui-skin-sam .yui-simple-dialog .bd .yui-icon{background:url(../js/yui/assets/skins/sam/sprite.png) no-repeat 0 0;width:16px;height:16px;margin-right:10px;float:left}.yui-skin-sam .yui-simple-dialog .bd span.blckicon{background-position:0 -1100px}.yui-skin-sam .yui-simple-dialog .bd span.alrticon{background-position:0 -1050px}.yui-skin-sam .yui-simple-dialog .bd span.hlpicon{background-position:0 -1150px}.yui-skin-sam .yui-simple-dialog .bd span.infoicon{background-position:0 -1200px}.yui-skin-sam .yui-simple-dialog .bd span.warnicon{background-position:0 -1900px}.yui-skin-sam .yui-simple-dialog .bd span.tipicon{background-position:0 -1250px}.yui-skin-sam .yui-tt .bd{position:relative;top:0;left:0;z-index:1;color:#000;padding:2px 5px;border-color:#d4c237 #A6982b #a6982b #A6982B;border-width:1px;border-style:solid;background-color:#ffee69}.yui-skin-sam .yui-tt.show-scrollbars .bd{overflow:auto}.yui-skin-sam .yui-tt-shadow{top:2px;right:-3px;left:-3px;bottom:-3px;background-color:#000}.yui-skin-sam .yui-tt-shadow-visible{opacity:.12;filter:alpha(opacity=12)}
+
+/*
+Copyright (c) 2011, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.9.0
+*/
+.yui-skin-sam .yui-dt-mask{position:absolute;z-index:9500}.yui-dt-tmp{position:absolute;left:-9000px}.yui-dt-scrollable .yui-dt-bd{overflow:auto}.yui-dt-scrollable .yui-dt-hd{overflow:hidden;position:relative}.yui-dt-scrollable .yui-dt-bd thead tr,.yui-dt-scrollable .yui-dt-bd thead th{position:absolute;left:-1500px}.yui-dt-scrollable tbody{-moz-outline:0}.yui-skin-sam thead .yui-dt-sortable{cursor:pointer}.yui-skin-sam thead .yui-dt-draggable{cursor:move}.yui-dt-coltarget{position:absolute;z-index:999}.yui-dt-hd{zoom:1}th.yui-dt-resizeable .yui-dt-resizerliner{position:relative}.yui-dt-resizer{position:absolute;right:0;bottom:0;height:100%;cursor:e-resize;cursor:col-resize;background-color:#CCC;opacity:0;filter:alpha(opacity=0)}.yui-dt-resizerproxy{visibility:hidden;position:absolute;z-index:9000;background-color:#CCC;opacity:0;filter:alpha(opacity=0)}th.yui-dt-hidden .yui-dt-liner,td.yui-dt-hidden .yui-dt-liner,th.yui-dt-hidden .yui-dt-resizer{display:none}.yui-dt-editor,.yui-dt-editor-shim{position:absolute;z-index:9000}.yui-skin-sam .yui-dt table{margin:0;padding:0;font-family:arial;font-size:inherit;border-collapse:separate;*border-collapse:collapse;border-spacing:0;border:1px solid #7f7f7f}.yui-skin-sam .yui-dt thead{border-spacing:0}.yui-skin-sam .yui-dt caption{color:#000;font-size:85%;font-weight:normal;font-style:italic;line-height:1;padding:1em 0;text-align:center}.yui-skin-sam .yui-dt th{background:#d8d8da url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 0}.yui-skin-sam .yui-dt th,.yui-skin-sam .yui-dt th a{font-weight:normal;text-decoration:none;color:#000;vertical-align:bottom}.yui-skin-sam .yui-dt th{margin:0;padding:0;border:0;border-right:1px solid #cbcbcb}.yui-skin-sam .yui-dt tr.yui-dt-first td{border-top:1px solid #7f7f7f}.yui-skin-sam .yui-dt th .yui-dt-liner{white-space:nowrap}.yui-skin-sam .yui-dt-liner{margin:0;padding:0;padding:4px 10px 4px 10px}.yui-skin-sam .yui-dt-coltarget{width:5px;background-color:red}.yui-skin-sam .yui-dt td{margin:0;padding:0;border:0;border-right:1px solid #cbcbcb;text-align:left}.yui-skin-sam .yui-dt-list td{border-right:0}.yui-skin-sam .yui-dt-resizer{width:6px}.yui-skin-sam .yui-dt-mask{background-color:#000;opacity:.25;filter:alpha(opacity=25)}.yui-skin-sam .yui-dt-message{background-color:#FFF}.yui-skin-sam .yui-dt-scrollable table{border:0}.yui-skin-sam .yui-dt-scrollable .yui-dt-hd{border-left:1px solid #7f7f7f;border-top:1px solid #7f7f7f;border-right:1px solid #7f7f7f}.yui-skin-sam .yui-dt-scrollable .yui-dt-bd{border-left:1px solid #7f7f7f;border-bottom:1px solid #7f7f7f;border-right:1px solid #7f7f7f;background-color:#FFF}.yui-skin-sam .yui-dt-scrollable .yui-dt-data tr.yui-dt-last td{border-bottom:1px solid #7f7f7f}.yui-skin-sam th.yui-dt-asc,.yui-skin-sam th.yui-dt-desc{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 -100px}.yui-skin-sam th.yui-dt-sortable .yui-dt-label{margin-right:10px}.yui-skin-sam th.yui-dt-asc .yui-dt-liner{background:url(../js/yui/assets/skins/sam/dt-arrow-up.png) no-repeat right}.yui-skin-sam th.yui-dt-desc .yui-dt-liner{background:url(../js/yui/assets/skins/sam/dt-arrow-dn.png) no-repeat right}tbody .yui-dt-editable{cursor:pointer}.yui-dt-editor{text-align:left;background-color:#f2f2f2;border:1px solid #808080;padding:6px}.yui-dt-editor label{padding-left:4px;padding-right:6px}.yui-dt-editor .yui-dt-button{padding-top:6px;text-align:right}.yui-dt-editor .yui-dt-button button{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 0;border:1px solid #999;width:4em;height:1.8em;margin-left:6px}.yui-dt-editor .yui-dt-button button.yui-dt-default{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 -1400px;background-color:#5584e0;border:1px solid #304369;color:#FFF}.yui-dt-editor .yui-dt-button button:hover{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 -1300px;color:#000}.yui-dt-editor .yui-dt-button button:active{background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 -1700px;color:#000}.yui-skin-sam tr.yui-dt-even{background-color:#FFF}.yui-skin-sam tr.yui-dt-odd{background-color:#edf5ff}.yui-skin-sam tr.yui-dt-even td.yui-dt-asc,.yui-skin-sam tr.yui-dt-even td.yui-dt-desc{background-color:#edf5ff}.yui-skin-sam tr.yui-dt-odd td.yui-dt-asc,.yui-skin-sam tr.yui-dt-odd td.yui-dt-desc{background-color:#dbeaff}.yui-skin-sam .yui-dt-list tr.yui-dt-even{background-color:#FFF}.yui-skin-sam .yui-dt-list tr.yui-dt-odd{background-color:#FFF}.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-asc,.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-desc{background-color:#edf5ff}.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-asc,.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-desc{background-color:#edf5ff}.yui-skin-sam th.yui-dt-highlighted,.yui-skin-sam th.yui-dt-highlighted a{background-color:#b2d2ff}.yui-skin-sam tr.yui-dt-highlighted,.yui-skin-sam tr.yui-dt-highlighted td.yui-dt-asc,.yui-skin-sam tr.yui-dt-highlighted td.yui-dt-desc,.yui-skin-sam tr.yui-dt-even td.yui-dt-highlighted,.yui-skin-sam tr.yui-dt-odd td.yui-dt-highlighted{cursor:pointer;background-color:#b2d2ff}.yui-skin-sam .yui-dt-list th.yui-dt-highlighted,.yui-skin-sam .yui-dt-list th.yui-dt-highlighted a{background-color:#b2d2ff}.yui-skin-sam .yui-dt-list tr.yui-dt-highlighted,.yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-asc,.yui-skin-sam .yui-dt-list tr.yui-dt-highlighted td.yui-dt-desc,.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-highlighted,.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-highlighted{cursor:pointer;background-color:#b2d2ff}.yui-skin-sam th.yui-dt-selected,.yui-skin-sam th.yui-dt-selected a{background-color:#446cd7}.yui-skin-sam tr.yui-dt-selected td,.yui-skin-sam tr.yui-dt-selected td.yui-dt-asc,.yui-skin-sam tr.yui-dt-selected td.yui-dt-desc{background-color:#426fd9;color:#FFF}.yui-skin-sam tr.yui-dt-even td.yui-dt-selected,.yui-skin-sam tr.yui-dt-odd td.yui-dt-selected{background-color:#446cd7;color:#FFF}.yui-skin-sam .yui-dt-list th.yui-dt-selected,.yui-skin-sam .yui-dt-list th.yui-dt-selected a{background-color:#446cd7}
+.yui-skin-sam .yui-dt-list tr.yui-dt-selected td,.yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-asc,.yui-skin-sam .yui-dt-list tr.yui-dt-selected td.yui-dt-desc{background-color:#426fd9;color:#FFF}.yui-skin-sam .yui-dt-list tr.yui-dt-even td.yui-dt-selected,.yui-skin-sam .yui-dt-list tr.yui-dt-odd td.yui-dt-selected{background-color:#446cd7;color:#FFF}.yui-skin-sam .yui-dt-paginator{display:block;margin:6px 0;white-space:nowrap}.yui-skin-sam .yui-dt-paginator .yui-dt-first,.yui-skin-sam .yui-dt-paginator .yui-dt-last,.yui-skin-sam .yui-dt-paginator .yui-dt-selected{padding:2px 6px}.yui-skin-sam .yui-dt-paginator a.yui-dt-first,.yui-skin-sam .yui-dt-paginator a.yui-dt-last{text-decoration:none}.yui-skin-sam .yui-dt-paginator .yui-dt-previous,.yui-skin-sam .yui-dt-paginator .yui-dt-next{display:none}.yui-skin-sam a.yui-dt-page{border:1px solid #cbcbcb;padding:2px 6px;text-decoration:none;background-color:#fff}.yui-skin-sam .yui-dt-selected{border:1px solid #fff;background-color:#fff}
+
+/*
+Copyright (c) 2011, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.9.0
+*/
+.yui-button{display:-moz-inline-box;display:inline-block;vertical-align:text-bottom;}.yui-button .first-child{display:block;*display:inline-block;}.yui-button button,.yui-button a{display:block;*display:inline-block;border:none;margin:0;}.yui-button button{background-color:transparent;*overflow:visible;cursor:pointer;}.yui-button a{text-decoration:none;}.yui-skin-sam .yui-button{border-width:1px 0;border-style:solid;border-color:#808080;background:url(../js/yui/assets/skins/sam/sprite.png) repeat-x 0 0;margin:auto .25em;}.yui-skin-sam .yui-button .first-child{border-width:0 1px;border-style:solid;border-color:#808080;margin:0 -1px;_margin:0;}.yui-skin-sam .yui-button button,.yui-skin-sam .yui-button a,.yui-skin-sam .yui-button a:visited{padding:0 10px;font-size:93%;line-height:2;*line-height:1.7;min-height:2em;*min-height:auto;color:#000;}.yui-skin-sam .yui-button a{*line-height:1.875;*padding-bottom:1px;}.yui-skin-sam .yui-split-button button,.yui-skin-sam .yui-menu-button button{padding-right:20px;background-position:right center;background-repeat:no-repeat;}.yui-skin-sam .yui-menu-button button{background-image:url(../js/yui/assets/skins/sam/menu-button-arrow.png);}.yui-skin-sam .yui-split-button button{background-image:url(../js/yui/assets/skins/sam/split-button-arrow.png);}.yui-skin-sam .yui-button-focus{border-color:#7D98B8;background-position:0 -1300px;}.yui-skin-sam .yui-button-focus .first-child{border-color:#7D98B8;}.yui-skin-sam .yui-split-button-focus button{background-image:url(../js/yui/assets/skins/sam/split-button-arrow-focus.png);}.yui-skin-sam .yui-button-hover{border-color:#7D98B8;background-position:0 -1300px;}.yui-skin-sam .yui-button-hover .first-child{border-color:#7D98B8;}.yui-skin-sam .yui-split-button-hover button{background-image:url(../js/yui/assets/skins/sam/split-button-arrow-hover.png);}.yui-skin-sam .yui-button-active{border-color:#7D98B8;background-position:0 -1700px;}.yui-skin-sam .yui-button-active .first-child{border-color:#7D98B8;}.yui-skin-sam .yui-split-button-activeoption{border-color:#808080;background-position:0 0;}.yui-skin-sam .yui-split-button-activeoption .first-child{border-color:#808080;}.yui-skin-sam .yui-split-button-activeoption button{background-image:url(../js/yui/assets/skins/sam/split-button-arrow-active.png);}.yui-skin-sam .yui-radio-button-checked,.yui-skin-sam .yui-checkbox-button-checked{border-color:#304369;background-position:0 -1400px;}.yui-skin-sam .yui-radio-button-checked .first-child,.yui-skin-sam .yui-checkbox-button-checked .first-child{border-color:#304369;}.yui-skin-sam .yui-radio-button-checked button,.yui-skin-sam .yui-checkbox-button-checked button{color:#fff;}.yui-skin-sam .yui-button-disabled{border-color:#ccc;background-position:0 -1500px;}.yui-skin-sam .yui-button-disabled .first-child{border-color:#ccc;}.yui-skin-sam .yui-button-disabled button,.yui-skin-sam .yui-button-disabled a,.yui-skin-sam .yui-button-disabled a:visited{color:#A6A6A6;cursor:default;}.yui-skin-sam .yui-menu-button-disabled button{background-image:url(../js/yui/assets/skins/sam/menu-button-arrow-disabled.png);}.yui-skin-sam .yui-split-button-disabled button{background-image:url(../js/yui/assets/skins/sam/split-button-arrow-disabled.png);}
+
+/*
+Copyright (c) 2011, Yahoo! Inc. All rights reserved.
+Code licensed under the BSD License:
+http://developer.yahoo.com/yui/license.html
+version: 2.9.0
+*/
+.yui-skin-sam .yui-pg-container{display:block;margin:6px 0;white-space:nowrap}.yui-skin-sam .yui-pg-first,.yui-skin-sam .yui-pg-previous,.yui-skin-sam .yui-pg-next,.yui-skin-sam .yui-pg-last,.yui-skin-sam .yui-pg-current,.yui-skin-sam .yui-pg-pages,.yui-skin-sam .yui-pg-page{display:inline-block;font-family:arial,helvetica,clean,sans-serif;padding:3px 6px;zoom:1}.yui-skin-sam .yui-pg-pages{padding:0}.yui-skin-sam .yui-pg-current{padding:3px 0}.yui-skin-sam a.yui-pg-first:link,.yui-skin-sam a.yui-pg-first:visited,.yui-skin-sam a.yui-pg-first:active,.yui-skin-sam a.yui-pg-first:hover,.yui-skin-sam a.yui-pg-previous:link,.yui-skin-sam a.yui-pg-previous:visited,.yui-skin-sam a.yui-pg-previous:active,.yui-skin-sam a.yui-pg-previous:hover,.yui-skin-sam a.yui-pg-next:link,.yui-skin-sam a.yui-pg-next:visited,.yui-skin-sam a.yui-pg-next:active,.yui-skin-sam a.yui-pg-next:hover,.yui-skin-sam a.yui-pg-last:link,.yui-skin-sam a.yui-pg-last:visited,.yui-skin-sam a.yui-pg-last:active,.yui-skin-sam a.yui-pg-last:hover,.yui-skin-sam a.yui-pg-page:link,.yui-skin-sam a.yui-pg-page:visited,.yui-skin-sam a.yui-pg-page:active,.yui-skin-sam a.yui-pg-page:hover{color:#06c;text-decoration:underline;outline:0}.yui-skin-sam span.yui-pg-first,.yui-skin-sam span.yui-pg-previous,.yui-skin-sam span.yui-pg-next,.yui-skin-sam span.yui-pg-last{color:#a6a6a6}.yui-skin-sam .yui-pg-page{background-color:#fff;border:1px solid #cbcbcb;padding:2px 6px;text-decoration:none}.yui-skin-sam .yui-pg-current-page{background-color:transparent;border:0;font-weight:bold;padding:3px 6px}.yui-skin-sam .yui-pg-page{margin-left:1px;margin-right:1px}.yui-skin-sam .yui-pg-first,.yui-skin-sam .yui-pg-previous{padding-left:0}.yui-skin-sam .yui-pg-next,.yui-skin-sam .yui-pg-last{padding-right:0}.yui-skin-sam .yui-pg-current,.yui-skin-sam .yui-pg-rpp-options{margin-left:1em;margin-right:1em}
diff --git a/devtools/client/styleeditor/test/doc_short_string.css b/devtools/client/styleeditor/test/doc_short_string.css
new file mode 100644
index 0000000000..585dccea1a
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_short_string.css
@@ -0,0 +1,15 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* ☺ */
+
+body {
+ background: white;
+}
+
+div {
+ font-size: 4em;
+}
+
+div > span {
+ text-decoration: underline;
+}
diff --git a/devtools/client/styleeditor/test/doc_sourcemap_chrome.html b/devtools/client/styleeditor/test/doc_sourcemap_chrome.html
new file mode 100644
index 0000000000..5052897708
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_sourcemap_chrome.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Testcase for sourcemap URLs pointing to unsupported protocols</title>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/sourcemaps_chrome.css"/>
+</head>
+<body>
+ <div>Protocol test</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/doc_xulpage.xhtml b/devtools/client/styleeditor/test/doc_xulpage.xhtml
new file mode 100644
index 0000000000..41385afa4a
--- /dev/null
+++ b/devtools/client/styleeditor/test/doc_xulpage.xhtml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="simple.css" type="text/css"?>
+<!DOCTYPE window>
+<window xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <label value="Simple XUL document" />
+</window>
diff --git a/devtools/client/styleeditor/test/four.html b/devtools/client/styleeditor/test/four.html
new file mode 100644
index 0000000000..c0d51d691c
--- /dev/null
+++ b/devtools/client/styleeditor/test/four.html
@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+<head>
+ <title>four stylesheets</title>
+ <link rel="stylesheet" type="text/css" media="scren" href="simple.css"/>
+ <style type="text/css">
+ div {
+ font-size: 2em;
+ }
+ </style>
+ <style type="text/css">
+ span {
+ font-size: 3em;
+ }
+ </style>
+ <style type="text/css">
+ p {
+ font-size: 4em;
+ }
+ </style>
+</head>
+<body>
+ <div>four <span>stylesheets</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/head.js b/devtools/client/styleeditor/test/head.js
new file mode 100644
index 0000000000..f685eca9bb
--- /dev/null
+++ b/devtools/client/styleeditor/test/head.js
@@ -0,0 +1,201 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* All top-level definitions here are exports. */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/shared/test/head.js",
+ this
+);
+
+const TEST_BASE =
+ "chrome://mochitests/content/browser/devtools/client/styleeditor/test/";
+const TEST_BASE_HTTP =
+ "http://example.com/browser/devtools/client/styleeditor/test/";
+const TEST_BASE_HTTPS =
+ "https://example.com/browser/devtools/client/styleeditor/test/";
+const TEST_HOST = "mochi.test:8888";
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @param {Window} win The window to add the tab to (default: current window).
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+var addTab = function (url, win) {
+ info("Adding a new tab with URL: '" + url + "'");
+
+ return new Promise(resolve => {
+ const targetWindow = win || window;
+ const targetBrowser = targetWindow.gBrowser;
+
+ const tab = (targetBrowser.selectedTab = BrowserTestUtils.addTab(
+ targetBrowser,
+ url
+ ));
+ BrowserTestUtils.browserLoaded(targetBrowser.selectedBrowser).then(
+ function () {
+ info("URL '" + url + "' loading complete");
+ resolve(tab);
+ }
+ );
+ });
+};
+
+var navigateToAndWaitForStyleSheets = async function (url, ui, editorCount) {
+ const onClear = ui.once("stylesheets-clear");
+ await navigateTo(url);
+ await onClear;
+ await waitUntil(() => ui.editors.length === editorCount);
+};
+
+var reloadPageAndWaitForStyleSheets = async function (ui, editorCount) {
+ info("Reloading the page.");
+
+ const onClear = ui.once("stylesheets-clear");
+ let count = 0;
+ const onAllEditorAdded = new Promise(res => {
+ const off = ui.on("editor-added", editor => {
+ count++;
+ info(`Received ${editor.friendlyName} (${count}/${editorCount})`);
+ if (count == editorCount) {
+ res();
+ off();
+ }
+ });
+ });
+
+ await reloadBrowser();
+ await onClear;
+
+ await onAllEditorAdded;
+ info("All expected editors added");
+};
+
+/**
+ * Open the style editor for the current tab.
+ */
+var openStyleEditor = async function (tab) {
+ if (!tab) {
+ tab = gBrowser.selectedTab;
+ }
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "styleeditor",
+ });
+ const panel = toolbox.getPanel("styleeditor");
+ const ui = panel.UI;
+
+ return { toolbox, panel, ui };
+};
+
+/**
+ * Creates a new tab in specified window navigates it to the given URL and
+ * opens style editor in it.
+ */
+var openStyleEditorForURL = async function (url, win) {
+ const tab = await addTab(url, win);
+ const result = await openStyleEditor(tab);
+ result.tab = tab;
+ return result;
+};
+
+/**
+ * Send an async message to the frame script and get back the requested
+ * computed style property.
+ *
+ * @param {String} selector
+ * The selector used to obtain the element.
+ * @param {String} pseudo
+ * pseudo id to query, or null.
+ * @param {String} name
+ * name of the property.
+ */
+var getComputedStyleProperty = async function (args) {
+ return SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [args],
+ function ({ selector, pseudo, name }) {
+ const element = content.document.querySelector(selector);
+ const style = content.getComputedStyle(element, pseudo);
+ return style.getPropertyValue(name);
+ }
+ );
+};
+
+/**
+ * Wait for "at-rules-list-changed" events to settle on StyleEditorUI.
+ * Returns a promise that resolves the number of events caught while waiting.
+ *
+ * @param {StyleEditorUI} ui
+ * Current StyleEditorUI on which at-rules-list-changed events should be fired.
+ * @param {Number} delay
+ */
+function waitForManyEvents(ui, delay) {
+ return new Promise(resolve => {
+ let timer;
+ let count = 0;
+ const onEvent = () => {
+ count++;
+ clearTimeout(timer);
+
+ // Wait for some time to catch subsequent events.
+ timer = setTimeout(() => {
+ // Remove the listener and resolve.
+ ui.off("at-rules-list-changed", onEvent);
+ resolve(count);
+ }, delay);
+ };
+ ui.on("at-rules-list-changed", onEvent);
+ });
+}
+
+/**
+ * Creates a new style sheet in the Style Editor
+
+ * @param {StyleEditorUI} ui
+ * Current StyleEditorUI on which to simulate pressing the + button.
+ * @param {Window} panelWindow
+ * The panelWindow property of the current Style Editor panel.
+ */
+function createNewStyleSheet(ui, panelWindow) {
+ info("Creating a new stylesheet now");
+
+ return new Promise(resolve => {
+ ui.once("editor-added", editor => {
+ editor.getSourceEditor().then(resolve);
+ });
+
+ waitForFocus(function () {
+ // create a new style sheet
+ const newButton = panelWindow.document.querySelector(
+ ".style-editor-newButton"
+ );
+ ok(newButton, "'new' button exists");
+
+ EventUtils.synthesizeMouseAtCenter(newButton, {}, panelWindow);
+ }, panelWindow);
+ });
+}
+
+/**
+ * Returns the panel root element (StyleEditorUI._root)
+ *
+ * @param {StyleEditorPanel} panel
+ * @returns {Element}
+ */
+function getRootElement(panel) {
+ return panel.panelWindow.document.getElementById("style-editor-chrome");
+}
+
+/**
+ * Returns the panel context menu element
+ *
+ * @param {StyleEditorPanel} panel
+ * @returns {Element}
+ */
+function getContextMenuElement(panel) {
+ return panel.panelWindow.document.getElementById("sidebar-context");
+}
diff --git a/devtools/client/styleeditor/test/iframe_service_worker.js b/devtools/client/styleeditor/test/iframe_service_worker.js
new file mode 100644
index 0000000000..56c32c204a
--- /dev/null
+++ b/devtools/client/styleeditor/test/iframe_service_worker.js
@@ -0,0 +1,12 @@
+"use strict";
+
+self.onfetch = function (event) {
+ if (event.request.url.includes("sheet.css")) {
+ return event.respondWith(new Response("* { color: green; }"));
+ }
+ return null;
+};
+
+self.onactivate = function (event) {
+ event.waitUntil(self.clients.claim());
+};
diff --git a/devtools/client/styleeditor/test/iframe_with_service_worker.html b/devtools/client/styleeditor/test/iframe_with_service_worker.html
new file mode 100644
index 0000000000..690515775e
--- /dev/null
+++ b/devtools/client/styleeditor/test/iframe_with_service_worker.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+Iframe loading a stylesheet via a service worker
+<script>
+"use strict";
+
+function waitForActive(swr) {
+ const sw = swr.installing || swr.waiting || swr.active;
+ return new Promise(resolve => {
+ if (sw.state === "activated") {
+ resolve(swr);
+ return;
+ }
+ sw.addEventListener("statechange", function onStateChange(evt) {
+ if (sw.state === "activated") {
+ sw.removeEventListener("statechange", onStateChange);
+ resolve(swr);
+ }
+ });
+ });
+}
+
+navigator.serviceWorker.register("iframe_service_worker.js", {scope: "."})
+ .then(registration => waitForActive(registration))
+ .then(() => {
+ const link = document.createElement("link");
+ link.setAttribute("rel", "stylesheet");
+ link.setAttribute("type", "text/css");
+ link.setAttribute("href", "sheet.css");
+ document.documentElement.appendChild(link);
+ });
+</script>
diff --git a/devtools/client/styleeditor/test/import.css b/devtools/client/styleeditor/test/import.css
new file mode 100644
index 0000000000..485688c0e6
--- /dev/null
+++ b/devtools/client/styleeditor/test/import.css
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+@import url(import2.css);
+
+body {
+ margin: 0;
+}
diff --git a/devtools/client/styleeditor/test/import.html b/devtools/client/styleeditor/test/import.html
new file mode 100644
index 0000000000..bc92baeba0
--- /dev/null
+++ b/devtools/client/styleeditor/test/import.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>import testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="import.css"/>
+</head>
+<body>
+ <div>import <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/import2.css b/devtools/client/styleeditor/test/import2.css
new file mode 100644
index 0000000000..6037d4b10c
--- /dev/null
+++ b/devtools/client/styleeditor/test/import2.css
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+@import url(import.css);
+
+p {
+ padding: 5px;
+}
diff --git a/devtools/client/styleeditor/test/inline-1.html b/devtools/client/styleeditor/test/inline-1.html
new file mode 100644
index 0000000000..76478893b2
--- /dev/null
+++ b/devtools/client/styleeditor/test/inline-1.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Inline test page #1</title>
+ <style type="text/css">
+ .second {
+ font-size:2em;
+ }
+ </style>
+ <style type="text/css">
+ .first {
+ font-size:3em;
+ }
+ </style>
+ </head>
+ <body class="first">
+ Inline test page #1
+ </body>
+</html>
diff --git a/devtools/client/styleeditor/test/inline-2.html b/devtools/client/styleeditor/test/inline-2.html
new file mode 100644
index 0000000000..e25285c31e
--- /dev/null
+++ b/devtools/client/styleeditor/test/inline-2.html
@@ -0,0 +1,19 @@
+<!doctype html>
+<html>
+ <head>
+ <title>Inline test page #2</title>
+ <style type="text/css">
+ .second {
+ font-size:2em;
+ }
+ </style>
+ <style type="text/css">
+ .first {
+ font-size:3em;
+ }
+ </style>
+ </head>
+ <body class="second">
+ Inline test page #2
+ </body>
+</html>
diff --git a/devtools/client/styleeditor/test/longload.html b/devtools/client/styleeditor/test/longload.html
new file mode 100644
index 0000000000..30a3802798
--- /dev/null
+++ b/devtools/client/styleeditor/test/longload.html
@@ -0,0 +1,29 @@
+<!doctype html>
+<html>
+<head>
+ <title>Long load</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <style type="text/css">
+ body {
+ background: white;
+ }
+
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+ </style>
+</head>
+<body>
+ Time passes:
+ <script type="application/javascript">
+ "use strict";
+ for (let i = 0; i < 5000; i++) {
+ document.write("<br>...");
+ }
+ </script>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/longname.html b/devtools/client/styleeditor/test/longname.html
new file mode 100644
index 0000000000..111e67a2b9
--- /dev/null
+++ b/devtools/client/styleeditor/test/longname.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>long name testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="veryveryverylongnamethatcanbreakthestyleeditor.css"/>
+</head>
+<body>
+ <div>long name testcase</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/many-media-rules-sourcemaps/index.html b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/index.html
new file mode 100644
index 0000000000..9734231b99
--- /dev/null
+++ b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/index.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title></title>
+ <link rel="stylesheet" type="text/css" href="sourcemap/sourcemap-css/sourcemaps.css"/>
+</head>
+
+<body>
+
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css
new file mode 100644
index 0000000000..edb7860289
--- /dev/null
+++ b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css
@@ -0,0 +1,201 @@
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: green; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+@media (min-width: 500px) {
+ div {
+ color: red; } }
+
+/*# sourceMappingURL=sourcemaps.css.map */
diff --git a/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css.map b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css.map
new file mode 100644
index 0000000000..f792f65c4e
--- /dev/null
+++ b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-css/sourcemaps.css.map
@@ -0,0 +1,10 @@
+{
+ "version": 3,
+ "file": "sourcemaps.css",
+ "sources": [
+ "../sourcemap-sass/sourcemaps.scss",
+ "../sourcemap-sass/_partial.scss"
+ ],
+ "names": [],
+ "mappings": "ACAA,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;AACnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,KAAK,GAAI;;ADtBnD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI;;AACjD,MAAM,EAAE,SAAS,EAAE,KAAK;EAAK,AAAA,GAAG,CAAC;IAAE,KAAK,EAAE,GAAG,GAAI"
+} \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/_partial.scss b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/_partial.scss
new file mode 100644
index 0000000000..7291ae831b
--- /dev/null
+++ b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/_partial.scss
@@ -0,0 +1,25 @@
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
+@media (min-width: 500px) { div { color: green; } }
diff --git a/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/sourcemaps.scss b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/sourcemaps.scss
new file mode 100644
index 0000000000..a8c25f0d85
--- /dev/null
+++ b/devtools/client/styleeditor/test/many-media-rules-sourcemaps/sourcemap/sourcemap-sass/sourcemaps.scss
@@ -0,0 +1,27 @@
+@import 'partial';
+
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
+@media (min-width: 500px) { div { color: red; } }
diff --git a/devtools/client/styleeditor/test/media-rules-sourcemaps.html b/devtools/client/styleeditor/test/media-rules-sourcemaps.html
new file mode 100644
index 0000000000..ba18c35503
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-rules-sourcemaps.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/media-rules.css"
+</head>
+<body>
+ <div>
+ Testing style editor media sidebar with source maps
+ </div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/media-rules.css b/devtools/client/styleeditor/test/media-rules.css
new file mode 100644
index 0000000000..1adce84c4b
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-rules.css
@@ -0,0 +1,29 @@
+@media not all {
+ div {
+ color: blue;
+ }
+}
+
+@media all {
+ div {
+ color: red;
+ }
+}
+
+div {
+ width: 20px;
+ height: 20px;
+ background-color: ghostwhite;
+}
+
+@media (max-width: 550px) {
+ div {
+ color: green;
+ }
+}
+
+@media (min-height: 300px) and (max-height: 320px) {
+ div {
+ color: orange;
+ }
+}
diff --git a/devtools/client/styleeditor/test/media-rules.html b/devtools/client/styleeditor/test/media-rules.html
new file mode 100644
index 0000000000..76725bfb54
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-rules.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <link rel="stylesheet" href="simple.css"/>
+ <link rel="stylesheet" href="media-rules.css"/>
+ <!-- This stylesheet is needed to ensure we cover the fix for Bug 1779043 -->
+ <style>
+ div {
+ color: salmon;
+ }
+ </style>
+ <style>
+ @media screen {
+ div {
+ outline: 1px solid tomato;
+ }
+
+ @supports (display: flex) {
+ @media (1px < height < 10000px) {
+ div {
+ display: flex;
+ }
+ }
+ }
+ }
+
+ @layer myLayer {}
+ @container (min-width: 1px) {
+ body {
+ div {
+ color: gold;
+ @supports selector(&) {
+ &:hover {
+ color: yellowgreen;
+ }
+ }
+ }
+ }
+ }
+ </style>
+</head>
+<body>
+ <div>
+ Testing style editor media sidebar
+ </div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/media-small.css b/devtools/client/styleeditor/test/media-small.css
new file mode 100644
index 0000000000..ea21db4447
--- /dev/null
+++ b/devtools/client/styleeditor/test/media-small.css
@@ -0,0 +1,4 @@
+/* this stylesheet applies when min-width<400px */
+body {
+ background: red;
+}
diff --git a/devtools/client/styleeditor/test/media.html b/devtools/client/styleeditor/test/media.html
new file mode 100644
index 0000000000..81222edb8e
--- /dev/null
+++ b/devtools/client/styleeditor/test/media.html
@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+<head>
+ <link rel="stylesheet" type="text/css" href="simple.css" media="screen,print"/>
+ <link rel="stylesheet" type="text/css" href="media-small.css" media="screen and (min-width: 200px)"/>
+</head>
+<body>
+ <div>test for media labels</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/minified.html b/devtools/client/styleeditor/test/minified.html
new file mode 100644
index 0000000000..d592e49b96
--- /dev/null
+++ b/devtools/client/styleeditor/test/minified.html
@@ -0,0 +1,16 @@
+<!doctype html>
+<html>
+<head>
+ <title>minified testcase</title>
+ <link rel="stylesheet" href="pretty.css"/>
+ <style type="text/css">body { background: red; }
+div {
+font-size: 5em;
+color: red;
+}</style>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/test-stylus.css"/>
+</head>
+<body>
+ <div>minified <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/missing.html b/devtools/client/styleeditor/test/missing.html
new file mode 100644
index 0000000000..ce4ec08be8
--- /dev/null
+++ b/devtools/client/styleeditor/test/missing.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>missing stylesheet testcase</title>
+ <link rel="stylesheet" charset="utf-8" type="text/css" media="screen" href="missing-stylesheet.css"/>
+ <link rel="stylesheet" charset="utf-8" type="text/css" media="screen" href="simple.css"/>
+</head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/nostyle.html b/devtools/client/styleeditor/test/nostyle.html
new file mode 100644
index 0000000000..951945a72b
--- /dev/null
+++ b/devtools/client/styleeditor/test/nostyle.html
@@ -0,0 +1,5 @@
+<html>
+ <div>
+ Page with no stylesheets
+ </div>
+</html>
diff --git a/devtools/client/styleeditor/test/pretty.css b/devtools/client/styleeditor/test/pretty.css
new file mode 100644
index 0000000000..72965c0b23
--- /dev/null
+++ b/devtools/client/styleeditor/test/pretty.css
@@ -0,0 +1,2 @@
+
+body{background:white;}div{font-size:4em;color:red}span{color:green;@media screen { background: blue; &>.myClass {padding: 1em} }}
diff --git a/devtools/client/styleeditor/test/resources_inpage.jsi b/devtools/client/styleeditor/test/resources_inpage.jsi
new file mode 100644
index 0000000000..8b7895af52
--- /dev/null
+++ b/devtools/client/styleeditor/test/resources_inpage.jsi
@@ -0,0 +1,12 @@
+
+// This script is used from within browser_styleeditor_cmd_edit.html
+
+window.addEventListener('load', function() {
+ var pid = document.getElementById('pid');
+ var h3 = document.createElement('h3');
+ h3.id = 'h3id';
+ h3.classList.add('h3class');
+ h3.appendChild(document.createTextNode('h3'));
+ h3.setAttribute('data-a1', 'h3');
+ pid.parentNode.appendChild(h3);
+});
diff --git a/devtools/client/styleeditor/test/resources_inpage1.css b/devtools/client/styleeditor/test/resources_inpage1.css
new file mode 100644
index 0000000000..644deaaea7
--- /dev/null
+++ b/devtools/client/styleeditor/test/resources_inpage1.css
@@ -0,0 +1,11 @@
+@charset "utf-8";
+
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#pid { border-top: 2px dotted #F00; }
+#divid { border-top: 2px dotted #00F; }
+#h4id { border-top: 2px dotted #0F0; }
+#h3id { border-top: 2px dotted #FF0; }
diff --git a/devtools/client/styleeditor/test/resources_inpage2.css b/devtools/client/styleeditor/test/resources_inpage2.css
new file mode 100644
index 0000000000..e4fa48e530
--- /dev/null
+++ b/devtools/client/styleeditor/test/resources_inpage2.css
@@ -0,0 +1,11 @@
+@charset "utf-8";
+
+/*
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+*[data-a1=p] { border-left: 4px solid #F00; }
+*[data-a1=div] { border-left: 4px solid #00F; }
+*[data-a1=h4] { border-left: 4px solid #0F0; }
+*[data-a1=h3] { border-left: 4px solid #FF0; }
diff --git a/devtools/client/styleeditor/test/selector-highlighter.html b/devtools/client/styleeditor/test/selector-highlighter.html
new file mode 100644
index 0000000000..e3b2f6f69c
--- /dev/null
+++ b/devtools/client/styleeditor/test/selector-highlighter.html
@@ -0,0 +1 @@
+<style>div{color:red}</style><div>highlighter test</div>
diff --git a/devtools/client/styleeditor/test/simple.css b/devtools/client/styleeditor/test/simple.css
new file mode 100644
index 0000000000..4d737f305e
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.css
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* ☺ */
+
+body {
+ margin: 0;
+}
diff --git a/devtools/client/styleeditor/test/simple.css.gz b/devtools/client/styleeditor/test/simple.css.gz
new file mode 100644
index 0000000000..ee3b9efbc1
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.css.gz
Binary files differ
diff --git a/devtools/client/styleeditor/test/simple.css.gz^headers^ b/devtools/client/styleeditor/test/simple.css.gz^headers^
new file mode 100644
index 0000000000..092020ab00
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.css.gz^headers^
@@ -0,0 +1,4 @@
+Vary: Accept-Encoding
+Content-Encoding: gzip
+Content-Type: text/css
+
diff --git a/devtools/client/styleeditor/test/simple.gz.html b/devtools/client/styleeditor/test/simple.gz.html
new file mode 100644
index 0000000000..d63362b8e0
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.gz.html
@@ -0,0 +1,23 @@
+<!doctype html>
+<html>
+<head>
+ <title>simple testcase</title>
+ <link rel="stylesheet" type="text/css" media="scren" href="simple.css.gz"/>
+ <style type="text/css">
+ body {
+ background: white;
+ }
+
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+ </style>
+</head>
+<body>
+ <div>simple <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/simple.html b/devtools/client/styleeditor/test/simple.html
new file mode 100644
index 0000000000..8f25cdf61e
--- /dev/null
+++ b/devtools/client/styleeditor/test/simple.html
@@ -0,0 +1,24 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>simple testcase</title>
+ <link rel="stylesheet" charset="UTF-8" type="text/css" media="screen" href="simple.css"/>
+ <style type="text/css">
+ body {
+ background: white;
+ }
+
+ div {
+ font-size: 4em;
+ }
+
+ div > span {
+ text-decoration: underline;
+ }
+ </style>
+</head>
+<body>
+ <div>simple <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sjs_huge-css-server.sjs b/devtools/client/styleeditor/test/sjs_huge-css-server.sjs
new file mode 100644
index 0000000000..480fb1ca91
--- /dev/null
+++ b/devtools/client/styleeditor/test/sjs_huge-css-server.sjs
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+function handleRequest(request, response) {
+ response.setStatusLine(request.httpVersion, 200, "Och Aye");
+
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ response.setHeader("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+ response.setHeader("Content-Type", "text/plain; charset=utf-8", false);
+
+ // Taken from devtools/shared/webconsole/network-monitor
+ const NETMONITOR_LIMIT = 1048576;
+
+ // 2 * NETMONITOR_LIMIT reaches the exact limit for the netmonitor
+ // 3 * NETMONITOR_LIMIT makes sure we go past it.
+ response.write("x".repeat(3 * NETMONITOR_LIMIT));
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-css/contained.css b/devtools/client/styleeditor/test/sourcemap-css/contained.css
new file mode 100644
index 0000000000..79572f6065
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/contained.css
@@ -0,0 +1,4 @@
+#header {
+ color: #f06; }
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiIiwic291cmNlcyI6WyJzYXNzL2NvbnRhaW5lZC5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUVBO0VBQ0UsT0FISyIsInNvdXJjZXNDb250ZW50IjpbIiRwaW5rOiAjZjA2O1xuXG4jaGVhZGVyIHtcbiAgY29sb3I6ICRwaW5rO1xufSJdfQ==*/ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-css/media-rules.css b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css
new file mode 100644
index 0000000000..d4283200fd
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css
@@ -0,0 +1,8 @@
+@media screen and (max-width: 320px) {
+ div {
+ width: 100px; } }
+@media screen and (min-width: 1200px) {
+ div {
+ width: 400px; } }
+
+/*# sourceMappingURL=media-rules.css.map */
diff --git a/devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map
new file mode 100644
index 0000000000..76cd48fe23
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/media-rules.css.map
@@ -0,0 +1,6 @@
+{
+"version": 3,
+"mappings": "AAIE,oCAA4C;EAD9C,GAAI;IAEA,KAAK,EAAE,KAAK;AAEd,qCAA4C;EAJ9C,GAAI;IAKA,KAAK,EAAE,KAAK",
+"sources": ["../sourcemap-sass/media-rules.scss"],
+"file": "media-rules.css"
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css
new file mode 100644
index 0000000000..7246a9082a
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css
@@ -0,0 +1,7 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=sourcemaps.css.map */ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map
new file mode 100644
index 0000000000..2e8f2911ce
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map
@@ -0,0 +1,6 @@
+{
+"version": 3,
+"mappings": "AAGA,GAAI;EACF,KAAK,EAHU,OAAI;;AAMrB,IAAK;EACH,gBAAgB,EAAE,IAAI",
+"sources": ["../sourcemap-sass/sourcemaps.scss"],
+"file": "sourcemaps.css"
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map^headers^ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map^headers^
new file mode 100644
index 0000000000..866f3e2fb0
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map^headers^
@@ -0,0 +1,2 @@
+X-Content-Type-Options: nosniff
+Content-Type: text/plain
diff --git a/devtools/client/styleeditor/test/sourcemap-css/sourcemaps_chrome.css b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps_chrome.css
new file mode 100644
index 0000000000..f6713bf1ea
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/sourcemaps_chrome.css
@@ -0,0 +1,7 @@
+div {
+ color: #ff0066; }
+
+span {
+ background-color: #EEE; }
+
+/*# sourceMappingURL=chrome://mochitests/content/browser/devtools/client/styleeditor/test/sourcemap-css/sourcemaps.css.map */
diff --git a/devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css b/devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css
new file mode 100644
index 0000000000..e943c6ef4e
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/test-bootstrap-scss.css
@@ -0,0 +1,4513 @@
+/*! normalize.css v3.0.1 | MIT License | git.io/normalize */
+html {
+ font-family: sans-serif;
+ -ms-text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%; }
+
+body {
+ margin: 0; }
+
+article, aside, details, figcaption, figure, footer, header, hgroup, main, nav, section, summary {
+ display: block; }
+
+audio, canvas, progress, video {
+ display: inline-block;
+ vertical-align: baseline; }
+
+audio:not([controls]) {
+ display: none;
+ height: 0; }
+
+[hidden], template {
+ display: none; }
+
+a {
+ background: transparent; }
+
+a:active, a:hover {
+ outline: 0; }
+
+abbr[title] {
+ border-bottom: 1px dotted; }
+
+b, strong {
+ font-weight: bold; }
+
+dfn {
+ font-style: italic; }
+
+h1 {
+ font-size: 2em;
+ margin: 0.67em 0; }
+
+mark {
+ background: #ff0;
+ color: #000; }
+
+small {
+ font-size: 80%; }
+
+sub, sup {
+ font-size: 75%;
+ line-height: 0;
+ position: relative;
+ vertical-align: baseline; }
+
+sup {
+ top: -0.5em; }
+
+sub {
+ bottom: -0.25em; }
+
+img {
+ border: 0; }
+
+svg:not(:root) {
+ overflow: hidden; }
+
+figure {
+ margin: 1em 40px; }
+
+hr {
+ box-sizing: content-box;
+ height: 0; }
+
+pre {
+ overflow: auto; }
+
+code, kbd, pre, samp {
+ font-family: monospace, monospace;
+ font-size: 1em; }
+
+button, input, optgroup, select, textarea {
+ color: inherit;
+ font: inherit;
+ margin: 0; }
+
+button {
+ overflow: visible; }
+
+button, select {
+ text-transform: none; }
+
+button, html input[type="button"], input[type="reset"], input[type="submit"] {
+ -webkit-appearance: button;
+ cursor: pointer; }
+
+button[disabled], html input[disabled] {
+ cursor: default; }
+
+button::-moz-focus-inner, input::-moz-focus-inner {
+ border: 0;
+ padding: 0; }
+
+input {
+ line-height: normal; }
+
+input[type="checkbox"], input[type="radio"] {
+ box-sizing: border-box;
+ padding: 0; }
+
+input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button {
+ height: auto; }
+
+input[type="search"] {
+ -webkit-appearance: textfield;
+ box-sizing: content-box; }
+
+input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration {
+ -webkit-appearance: none; }
+
+fieldset {
+ border: 1px solid #c0c0c0;
+ margin: 0 2px;
+ padding: 0.35em 0.625em 0.75em; }
+
+legend {
+ border: 0;
+ padding: 0; }
+
+textarea {
+ overflow: auto; }
+
+optgroup {
+ font-weight: bold; }
+
+table {
+ border-collapse: collapse;
+ border-spacing: 0; }
+
+td, th {
+ padding: 0; }
+
+@media print {
+ * {
+ text-shadow: none !important;
+ color: #000 !important;
+ background: transparent !important;
+ box-shadow: none !important; }
+ a, a:visited {
+ text-decoration: underline; }
+ a[href]:after {
+ content: " (" attr(href) ")"; }
+ abbr[title]:after {
+ content: " (" attr(title) ")"; }
+ a[href^="javascript:"]:after, a[href^="#"]:after {
+ content: ""; }
+ pre, blockquote {
+ border: 1px solid #999;
+ page-break-inside: avoid; }
+ thead {
+ display: table-header-group; }
+ tr, img {
+ page-break-inside: avoid; }
+ img {
+ max-width: 100% !important; }
+ p, h2, h3 {
+ orphans: 3;
+ widows: 3; }
+ h2, h3 {
+ page-break-after: avoid; }
+ select {
+ background: #fff !important; }
+ .navbar {
+ display: none; }
+ .table td, .table th {
+ background-color: #fff !important; }
+ .btn > .caret, .dropup > .btn > .caret {
+ border-top-color: #000 !important; }
+ .label {
+ border: 1px solid #000; }
+ .table {
+ border-collapse: collapse !important; }
+ .table-bordered th, .table-bordered td {
+ border: 1px solid #ddd !important; } }
+
+@font-face {
+ font-family: 'Glyphicons Halflings';
+ src: url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot');
+ src: url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.woff') format('woff'), url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.ttf') format('truetype'), url('../bower_components/bootstrap-sass-official/vendor/assets/fonts/bootstrap/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); }
+
+.glyphicon {
+ position: relative;
+ top: 1px;
+ display: inline-block;
+ font-family: 'Glyphicons Halflings';
+ font-style: normal;
+ font-weight: normal;
+ line-height: 1;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale; }
+
+.glyphicon-asterisk:before {
+ content: "\2a"; }
+
+.glyphicon-plus:before {
+ content: "\2b"; }
+
+.glyphicon-euro:before {
+ content: "\20ac"; }
+
+.glyphicon-minus:before {
+ content: "\2212"; }
+
+.glyphicon-cloud:before {
+ content: "\2601"; }
+
+.glyphicon-envelope:before {
+ content: "\2709"; }
+
+.glyphicon-pencil:before {
+ content: "\270f"; }
+
+.glyphicon-glass:before {
+ content: "\e001"; }
+
+.glyphicon-music:before {
+ content: "\e002"; }
+
+.glyphicon-search:before {
+ content: "\e003"; }
+
+.glyphicon-heart:before {
+ content: "\e005"; }
+
+.glyphicon-star:before {
+ content: "\e006"; }
+
+.glyphicon-star-empty:before {
+ content: "\e007"; }
+
+.glyphicon-user:before {
+ content: "\e008"; }
+
+.glyphicon-film:before {
+ content: "\e009"; }
+
+.glyphicon-th-large:before {
+ content: "\e010"; }
+
+.glyphicon-th:before {
+ content: "\e011"; }
+
+.glyphicon-th-list:before {
+ content: "\e012"; }
+
+.glyphicon-ok:before {
+ content: "\e013"; }
+
+.glyphicon-remove:before {
+ content: "\e014"; }
+
+.glyphicon-zoom-in:before {
+ content: "\e015"; }
+
+.glyphicon-zoom-out:before {
+ content: "\e016"; }
+
+.glyphicon-off:before {
+ content: "\e017"; }
+
+.glyphicon-signal:before {
+ content: "\e018"; }
+
+.glyphicon-cog:before {
+ content: "\e019"; }
+
+.glyphicon-trash:before {
+ content: "\e020"; }
+
+.glyphicon-home:before {
+ content: "\e021"; }
+
+.glyphicon-file:before {
+ content: "\e022"; }
+
+.glyphicon-time:before {
+ content: "\e023"; }
+
+.glyphicon-road:before {
+ content: "\e024"; }
+
+.glyphicon-download-alt:before {
+ content: "\e025"; }
+
+.glyphicon-download:before {
+ content: "\e026"; }
+
+.glyphicon-upload:before {
+ content: "\e027"; }
+
+.glyphicon-inbox:before {
+ content: "\e028"; }
+
+.glyphicon-play-circle:before {
+ content: "\e029"; }
+
+.glyphicon-repeat:before {
+ content: "\e030"; }
+
+.glyphicon-refresh:before {
+ content: "\e031"; }
+
+.glyphicon-list-alt:before {
+ content: "\e032"; }
+
+.glyphicon-lock:before {
+ content: "\e033"; }
+
+.glyphicon-flag:before {
+ content: "\e034"; }
+
+.glyphicon-headphones:before {
+ content: "\e035"; }
+
+.glyphicon-volume-off:before {
+ content: "\e036"; }
+
+.glyphicon-volume-down:before {
+ content: "\e037"; }
+
+.glyphicon-volume-up:before {
+ content: "\e038"; }
+
+.glyphicon-qrcode:before {
+ content: "\e039"; }
+
+.glyphicon-barcode:before {
+ content: "\e040"; }
+
+.glyphicon-tag:before {
+ content: "\e041"; }
+
+.glyphicon-tags:before {
+ content: "\e042"; }
+
+.glyphicon-book:before {
+ content: "\e043"; }
+
+.glyphicon-bookmark:before {
+ content: "\e044"; }
+
+.glyphicon-print:before {
+ content: "\e045"; }
+
+.glyphicon-camera:before {
+ content: "\e046"; }
+
+.glyphicon-font:before {
+ content: "\e047"; }
+
+.glyphicon-bold:before {
+ content: "\e048"; }
+
+.glyphicon-italic:before {
+ content: "\e049"; }
+
+.glyphicon-text-height:before {
+ content: "\e050"; }
+
+.glyphicon-text-width:before {
+ content: "\e051"; }
+
+.glyphicon-align-left:before {
+ content: "\e052"; }
+
+.glyphicon-align-center:before {
+ content: "\e053"; }
+
+.glyphicon-align-right:before {
+ content: "\e054"; }
+
+.glyphicon-align-justify:before {
+ content: "\e055"; }
+
+.glyphicon-list:before {
+ content: "\e056"; }
+
+.glyphicon-indent-left:before {
+ content: "\e057"; }
+
+.glyphicon-indent-right:before {
+ content: "\e058"; }
+
+.glyphicon-facetime-video:before {
+ content: "\e059"; }
+
+.glyphicon-picture:before {
+ content: "\e060"; }
+
+.glyphicon-map-marker:before {
+ content: "\e062"; }
+
+.glyphicon-adjust:before {
+ content: "\e063"; }
+
+.glyphicon-tint:before {
+ content: "\e064"; }
+
+.glyphicon-edit:before {
+ content: "\e065"; }
+
+.glyphicon-share:before {
+ content: "\e066"; }
+
+.glyphicon-check:before {
+ content: "\e067"; }
+
+.glyphicon-move:before {
+ content: "\e068"; }
+
+.glyphicon-step-backward:before {
+ content: "\e069"; }
+
+.glyphicon-fast-backward:before {
+ content: "\e070"; }
+
+.glyphicon-backward:before {
+ content: "\e071"; }
+
+.glyphicon-play:before {
+ content: "\e072"; }
+
+.glyphicon-pause:before {
+ content: "\e073"; }
+
+.glyphicon-stop:before {
+ content: "\e074"; }
+
+.glyphicon-forward:before {
+ content: "\e075"; }
+
+.glyphicon-fast-forward:before {
+ content: "\e076"; }
+
+.glyphicon-step-forward:before {
+ content: "\e077"; }
+
+.glyphicon-eject:before {
+ content: "\e078"; }
+
+.glyphicon-chevron-left:before {
+ content: "\e079"; }
+
+.glyphicon-chevron-right:before {
+ content: "\e080"; }
+
+.glyphicon-plus-sign:before {
+ content: "\e081"; }
+
+.glyphicon-minus-sign:before {
+ content: "\e082"; }
+
+.glyphicon-remove-sign:before {
+ content: "\e083"; }
+
+.glyphicon-ok-sign:before {
+ content: "\e084"; }
+
+.glyphicon-question-sign:before {
+ content: "\e085"; }
+
+.glyphicon-info-sign:before {
+ content: "\e086"; }
+
+.glyphicon-screenshot:before {
+ content: "\e087"; }
+
+.glyphicon-remove-circle:before {
+ content: "\e088"; }
+
+.glyphicon-ok-circle:before {
+ content: "\e089"; }
+
+.glyphicon-ban-circle:before {
+ content: "\e090"; }
+
+.glyphicon-arrow-left:before {
+ content: "\e091"; }
+
+.glyphicon-arrow-right:before {
+ content: "\e092"; }
+
+.glyphicon-arrow-up:before {
+ content: "\e093"; }
+
+.glyphicon-arrow-down:before {
+ content: "\e094"; }
+
+.glyphicon-share-alt:before {
+ content: "\e095"; }
+
+.glyphicon-resize-full:before {
+ content: "\e096"; }
+
+.glyphicon-resize-small:before {
+ content: "\e097"; }
+
+.glyphicon-exclamation-sign:before {
+ content: "\e101"; }
+
+.glyphicon-gift:before {
+ content: "\e102"; }
+
+.glyphicon-leaf:before {
+ content: "\e103"; }
+
+.glyphicon-fire:before {
+ content: "\e104"; }
+
+.glyphicon-eye-open:before {
+ content: "\e105"; }
+
+.glyphicon-eye-close:before {
+ content: "\e106"; }
+
+.glyphicon-warning-sign:before {
+ content: "\e107"; }
+
+.glyphicon-plane:before {
+ content: "\e108"; }
+
+.glyphicon-calendar:before {
+ content: "\e109"; }
+
+.glyphicon-random:before {
+ content: "\e110"; }
+
+.glyphicon-comment:before {
+ content: "\e111"; }
+
+.glyphicon-magnet:before {
+ content: "\e112"; }
+
+.glyphicon-chevron-up:before {
+ content: "\e113"; }
+
+.glyphicon-chevron-down:before {
+ content: "\e114"; }
+
+.glyphicon-retweet:before {
+ content: "\e115"; }
+
+.glyphicon-shopping-cart:before {
+ content: "\e116"; }
+
+.glyphicon-folder-close:before {
+ content: "\e117"; }
+
+.glyphicon-folder-open:before {
+ content: "\e118"; }
+
+.glyphicon-resize-vertical:before {
+ content: "\e119"; }
+
+.glyphicon-resize-horizontal:before {
+ content: "\e120"; }
+
+.glyphicon-hdd:before {
+ content: "\e121"; }
+
+.glyphicon-bullhorn:before {
+ content: "\e122"; }
+
+.glyphicon-bell:before {
+ content: "\e123"; }
+
+.glyphicon-certificate:before {
+ content: "\e124"; }
+
+.glyphicon-thumbs-up:before {
+ content: "\e125"; }
+
+.glyphicon-thumbs-down:before {
+ content: "\e126"; }
+
+.glyphicon-hand-right:before {
+ content: "\e127"; }
+
+.glyphicon-hand-left:before {
+ content: "\e128"; }
+
+.glyphicon-hand-up:before {
+ content: "\e129"; }
+
+.glyphicon-hand-down:before {
+ content: "\e130"; }
+
+.glyphicon-circle-arrow-right:before {
+ content: "\e131"; }
+
+.glyphicon-circle-arrow-left:before {
+ content: "\e132"; }
+
+.glyphicon-circle-arrow-up:before {
+ content: "\e133"; }
+
+.glyphicon-circle-arrow-down:before {
+ content: "\e134"; }
+
+.glyphicon-globe:before {
+ content: "\e135"; }
+
+.glyphicon-wrench:before {
+ content: "\e136"; }
+
+.glyphicon-tasks:before {
+ content: "\e137"; }
+
+.glyphicon-filter:before {
+ content: "\e138"; }
+
+.glyphicon-briefcase:before {
+ content: "\e139"; }
+
+.glyphicon-fullscreen:before {
+ content: "\e140"; }
+
+.glyphicon-dashboard:before {
+ content: "\e141"; }
+
+.glyphicon-paperclip:before {
+ content: "\e142"; }
+
+.glyphicon-heart-empty:before {
+ content: "\e143"; }
+
+.glyphicon-link:before {
+ content: "\e144"; }
+
+.glyphicon-phone:before {
+ content: "\e145"; }
+
+.glyphicon-pushpin:before {
+ content: "\e146"; }
+
+.glyphicon-usd:before {
+ content: "\e148"; }
+
+.glyphicon-gbp:before {
+ content: "\e149"; }
+
+.glyphicon-sort:before {
+ content: "\e150"; }
+
+.glyphicon-sort-by-alphabet:before {
+ content: "\e151"; }
+
+.glyphicon-sort-by-alphabet-alt:before {
+ content: "\e152"; }
+
+.glyphicon-sort-by-order:before {
+ content: "\e153"; }
+
+.glyphicon-sort-by-order-alt:before {
+ content: "\e154"; }
+
+.glyphicon-sort-by-attributes:before {
+ content: "\e155"; }
+
+.glyphicon-sort-by-attributes-alt:before {
+ content: "\e156"; }
+
+.glyphicon-unchecked:before {
+ content: "\e157"; }
+
+.glyphicon-expand:before {
+ content: "\e158"; }
+
+.glyphicon-collapse-down:before {
+ content: "\e159"; }
+
+.glyphicon-collapse-up:before {
+ content: "\e160"; }
+
+.glyphicon-log-in:before {
+ content: "\e161"; }
+
+.glyphicon-flash:before {
+ content: "\e162"; }
+
+.glyphicon-log-out:before {
+ content: "\e163"; }
+
+.glyphicon-new-window:before {
+ content: "\e164"; }
+
+.glyphicon-record:before {
+ content: "\e165"; }
+
+.glyphicon-save:before {
+ content: "\e166"; }
+
+.glyphicon-open:before {
+ content: "\e167"; }
+
+.glyphicon-saved:before {
+ content: "\e168"; }
+
+.glyphicon-import:before {
+ content: "\e169"; }
+
+.glyphicon-export:before {
+ content: "\e170"; }
+
+.glyphicon-send:before {
+ content: "\e171"; }
+
+.glyphicon-floppy-disk:before {
+ content: "\e172"; }
+
+.glyphicon-floppy-saved:before {
+ content: "\e173"; }
+
+.glyphicon-floppy-remove:before {
+ content: "\e174"; }
+
+.glyphicon-floppy-save:before {
+ content: "\e175"; }
+
+.glyphicon-floppy-open:before {
+ content: "\e176"; }
+
+.glyphicon-credit-card:before {
+ content: "\e177"; }
+
+.glyphicon-transfer:before {
+ content: "\e178"; }
+
+.glyphicon-cutlery:before {
+ content: "\e179"; }
+
+.glyphicon-header:before {
+ content: "\e180"; }
+
+.glyphicon-compressed:before {
+ content: "\e181"; }
+
+.glyphicon-earphone:before {
+ content: "\e182"; }
+
+.glyphicon-phone-alt:before {
+ content: "\e183"; }
+
+.glyphicon-tower:before {
+ content: "\e184"; }
+
+.glyphicon-stats:before {
+ content: "\e185"; }
+
+.glyphicon-sd-video:before {
+ content: "\e186"; }
+
+.glyphicon-hd-video:before {
+ content: "\e187"; }
+
+.glyphicon-subtitles:before {
+ content: "\e188"; }
+
+.glyphicon-sound-stereo:before {
+ content: "\e189"; }
+
+.glyphicon-sound-dolby:before {
+ content: "\e190"; }
+
+.glyphicon-sound-5-1:before {
+ content: "\e191"; }
+
+.glyphicon-sound-6-1:before {
+ content: "\e192"; }
+
+.glyphicon-sound-7-1:before {
+ content: "\e193"; }
+
+.glyphicon-copyright-mark:before {
+ content: "\e194"; }
+
+.glyphicon-registration-mark:before {
+ content: "\e195"; }
+
+.glyphicon-cloud-download:before {
+ content: "\e197"; }
+
+.glyphicon-cloud-upload:before {
+ content: "\e198"; }
+
+.glyphicon-tree-conifer:before {
+ content: "\e199"; }
+
+.glyphicon-tree-deciduous:before {
+ content: "\e200"; }
+
+* {
+ box-sizing: border-box; }
+
+*:before, *:after {
+ box-sizing: border-box; }
+
+html {
+ font-size: 62.5%;
+ -webkit-tap-highlight-color: rgba(0, 0, 0, 0); }
+
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #333333;
+ background-color: #fff; }
+
+input, button, select, textarea {
+ font-family: inherit;
+ font-size: inherit;
+ line-height: inherit; }
+
+a {
+ color: #428bca;
+ text-decoration: none; }
+ a:hover, a:focus {
+ color: #2a6596;
+ text-decoration: underline; }
+ a:focus {
+ outline: thin dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px; }
+
+figure {
+ margin: 0; }
+
+img {
+ vertical-align: middle; }
+
+.img-responsive {
+ display: block;
+ max-width: 100%;
+ height: auto; }
+
+.img-rounded {
+ border-radius: 6px; }
+
+.img-thumbnail {
+ padding: 4px;
+ line-height: 1.42857;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: all 0.2s ease-in-out;
+ display: inline-block;
+ max-width: 100%;
+ height: auto; }
+
+.img-circle {
+ border-radius: 50%; }
+
+hr {
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 0;
+ border-top: 1px solid #eeeeee; }
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ margin: -1px;
+ padding: 0;
+ overflow: hidden;
+ clip: rect(0, 0, 0, 0);
+ border: 0; }
+
+.sr-only-focusable:active, .sr-only-focusable:focus {
+ position: static;
+ width: auto;
+ height: auto;
+ margin: 0;
+ overflow: visible;
+ clip: auto; }
+
+h1, h2, h3, h4, h5, h6, .h1, .h2, .h3, .h4, .h5, .h6 {
+ font-family: inherit;
+ font-weight: 500;
+ line-height: 1.1;
+ color: inherit; }
+ h1 small, h1 .small, h2 small, h2 .small, h3 small, h3 .small, h4 small, h4 .small, h5 small, h5 .small, h6 small, h6 .small, .h1 small, .h1 .small, .h2 small, .h2 .small, .h3 small, .h3 .small, .h4 small, .h4 .small, .h5 small, .h5 .small, .h6 small, .h6 .small {
+ font-weight: normal;
+ line-height: 1;
+ color: #999999; }
+
+h1, .h1, h2, .h2, h3, .h3 {
+ margin-top: 20px;
+ margin-bottom: 10px; }
+ h1 small, h1 .small, .h1 small, .h1 .small, h2 small, h2 .small, .h2 small, .h2 .small, h3 small, h3 .small, .h3 small, .h3 .small {
+ font-size: 65%; }
+
+h4, .h4, h5, .h5, h6, .h6 {
+ margin-top: 10px;
+ margin-bottom: 10px; }
+ h4 small, h4 .small, .h4 small, .h4 .small, h5 small, h5 .small, .h5 small, .h5 .small, h6 small, h6 .small, .h6 small, .h6 .small {
+ font-size: 75%; }
+
+h1, .h1 {
+ font-size: 36px; }
+
+h2, .h2 {
+ font-size: 30px; }
+
+h3, .h3 {
+ font-size: 24px; }
+
+h4, .h4 {
+ font-size: 18px; }
+
+h5, .h5 {
+ font-size: 14px; }
+
+h6, .h6 {
+ font-size: 12px; }
+
+p {
+ margin: 0 0 10px; }
+
+.lead {
+ margin-bottom: 20px;
+ font-size: 16px;
+ font-weight: 200;
+ line-height: 1.4; }
+ @media (min-width: 768px) {
+ .lead {
+ font-size: 21px; } }
+
+small, .small {
+ font-size: 85%; }
+
+cite {
+ font-style: normal; }
+
+mark, .mark {
+ background-color: #fcf8e3;
+ padding: 0.2em; }
+
+.text-left {
+ text-align: left; }
+
+.text-right {
+ text-align: right; }
+
+.text-center {
+ text-align: center; }
+
+.text-justify {
+ text-align: justify; }
+
+.text-muted {
+ color: #999999; }
+
+.text-primary {
+ color: #428bca; }
+
+a.text-primary:hover {
+ color: #3073a9; }
+
+.text-success {
+ color: #3c763d; }
+
+a.text-success:hover {
+ color: #2b542b; }
+
+.text-info {
+ color: #31708f; }
+
+a.text-info:hover {
+ color: #245369; }
+
+.text-warning {
+ color: #8a6d3b; }
+
+a.text-warning:hover {
+ color: #66502c; }
+
+.text-danger {
+ color: #a94442; }
+
+a.text-danger:hover {
+ color: #843534; }
+
+.bg-primary {
+ color: #fff; }
+
+.bg-primary {
+ background-color: #428bca; }
+
+a.bg-primary:hover {
+ background-color: #3073a9; }
+
+.bg-success {
+ background-color: #dff0d8; }
+
+a.bg-success:hover {
+ background-color: #c1e2b3; }
+
+.bg-info {
+ background-color: #d9edf7; }
+
+a.bg-info:hover {
+ background-color: #afdaee; }
+
+.bg-warning {
+ background-color: #fcf8e3; }
+
+a.bg-warning:hover {
+ background-color: #f7ecb5; }
+
+.bg-danger {
+ background-color: #f2dede; }
+
+a.bg-danger:hover {
+ background-color: #e4b9b9; }
+
+.page-header {
+ padding-bottom: 9px;
+ margin: 40px 0 20px;
+ border-bottom: 1px solid #eeeeee; }
+
+ul, ol {
+ margin-top: 0;
+ margin-bottom: 10px; }
+ ul ul, ul ol, ol ul, ol ol {
+ margin-bottom: 0; }
+
+.list-unstyled, .list-inline {
+ padding-left: 0;
+ list-style: none; }
+
+.list-inline {
+ margin-left: -5px; }
+ .list-inline > li {
+ display: inline-block;
+ padding-left: 5px;
+ padding-right: 5px; }
+
+dl {
+ margin-top: 0;
+ margin-bottom: 20px; }
+
+dt, dd {
+ line-height: 1.42857; }
+
+dt {
+ font-weight: bold; }
+
+dd {
+ margin-left: 0; }
+
+.dl-horizontal dd:before, .dl-horizontal dd:after {
+ content: " ";
+ display: table; }
+.dl-horizontal dd:after {
+ clear: both; }
+@media (min-width: 768px) {
+ .dl-horizontal dt {
+ float: left;
+ width: 160px;
+ clear: left;
+ text-align: right;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap; }
+ .dl-horizontal dd {
+ margin-left: 180px; } }
+
+abbr[title], abbr[data-original-title] {
+ cursor: help;
+ border-bottom: 1px dotted #999999; }
+
+.initialism {
+ font-size: 90%;
+ text-transform: uppercase; }
+
+blockquote {
+ padding: 10px 20px;
+ margin: 0 0 20px;
+ font-size: 17.5px;
+ border-left: 5px solid #eeeeee; }
+ blockquote p:last-child, blockquote ul:last-child, blockquote ol:last-child {
+ margin-bottom: 0; }
+ blockquote footer, blockquote small, blockquote .small {
+ display: block;
+ font-size: 80%;
+ line-height: 1.42857;
+ color: #999999; }
+ blockquote footer:before, blockquote small:before, blockquote .small:before {
+ content: '\2014 \00A0'; }
+
+.blockquote-reverse, blockquote.pull-right {
+ padding-right: 15px;
+ padding-left: 0;
+ border-right: 5px solid #eeeeee;
+ border-left: 0;
+ text-align: right; }
+ .blockquote-reverse footer:before, .blockquote-reverse small:before, .blockquote-reverse .small:before, blockquote.pull-right footer:before, blockquote.pull-right small:before, blockquote.pull-right .small:before {
+ content: ''; }
+ .blockquote-reverse footer:after, .blockquote-reverse small:after, .blockquote-reverse .small:after, blockquote.pull-right footer:after, blockquote.pull-right small:after, blockquote.pull-right .small:after {
+ content: '\00A0 \2014'; }
+
+blockquote:before, blockquote:after {
+ content: ""; }
+
+address {
+ margin-bottom: 20px;
+ font-style: normal;
+ line-height: 1.42857; }
+
+code, kbd, pre, samp {
+ font-family: Menlo, Monaco, Consolas, "Courier New", monospace; }
+
+code {
+ padding: 2px 4px;
+ font-size: 90%;
+ color: #c7254e;
+ background-color: #f9f2f4;
+ border-radius: 4px; }
+
+kbd {
+ padding: 2px 4px;
+ font-size: 90%;
+ color: #fff;
+ background-color: #333;
+ border-radius: 3px;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25); }
+
+pre {
+ display: block;
+ padding: 9.5px;
+ margin: 0 0 10px;
+ font-size: 13px;
+ line-height: 1.42857;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: #333333;
+ background-color: #f5f5f5;
+ border: 1px solid #ccc;
+ border-radius: 4px; }
+ pre code {
+ padding: 0;
+ font-size: inherit;
+ color: inherit;
+ white-space: pre-wrap;
+ background-color: transparent;
+ border-radius: 0; }
+
+.pre-scrollable {
+ max-height: 340px;
+ overflow-y: scroll; }
+
+.container {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px; }
+ .container:before, .container:after {
+ content: " ";
+ display: table; }
+ .container:after {
+ clear: both; }
+ @media (min-width: 768px) {
+ .container {
+ width: 750px; } }
+ @media (min-width: 992px) {
+ .container {
+ width: 970px; } }
+ @media (min-width: 1200px) {
+ .container {
+ width: 1170px; } }
+
+.container-fluid {
+ margin-right: auto;
+ margin-left: auto;
+ padding-left: 15px;
+ padding-right: 15px; }
+ .container-fluid:before, .container-fluid:after {
+ content: " ";
+ display: table; }
+ .container-fluid:after {
+ clear: both; }
+
+.row {
+ margin-left: -15px;
+ margin-right: -15px; }
+ .row:before, .row:after {
+ content: " ";
+ display: table; }
+ .row:after {
+ clear: both; }
+
+.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {
+ position: relative;
+ min-height: 1px;
+ padding-left: 15px;
+ padding-right: 15px; }
+
+.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {
+ float: left; }
+
+.col-xs-1 {
+ width: 8.33333%; }
+
+.col-xs-2 {
+ width: 16.66667%; }
+
+.col-xs-3 {
+ width: 25%; }
+
+.col-xs-4 {
+ width: 33.33333%; }
+
+.col-xs-5 {
+ width: 41.66667%; }
+
+.col-xs-6 {
+ width: 50%; }
+
+.col-xs-7 {
+ width: 58.33333%; }
+
+.col-xs-8 {
+ width: 66.66667%; }
+
+.col-xs-9 {
+ width: 75%; }
+
+.col-xs-10 {
+ width: 83.33333%; }
+
+.col-xs-11 {
+ width: 91.66667%; }
+
+.col-xs-12 {
+ width: 100%; }
+
+.col-xs-pull-0 {
+ right: auto; }
+
+.col-xs-pull-1 {
+ right: 8.33333%; }
+
+.col-xs-pull-2 {
+ right: 16.66667%; }
+
+.col-xs-pull-3 {
+ right: 25%; }
+
+.col-xs-pull-4 {
+ right: 33.33333%; }
+
+.col-xs-pull-5 {
+ right: 41.66667%; }
+
+.col-xs-pull-6 {
+ right: 50%; }
+
+.col-xs-pull-7 {
+ right: 58.33333%; }
+
+.col-xs-pull-8 {
+ right: 66.66667%; }
+
+.col-xs-pull-9 {
+ right: 75%; }
+
+.col-xs-pull-10 {
+ right: 83.33333%; }
+
+.col-xs-pull-11 {
+ right: 91.66667%; }
+
+.col-xs-pull-12 {
+ right: 100%; }
+
+.col-xs-push-0 {
+ left: auto; }
+
+.col-xs-push-1 {
+ left: 8.33333%; }
+
+.col-xs-push-2 {
+ left: 16.66667%; }
+
+.col-xs-push-3 {
+ left: 25%; }
+
+.col-xs-push-4 {
+ left: 33.33333%; }
+
+.col-xs-push-5 {
+ left: 41.66667%; }
+
+.col-xs-push-6 {
+ left: 50%; }
+
+.col-xs-push-7 {
+ left: 58.33333%; }
+
+.col-xs-push-8 {
+ left: 66.66667%; }
+
+.col-xs-push-9 {
+ left: 75%; }
+
+.col-xs-push-10 {
+ left: 83.33333%; }
+
+.col-xs-push-11 {
+ left: 91.66667%; }
+
+.col-xs-push-12 {
+ left: 100%; }
+
+.col-xs-offset-0 {
+ margin-left: 0%; }
+
+.col-xs-offset-1 {
+ margin-left: 8.33333%; }
+
+.col-xs-offset-2 {
+ margin-left: 16.66667%; }
+
+.col-xs-offset-3 {
+ margin-left: 25%; }
+
+.col-xs-offset-4 {
+ margin-left: 33.33333%; }
+
+.col-xs-offset-5 {
+ margin-left: 41.66667%; }
+
+.col-xs-offset-6 {
+ margin-left: 50%; }
+
+.col-xs-offset-7 {
+ margin-left: 58.33333%; }
+
+.col-xs-offset-8 {
+ margin-left: 66.66667%; }
+
+.col-xs-offset-9 {
+ margin-left: 75%; }
+
+.col-xs-offset-10 {
+ margin-left: 83.33333%; }
+
+.col-xs-offset-11 {
+ margin-left: 91.66667%; }
+
+.col-xs-offset-12 {
+ margin-left: 100%; }
+
+@media (min-width: 768px) {
+ .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
+ float: left; }
+ .col-sm-1 {
+ width: 8.33333%; }
+ .col-sm-2 {
+ width: 16.66667%; }
+ .col-sm-3 {
+ width: 25%; }
+ .col-sm-4 {
+ width: 33.33333%; }
+ .col-sm-5 {
+ width: 41.66667%; }
+ .col-sm-6 {
+ width: 50%; }
+ .col-sm-7 {
+ width: 58.33333%; }
+ .col-sm-8 {
+ width: 66.66667%; }
+ .col-sm-9 {
+ width: 75%; }
+ .col-sm-10 {
+ width: 83.33333%; }
+ .col-sm-11 {
+ width: 91.66667%; }
+ .col-sm-12 {
+ width: 100%; }
+ .col-sm-pull-0 {
+ right: auto; }
+ .col-sm-pull-1 {
+ right: 8.33333%; }
+ .col-sm-pull-2 {
+ right: 16.66667%; }
+ .col-sm-pull-3 {
+ right: 25%; }
+ .col-sm-pull-4 {
+ right: 33.33333%; }
+ .col-sm-pull-5 {
+ right: 41.66667%; }
+ .col-sm-pull-6 {
+ right: 50%; }
+ .col-sm-pull-7 {
+ right: 58.33333%; }
+ .col-sm-pull-8 {
+ right: 66.66667%; }
+ .col-sm-pull-9 {
+ right: 75%; }
+ .col-sm-pull-10 {
+ right: 83.33333%; }
+ .col-sm-pull-11 {
+ right: 91.66667%; }
+ .col-sm-pull-12 {
+ right: 100%; }
+ .col-sm-push-0 {
+ left: auto; }
+ .col-sm-push-1 {
+ left: 8.33333%; }
+ .col-sm-push-2 {
+ left: 16.66667%; }
+ .col-sm-push-3 {
+ left: 25%; }
+ .col-sm-push-4 {
+ left: 33.33333%; }
+ .col-sm-push-5 {
+ left: 41.66667%; }
+ .col-sm-push-6 {
+ left: 50%; }
+ .col-sm-push-7 {
+ left: 58.33333%; }
+ .col-sm-push-8 {
+ left: 66.66667%; }
+ .col-sm-push-9 {
+ left: 75%; }
+ .col-sm-push-10 {
+ left: 83.33333%; }
+ .col-sm-push-11 {
+ left: 91.66667%; }
+ .col-sm-push-12 {
+ left: 100%; }
+ .col-sm-offset-0 {
+ margin-left: 0%; }
+ .col-sm-offset-1 {
+ margin-left: 8.33333%; }
+ .col-sm-offset-2 {
+ margin-left: 16.66667%; }
+ .col-sm-offset-3 {
+ margin-left: 25%; }
+ .col-sm-offset-4 {
+ margin-left: 33.33333%; }
+ .col-sm-offset-5 {
+ margin-left: 41.66667%; }
+ .col-sm-offset-6 {
+ margin-left: 50%; }
+ .col-sm-offset-7 {
+ margin-left: 58.33333%; }
+ .col-sm-offset-8 {
+ margin-left: 66.66667%; }
+ .col-sm-offset-9 {
+ margin-left: 75%; }
+ .col-sm-offset-10 {
+ margin-left: 83.33333%; }
+ .col-sm-offset-11 {
+ margin-left: 91.66667%; }
+ .col-sm-offset-12 {
+ margin-left: 100%; } }
+
+@media (min-width: 992px) {
+ .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {
+ float: left; }
+ .col-md-1 {
+ width: 8.33333%; }
+ .col-md-2 {
+ width: 16.66667%; }
+ .col-md-3 {
+ width: 25%; }
+ .col-md-4 {
+ width: 33.33333%; }
+ .col-md-5 {
+ width: 41.66667%; }
+ .col-md-6 {
+ width: 50%; }
+ .col-md-7 {
+ width: 58.33333%; }
+ .col-md-8 {
+ width: 66.66667%; }
+ .col-md-9 {
+ width: 75%; }
+ .col-md-10 {
+ width: 83.33333%; }
+ .col-md-11 {
+ width: 91.66667%; }
+ .col-md-12 {
+ width: 100%; }
+ .col-md-pull-0 {
+ right: auto; }
+ .col-md-pull-1 {
+ right: 8.33333%; }
+ .col-md-pull-2 {
+ right: 16.66667%; }
+ .col-md-pull-3 {
+ right: 25%; }
+ .col-md-pull-4 {
+ right: 33.33333%; }
+ .col-md-pull-5 {
+ right: 41.66667%; }
+ .col-md-pull-6 {
+ right: 50%; }
+ .col-md-pull-7 {
+ right: 58.33333%; }
+ .col-md-pull-8 {
+ right: 66.66667%; }
+ .col-md-pull-9 {
+ right: 75%; }
+ .col-md-pull-10 {
+ right: 83.33333%; }
+ .col-md-pull-11 {
+ right: 91.66667%; }
+ .col-md-pull-12 {
+ right: 100%; }
+ .col-md-push-0 {
+ left: auto; }
+ .col-md-push-1 {
+ left: 8.33333%; }
+ .col-md-push-2 {
+ left: 16.66667%; }
+ .col-md-push-3 {
+ left: 25%; }
+ .col-md-push-4 {
+ left: 33.33333%; }
+ .col-md-push-5 {
+ left: 41.66667%; }
+ .col-md-push-6 {
+ left: 50%; }
+ .col-md-push-7 {
+ left: 58.33333%; }
+ .col-md-push-8 {
+ left: 66.66667%; }
+ .col-md-push-9 {
+ left: 75%; }
+ .col-md-push-10 {
+ left: 83.33333%; }
+ .col-md-push-11 {
+ left: 91.66667%; }
+ .col-md-push-12 {
+ left: 100%; }
+ .col-md-offset-0 {
+ margin-left: 0%; }
+ .col-md-offset-1 {
+ margin-left: 8.33333%; }
+ .col-md-offset-2 {
+ margin-left: 16.66667%; }
+ .col-md-offset-3 {
+ margin-left: 25%; }
+ .col-md-offset-4 {
+ margin-left: 33.33333%; }
+ .col-md-offset-5 {
+ margin-left: 41.66667%; }
+ .col-md-offset-6 {
+ margin-left: 50%; }
+ .col-md-offset-7 {
+ margin-left: 58.33333%; }
+ .col-md-offset-8 {
+ margin-left: 66.66667%; }
+ .col-md-offset-9 {
+ margin-left: 75%; }
+ .col-md-offset-10 {
+ margin-left: 83.33333%; }
+ .col-md-offset-11 {
+ margin-left: 91.66667%; }
+ .col-md-offset-12 {
+ margin-left: 100%; } }
+
+@media (min-width: 1200px) {
+ .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {
+ float: left; }
+ .col-lg-1 {
+ width: 8.33333%; }
+ .col-lg-2 {
+ width: 16.66667%; }
+ .col-lg-3 {
+ width: 25%; }
+ .col-lg-4 {
+ width: 33.33333%; }
+ .col-lg-5 {
+ width: 41.66667%; }
+ .col-lg-6 {
+ width: 50%; }
+ .col-lg-7 {
+ width: 58.33333%; }
+ .col-lg-8 {
+ width: 66.66667%; }
+ .col-lg-9 {
+ width: 75%; }
+ .col-lg-10 {
+ width: 83.33333%; }
+ .col-lg-11 {
+ width: 91.66667%; }
+ .col-lg-12 {
+ width: 100%; }
+ .col-lg-pull-0 {
+ right: auto; }
+ .col-lg-pull-1 {
+ right: 8.33333%; }
+ .col-lg-pull-2 {
+ right: 16.66667%; }
+ .col-lg-pull-3 {
+ right: 25%; }
+ .col-lg-pull-4 {
+ right: 33.33333%; }
+ .col-lg-pull-5 {
+ right: 41.66667%; }
+ .col-lg-pull-6 {
+ right: 50%; }
+ .col-lg-pull-7 {
+ right: 58.33333%; }
+ .col-lg-pull-8 {
+ right: 66.66667%; }
+ .col-lg-pull-9 {
+ right: 75%; }
+ .col-lg-pull-10 {
+ right: 83.33333%; }
+ .col-lg-pull-11 {
+ right: 91.66667%; }
+ .col-lg-pull-12 {
+ right: 100%; }
+ .col-lg-push-0 {
+ left: auto; }
+ .col-lg-push-1 {
+ left: 8.33333%; }
+ .col-lg-push-2 {
+ left: 16.66667%; }
+ .col-lg-push-3 {
+ left: 25%; }
+ .col-lg-push-4 {
+ left: 33.33333%; }
+ .col-lg-push-5 {
+ left: 41.66667%; }
+ .col-lg-push-6 {
+ left: 50%; }
+ .col-lg-push-7 {
+ left: 58.33333%; }
+ .col-lg-push-8 {
+ left: 66.66667%; }
+ .col-lg-push-9 {
+ left: 75%; }
+ .col-lg-push-10 {
+ left: 83.33333%; }
+ .col-lg-push-11 {
+ left: 91.66667%; }
+ .col-lg-push-12 {
+ left: 100%; }
+ .col-lg-offset-0 {
+ margin-left: 0%; }
+ .col-lg-offset-1 {
+ margin-left: 8.33333%; }
+ .col-lg-offset-2 {
+ margin-left: 16.66667%; }
+ .col-lg-offset-3 {
+ margin-left: 25%; }
+ .col-lg-offset-4 {
+ margin-left: 33.33333%; }
+ .col-lg-offset-5 {
+ margin-left: 41.66667%; }
+ .col-lg-offset-6 {
+ margin-left: 50%; }
+ .col-lg-offset-7 {
+ margin-left: 58.33333%; }
+ .col-lg-offset-8 {
+ margin-left: 66.66667%; }
+ .col-lg-offset-9 {
+ margin-left: 75%; }
+ .col-lg-offset-10 {
+ margin-left: 83.33333%; }
+ .col-lg-offset-11 {
+ margin-left: 91.66667%; }
+ .col-lg-offset-12 {
+ margin-left: 100%; } }
+
+table {
+ max-width: 100%;
+ background-color: transparent; }
+
+th {
+ text-align: left; }
+
+.table {
+ width: 100%;
+ margin-bottom: 20px; }
+ .table > thead > tr > th, .table > thead > tr > td, .table > tbody > tr > th, .table > tbody > tr > td, .table > tfoot > tr > th, .table > tfoot > tr > td {
+ padding: 8px;
+ line-height: 1.42857;
+ vertical-align: top;
+ border-top: 1px solid #ddd; }
+ .table > thead > tr > th {
+ vertical-align: bottom;
+ border-bottom: 2px solid #ddd; }
+ .table > caption + thead > tr:first-child > th, .table > caption + thead > tr:first-child > td, .table > colgroup + thead > tr:first-child > th, .table > colgroup + thead > tr:first-child > td, .table > thead:first-child > tr:first-child > th, .table > thead:first-child > tr:first-child > td {
+ border-top: 0; }
+ .table > tbody + tbody {
+ border-top: 2px solid #ddd; }
+ .table .table {
+ background-color: #fff; }
+
+.table-condensed > thead > tr > th, .table-condensed > thead > tr > td, .table-condensed > tbody > tr > th, .table-condensed > tbody > tr > td, .table-condensed > tfoot > tr > th, .table-condensed > tfoot > tr > td {
+ padding: 5px; }
+
+.table-bordered {
+ border: 1px solid #ddd; }
+ .table-bordered > thead > tr > th, .table-bordered > thead > tr > td, .table-bordered > tbody > tr > th, .table-bordered > tbody > tr > td, .table-bordered > tfoot > tr > th, .table-bordered > tfoot > tr > td {
+ border: 1px solid #ddd; }
+ .table-bordered > thead > tr > th, .table-bordered > thead > tr > td {
+ border-bottom-width: 2px; }
+
+.table-striped > tbody > tr:nth-child(odd) > td, .table-striped > tbody > tr:nth-child(odd) > th {
+ background-color: #f9f9f9; }
+
+.table-hover > tbody > tr:hover > td, .table-hover > tbody > tr:hover > th {
+ background-color: #f5f5f5; }
+
+table col[class*="col-"] {
+ position: static;
+ float: none;
+ display: table-column; }
+
+table td[class*="col-"], table th[class*="col-"] {
+ position: static;
+ float: none;
+ display: table-cell; }
+
+.table > thead > tr > td.active, .table > thead > tr > th.active, .table > thead > tr.active > td, .table > thead > tr.active > th, .table > tbody > tr > td.active, .table > tbody > tr > th.active, .table > tbody > tr.active > td, .table > tbody > tr.active > th, .table > tfoot > tr > td.active, .table > tfoot > tr > th.active, .table > tfoot > tr.active > td, .table > tfoot > tr.active > th {
+ background-color: #f5f5f5; }
+
+.table-hover > tbody > tr > td.active:hover, .table-hover > tbody > tr > th.active:hover, .table-hover > tbody > tr.active:hover > td, .table-hover > tbody > tr:hover > .active, .table-hover > tbody > tr.active:hover > th {
+ background-color: #e8e8e8; }
+
+.table > thead > tr > td.success, .table > thead > tr > th.success, .table > thead > tr.success > td, .table > thead > tr.success > th, .table > tbody > tr > td.success, .table > tbody > tr > th.success, .table > tbody > tr.success > td, .table > tbody > tr.success > th, .table > tfoot > tr > td.success, .table > tfoot > tr > th.success, .table > tfoot > tr.success > td, .table > tfoot > tr.success > th {
+ background-color: #dff0d8; }
+
+.table-hover > tbody > tr > td.success:hover, .table-hover > tbody > tr > th.success:hover, .table-hover > tbody > tr.success:hover > td, .table-hover > tbody > tr:hover > .success, .table-hover > tbody > tr.success:hover > th {
+ background-color: #d0e9c6; }
+
+.table > thead > tr > td.info, .table > thead > tr > th.info, .table > thead > tr.info > td, .table > thead > tr.info > th, .table > tbody > tr > td.info, .table > tbody > tr > th.info, .table > tbody > tr.info > td, .table > tbody > tr.info > th, .table > tfoot > tr > td.info, .table > tfoot > tr > th.info, .table > tfoot > tr.info > td, .table > tfoot > tr.info > th {
+ background-color: #d9edf7; }
+
+.table-hover > tbody > tr > td.info:hover, .table-hover > tbody > tr > th.info:hover, .table-hover > tbody > tr.info:hover > td, .table-hover > tbody > tr:hover > .info, .table-hover > tbody > tr.info:hover > th {
+ background-color: #c4e4f3; }
+
+.table > thead > tr > td.warning, .table > thead > tr > th.warning, .table > thead > tr.warning > td, .table > thead > tr.warning > th, .table > tbody > tr > td.warning, .table > tbody > tr > th.warning, .table > tbody > tr.warning > td, .table > tbody > tr.warning > th, .table > tfoot > tr > td.warning, .table > tfoot > tr > th.warning, .table > tfoot > tr.warning > td, .table > tfoot > tr.warning > th {
+ background-color: #fcf8e3; }
+
+.table-hover > tbody > tr > td.warning:hover, .table-hover > tbody > tr > th.warning:hover, .table-hover > tbody > tr.warning:hover > td, .table-hover > tbody > tr:hover > .warning, .table-hover > tbody > tr.warning:hover > th {
+ background-color: #faf2cc; }
+
+.table > thead > tr > td.danger, .table > thead > tr > th.danger, .table > thead > tr.danger > td, .table > thead > tr.danger > th, .table > tbody > tr > td.danger, .table > tbody > tr > th.danger, .table > tbody > tr.danger > td, .table > tbody > tr.danger > th, .table > tfoot > tr > td.danger, .table > tfoot > tr > th.danger, .table > tfoot > tr.danger > td, .table > tfoot > tr.danger > th {
+ background-color: #f2dede; }
+
+.table-hover > tbody > tr > td.danger:hover, .table-hover > tbody > tr > th.danger:hover, .table-hover > tbody > tr.danger:hover > td, .table-hover > tbody > tr:hover > .danger, .table-hover > tbody > tr.danger:hover > th {
+ background-color: #ebcccc; }
+
+@media screen and (max-width: 767px) {
+ .table-responsive {
+ width: 100%;
+ margin-bottom: 15px;
+ overflow-y: hidden;
+ overflow-x: scroll;
+ -ms-overflow-style: -ms-autohiding-scrollbar;
+ border: 1px solid #ddd;
+ -webkit-overflow-scrolling: touch; }
+ .table-responsive > .table {
+ margin-bottom: 0; }
+ .table-responsive > .table > thead > tr > th, .table-responsive > .table > thead > tr > td, .table-responsive > .table > tbody > tr > th, .table-responsive > .table > tbody > tr > td, .table-responsive > .table > tfoot > tr > th, .table-responsive > .table > tfoot > tr > td {
+ white-space: nowrap; }
+ .table-responsive > .table-bordered {
+ border: 0; }
+ .table-responsive > .table-bordered > thead > tr > th:first-child, .table-responsive > .table-bordered > thead > tr > td:first-child, .table-responsive > .table-bordered > tbody > tr > th:first-child, .table-responsive > .table-bordered > tbody > tr > td:first-child, .table-responsive > .table-bordered > tfoot > tr > th:first-child, .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+ border-left: 0; }
+ .table-responsive > .table-bordered > thead > tr > th:last-child, .table-responsive > .table-bordered > thead > tr > td:last-child, .table-responsive > .table-bordered > tbody > tr > th:last-child, .table-responsive > .table-bordered > tbody > tr > td:last-child, .table-responsive > .table-bordered > tfoot > tr > th:last-child, .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+ border-right: 0; }
+ .table-responsive > .table-bordered > tbody > tr:last-child > th, .table-responsive > .table-bordered > tbody > tr:last-child > td, .table-responsive > .table-bordered > tfoot > tr:last-child > th, .table-responsive > .table-bordered > tfoot > tr:last-child > td {
+ border-bottom: 0; } }
+
+fieldset {
+ padding: 0;
+ margin: 0;
+ border: 0;
+ min-width: 0; }
+
+legend {
+ display: block;
+ width: 100%;
+ padding: 0;
+ margin-bottom: 20px;
+ font-size: 21px;
+ line-height: inherit;
+ color: #333333;
+ border: 0;
+ border-bottom: 1px solid #e5e5e5; }
+
+label {
+ display: inline-block;
+ max-width: 100%;
+ margin-bottom: 5px;
+ font-weight: bold; }
+
+input[type="search"] {
+ box-sizing: border-box; }
+
+input[type="radio"], input[type="checkbox"] {
+ margin: 4px 0 0;
+ margin-top: 1px \9;
+ line-height: normal; }
+
+input[type="file"] {
+ display: block; }
+
+input[type="range"] {
+ display: block;
+ width: 100%; }
+
+select[multiple], select[size] {
+ height: auto; }
+
+input[type="file"]:focus, input[type="radio"]:focus, input[type="checkbox"]:focus {
+ outline: thin dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px; }
+
+output {
+ display: block;
+ padding-top: 7px;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #555555; }
+
+.form-control {
+ display: block;
+ width: 100%;
+ height: 34px;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857;
+ color: #555555;
+ background-color: #fff;
+ background-image: none;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);
+ transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s; }
+ .form-control:focus {
+ border-color: #66afe9;
+ outline: 0;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); }
+ .form-control::placeholder {
+ color: #999999;
+ opacity: 1; }
+ .form-control:-ms-input-placeholder {
+ color: #999999; }
+ .form-control::-webkit-input-placeholder {
+ color: #999999; }
+ .form-control[disabled], .form-control[readonly], fieldset[disabled] .form-control {
+ cursor: not-allowed;
+ background-color: #eeeeee;
+ opacity: 1; }
+
+textarea.form-control {
+ height: auto; }
+
+input[type="search"] {
+ -webkit-appearance: none; }
+
+input[type="date"], input[type="time"], input[type="datetime-local"], input[type="month"] {
+ line-height: 34px;
+ line-height: 1.42857 \0; }
+ input[type="date"].input-sm, .input-group-sm > input[type="date"].form-control, .input-group-sm > input[type="date"].input-group-addon, .input-group-sm > .input-group-btn > input[type="date"].btn, input[type="time"].input-sm, .input-group-sm > input[type="time"].form-control, .input-group-sm > input[type="time"].input-group-addon, .input-group-sm > .input-group-btn > input[type="time"].btn, input[type="datetime-local"].input-sm, .input-group-sm > input[type="datetime-local"].form-control, .input-group-sm > input[type="datetime-local"].input-group-addon, .input-group-sm > .input-group-btn > input[type="datetime-local"].btn, input[type="month"].input-sm, .input-group-sm > input[type="month"].form-control, .input-group-sm > input[type="month"].input-group-addon, .input-group-sm > .input-group-btn > input[type="month"].btn {
+ line-height: 30px; }
+ input[type="date"].input-lg, .input-group-lg > input[type="date"].form-control, .input-group-lg > input[type="date"].input-group-addon, .input-group-lg > .input-group-btn > input[type="date"].btn, input[type="time"].input-lg, .input-group-lg > input[type="time"].form-control, .input-group-lg > input[type="time"].input-group-addon, .input-group-lg > .input-group-btn > input[type="time"].btn, input[type="datetime-local"].input-lg, .input-group-lg > input[type="datetime-local"].form-control, .input-group-lg > input[type="datetime-local"].input-group-addon, .input-group-lg > .input-group-btn > input[type="datetime-local"].btn, input[type="month"].input-lg, .input-group-lg > input[type="month"].form-control, .input-group-lg > input[type="month"].input-group-addon, .input-group-lg > .input-group-btn > input[type="month"].btn {
+ line-height: 46px; }
+
+.form-group {
+ margin-bottom: 15px; }
+
+.radio, .checkbox {
+ display: block;
+ min-height: 20px;
+ margin-top: 10px;
+ margin-bottom: 10px; }
+ .radio label, .checkbox label {
+ padding-left: 20px;
+ margin-bottom: 0;
+ font-weight: normal;
+ cursor: pointer; }
+
+.radio input[type="radio"], .radio-inline input[type="radio"], .checkbox input[type="checkbox"], .checkbox-inline input[type="checkbox"] {
+ float: left;
+ margin-left: -20px; }
+
+.radio + .radio, .checkbox + .checkbox {
+ margin-top: -5px; }
+
+.radio-inline, .checkbox-inline {
+ display: inline-block;
+ padding-left: 20px;
+ margin-bottom: 0;
+ vertical-align: middle;
+ font-weight: normal;
+ cursor: pointer; }
+
+.radio-inline + .radio-inline, .checkbox-inline + .checkbox-inline {
+ margin-top: 0;
+ margin-left: 10px; }
+
+input[type="radio"][disabled], fieldset[disabled] input[type="radio"], input[type="checkbox"][disabled], fieldset[disabled] input[type="checkbox"], .radio[disabled], fieldset[disabled] .radio, .radio-inline[disabled], fieldset[disabled] .radio-inline, .checkbox[disabled], fieldset[disabled] .checkbox, .checkbox-inline[disabled], fieldset[disabled] .checkbox-inline {
+ cursor: not-allowed; }
+
+.input-sm, .input-group-sm > .form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .btn {
+ height: 30px;
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px; }
+
+select.input-sm, .input-group-sm > select.form-control, .input-group-sm > select.input-group-addon, .input-group-sm > .input-group-btn > select.btn {
+ height: 30px;
+ line-height: 30px; }
+
+textarea.input-sm, .input-group-sm > textarea.form-control, .input-group-sm > textarea.input-group-addon, .input-group-sm > .input-group-btn > textarea.btn, select[multiple].input-sm, .input-group-sm > select[multiple].form-control, .input-group-sm > select[multiple].input-group-addon, .input-group-sm > .input-group-btn > select[multiple].btn {
+ height: auto; }
+
+.input-lg, .input-group-lg > .form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .btn {
+ height: 46px;
+ padding: 10px 16px;
+ font-size: 18px;
+ line-height: 1.33;
+ border-radius: 6px; }
+
+select.input-lg, .input-group-lg > select.form-control, .input-group-lg > select.input-group-addon, .input-group-lg > .input-group-btn > select.btn {
+ height: 46px;
+ line-height: 46px; }
+
+textarea.input-lg, .input-group-lg > textarea.form-control, .input-group-lg > textarea.input-group-addon, .input-group-lg > .input-group-btn > textarea.btn, select[multiple].input-lg, .input-group-lg > select[multiple].form-control, .input-group-lg > select[multiple].input-group-addon, .input-group-lg > .input-group-btn > select[multiple].btn {
+ height: auto; }
+
+.has-feedback {
+ position: relative; }
+ .has-feedback .form-control {
+ padding-right: 42.5px; }
+
+.form-control-feedback {
+ position: absolute;
+ top: 25px;
+ right: 0;
+ z-index: 2;
+ display: block;
+ width: 34px;
+ height: 34px;
+ line-height: 34px;
+ text-align: center; }
+
+.input-lg + .form-control-feedback, .input-lg + .input-group-lg > .form-control, .input-group-lg > .input-lg + .form-control, .input-lg + .input-group-lg > .input-group-addon, .input-group-lg > .input-lg + .input-group-addon, .input-lg + .input-group-lg > .input-group-btn > .btn, .input-group-lg > .input-group-btn > .input-lg + .btn {
+ width: 46px;
+ height: 46px;
+ line-height: 46px; }
+
+.input-sm + .form-control-feedback, .input-sm + .input-group-sm > .form-control, .input-group-sm > .input-sm + .form-control, .input-sm + .input-group-sm > .input-group-addon, .input-group-sm > .input-sm + .input-group-addon, .input-sm + .input-group-sm > .input-group-btn > .btn, .input-group-sm > .input-group-btn > .input-sm + .btn {
+ width: 30px;
+ height: 30px;
+ line-height: 30px; }
+
+.has-success .help-block, .has-success .control-label, .has-success .radio, .has-success .checkbox, .has-success .radio-inline, .has-success .checkbox-inline {
+ color: #3c763d; }
+.has-success .form-control {
+ border-color: #3c763d;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); }
+ .has-success .form-control:focus {
+ border-color: #2b542b;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168; }
+.has-success .input-group-addon {
+ color: #3c763d;
+ border-color: #3c763d;
+ background-color: #dff0d8; }
+.has-success .form-control-feedback {
+ color: #3c763d; }
+
+.has-warning .help-block, .has-warning .control-label, .has-warning .radio, .has-warning .checkbox, .has-warning .radio-inline, .has-warning .checkbox-inline {
+ color: #8a6d3b; }
+.has-warning .form-control {
+ border-color: #8a6d3b;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); }
+ .has-warning .form-control:focus {
+ border-color: #66502c;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c09f6b; }
+.has-warning .input-group-addon {
+ color: #8a6d3b;
+ border-color: #8a6d3b;
+ background-color: #fcf8e3; }
+.has-warning .form-control-feedback {
+ color: #8a6d3b; }
+
+.has-error .help-block, .has-error .control-label, .has-error .radio, .has-error .checkbox, .has-error .radio-inline, .has-error .checkbox-inline {
+ color: #a94442; }
+.has-error .form-control {
+ border-color: #a94442;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); }
+ .has-error .form-control:focus {
+ border-color: #843534;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483; }
+.has-error .input-group-addon {
+ color: #a94442;
+ border-color: #a94442;
+ background-color: #f2dede; }
+.has-error .form-control-feedback {
+ color: #a94442; }
+
+.form-control-static {
+ margin-bottom: 0; }
+
+.help-block {
+ display: block;
+ margin-top: 5px;
+ margin-bottom: 10px;
+ color: #737373; }
+
+@media (min-width: 768px) {
+ .form-inline .form-group, .form-inline .navbar-form {
+ display: inline-block;
+ margin-bottom: 0;
+ vertical-align: middle; }
+ .form-inline .form-control, .form-inline .navbar-form {
+ display: inline-block;
+ width: auto;
+ vertical-align: middle; }
+ .form-inline .input-group, .form-inline .navbar-form {
+ display: inline-table;
+ vertical-align: middle; }
+ .form-inline .input-group .input-group-addon, .form-inline .input-group .navbar-form, .form-inline .input-group .input-group-btn, .form-inline .input-group .navbar-form, .form-inline .input-group .form-control, .form-inline .input-group .navbar-form {
+ width: auto; }
+ .form-inline .input-group > .form-control, .form-inline .input-group > .navbar-form {
+ width: 100%; }
+ .form-inline .control-label, .form-inline .navbar-form {
+ margin-bottom: 0;
+ vertical-align: middle; }
+ .form-inline .radio, .form-inline .navbar-form, .form-inline .checkbox, .form-inline .navbar-form {
+ display: inline-block;
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-left: 0;
+ vertical-align: middle; }
+ .form-inline .radio input[type="radio"], .form-inline .radio .navbar-form, .form-inline .checkbox input[type="checkbox"], .form-inline .checkbox .navbar-form {
+ float: none;
+ margin-left: 0; }
+ .form-inline .has-feedback .form-control-feedback, .form-inline .has-feedback .navbar-form {
+ top: 0; } }
+
+.form-horizontal .radio, .form-horizontal .checkbox, .form-horizontal .radio-inline, .form-horizontal .checkbox-inline {
+ margin-top: 0;
+ margin-bottom: 0;
+ padding-top: 7px; }
+.form-horizontal .radio, .form-horizontal .checkbox {
+ min-height: 27px; }
+.form-horizontal .form-group {
+ margin-left: -15px;
+ margin-right: -15px; }
+ .form-horizontal .form-group:before, .form-horizontal .form-group:after {
+ content: " ";
+ display: table; }
+ .form-horizontal .form-group:after {
+ clear: both; }
+.form-horizontal .form-control-static {
+ padding-top: 7px;
+ padding-bottom: 7px; }
+@media (min-width: 768px) {
+ .form-horizontal .control-label {
+ text-align: right;
+ margin-bottom: 0;
+ padding-top: 7px; } }
+.form-horizontal .has-feedback .form-control-feedback {
+ top: 0;
+ right: 15px; }
+
+.btn {
+ display: inline-block;
+ margin-bottom: 0;
+ font-weight: normal;
+ text-align: center;
+ vertical-align: middle;
+ cursor: pointer;
+ background-image: none;
+ border: 1px solid transparent;
+ white-space: nowrap;
+ padding: 6px 12px;
+ font-size: 14px;
+ line-height: 1.42857;
+ border-radius: 4px;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ -ms-user-select: none;
+ user-select: none; }
+ .btn:focus, .btn:active:focus, .btn.active:focus {
+ outline: thin dotted;
+ outline: 5px auto -webkit-focus-ring-color;
+ outline-offset: -2px; }
+ .btn:hover, .btn:focus {
+ color: #333;
+ text-decoration: none; }
+ .btn:active, .btn.active {
+ outline: 0;
+ background-image: none;
+ box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); }
+ .btn.disabled, .btn[disabled], fieldset[disabled] .btn {
+ cursor: not-allowed;
+ pointer-events: none;
+ opacity: 0.65;
+ filter: alpha(opacity=65);
+ box-shadow: none; }
+
+.btn-default {
+ color: #333;
+ background-color: #fff;
+ border-color: #ccc; }
+ .btn-default:hover, .btn-default:focus, .btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
+ color: #333;
+ background-color: #e6e6e6;
+ border-color: #adadad; }
+ .btn-default:active, .btn-default.active, .open > .btn-default.dropdown-toggle {
+ background-image: none; }
+ .btn-default.disabled, .btn-default.disabled:hover, .btn-default.disabled:focus, .btn-default.disabled:active, .btn-default.disabled.active, .btn-default[disabled], .btn-default[disabled]:hover, .btn-default[disabled]:focus, .btn-default[disabled]:active, .btn-default[disabled].active, fieldset[disabled] .btn-default, fieldset[disabled] .btn-default:hover, fieldset[disabled] .btn-default:focus, fieldset[disabled] .btn-default:active, fieldset[disabled] .btn-default.active {
+ background-color: #fff;
+ border-color: #ccc; }
+ .btn-default .badge {
+ color: #fff;
+ background-color: #333; }
+
+.btn-primary {
+ color: #fff;
+ background-color: #428bca;
+ border-color: #3580bd; }
+ .btn-primary:hover, .btn-primary:focus, .btn-primary:active, .btn-primary.active, .open > .btn-primary.dropdown-toggle {
+ color: #fff;
+ background-color: #3073a9;
+ border-color: #28608e; }
+ .btn-primary:active, .btn-primary.active, .open > .btn-primary.dropdown-toggle {
+ background-image: none; }
+ .btn-primary.disabled, .btn-primary.disabled:hover, .btn-primary.disabled:focus, .btn-primary.disabled:active, .btn-primary.disabled.active, .btn-primary[disabled], .btn-primary[disabled]:hover, .btn-primary[disabled]:focus, .btn-primary[disabled]:active, .btn-primary[disabled].active, fieldset[disabled] .btn-primary, fieldset[disabled] .btn-primary:hover, fieldset[disabled] .btn-primary:focus, fieldset[disabled] .btn-primary:active, fieldset[disabled] .btn-primary.active {
+ background-color: #428bca;
+ border-color: #3580bd; }
+ .btn-primary .badge {
+ color: #428bca;
+ background-color: #fff; }
+
+.btn-success {
+ color: #fff;
+ background-color: #5cb85c;
+ border-color: #4eae4c; }
+ .btn-success:hover, .btn-success:focus, .btn-success:active, .btn-success.active, .open > .btn-success.dropdown-toggle {
+ color: #fff;
+ background-color: #469d44;
+ border-color: #3b8439; }
+ .btn-success:active, .btn-success.active, .open > .btn-success.dropdown-toggle {
+ background-image: none; }
+ .btn-success.disabled, .btn-success.disabled:hover, .btn-success.disabled:focus, .btn-success.disabled:active, .btn-success.disabled.active, .btn-success[disabled], .btn-success[disabled]:hover, .btn-success[disabled]:focus, .btn-success[disabled]:active, .btn-success[disabled].active, fieldset[disabled] .btn-success, fieldset[disabled] .btn-success:hover, fieldset[disabled] .btn-success:focus, fieldset[disabled] .btn-success:active, fieldset[disabled] .btn-success.active {
+ background-color: #5cb85c;
+ border-color: #4eae4c; }
+ .btn-success .badge {
+ color: #5cb85c;
+ background-color: #fff; }
+
+.btn-info {
+ color: #fff;
+ background-color: #5bc0de;
+ border-color: #46bada; }
+ .btn-info:hover, .btn-info:focus, .btn-info:active, .btn-info.active, .open > .btn-info.dropdown-toggle {
+ color: #fff;
+ background-color: #31b2d5;
+ border-color: #269cbc; }
+ .btn-info:active, .btn-info.active, .open > .btn-info.dropdown-toggle {
+ background-image: none; }
+ .btn-info.disabled, .btn-info.disabled:hover, .btn-info.disabled:focus, .btn-info.disabled:active, .btn-info.disabled.active, .btn-info[disabled], .btn-info[disabled]:hover, .btn-info[disabled]:focus, .btn-info[disabled]:active, .btn-info[disabled].active, fieldset[disabled] .btn-info, fieldset[disabled] .btn-info:hover, fieldset[disabled] .btn-info:focus, fieldset[disabled] .btn-info:active, fieldset[disabled] .btn-info.active {
+ background-color: #5bc0de;
+ border-color: #46bada; }
+ .btn-info .badge {
+ color: #5bc0de;
+ background-color: #fff; }
+
+.btn-warning {
+ color: #fff;
+ background-color: #f0ad4e;
+ border-color: #eea236; }
+ .btn-warning:hover, .btn-warning:focus, .btn-warning:active, .btn-warning.active, .open > .btn-warning.dropdown-toggle {
+ color: #fff;
+ background-color: #ec971f;
+ border-color: #d58112; }
+ .btn-warning:active, .btn-warning.active, .open > .btn-warning.dropdown-toggle {
+ background-image: none; }
+ .btn-warning.disabled, .btn-warning.disabled:hover, .btn-warning.disabled:focus, .btn-warning.disabled:active, .btn-warning.disabled.active, .btn-warning[disabled], .btn-warning[disabled]:hover, .btn-warning[disabled]:focus, .btn-warning[disabled]:active, .btn-warning[disabled].active, fieldset[disabled] .btn-warning, fieldset[disabled] .btn-warning:hover, fieldset[disabled] .btn-warning:focus, fieldset[disabled] .btn-warning:active, fieldset[disabled] .btn-warning.active {
+ background-color: #f0ad4e;
+ border-color: #eea236; }
+ .btn-warning .badge {
+ color: #f0ad4e;
+ background-color: #fff; }
+
+.btn-danger {
+ color: #fff;
+ background-color: #d9534f;
+ border-color: #d43d3a; }
+ .btn-danger:hover, .btn-danger:focus, .btn-danger:active, .btn-danger.active, .open > .btn-danger.dropdown-toggle {
+ color: #fff;
+ background-color: #c92e2c;
+ border-color: #ac2525; }
+ .btn-danger:active, .btn-danger.active, .open > .btn-danger.dropdown-toggle {
+ background-image: none; }
+ .btn-danger.disabled, .btn-danger.disabled:hover, .btn-danger.disabled:focus, .btn-danger.disabled:active, .btn-danger.disabled.active, .btn-danger[disabled], .btn-danger[disabled]:hover, .btn-danger[disabled]:focus, .btn-danger[disabled]:active, .btn-danger[disabled].active, fieldset[disabled] .btn-danger, fieldset[disabled] .btn-danger:hover, fieldset[disabled] .btn-danger:focus, fieldset[disabled] .btn-danger:active, fieldset[disabled] .btn-danger.active {
+ background-color: #d9534f;
+ border-color: #d43d3a; }
+ .btn-danger .badge {
+ color: #d9534f;
+ background-color: #fff; }
+
+.btn-link {
+ color: #428bca;
+ font-weight: normal;
+ cursor: pointer;
+ border-radius: 0; }
+ .btn-link, .btn-link:active, .btn-link[disabled], fieldset[disabled] .btn-link {
+ background-color: transparent;
+ box-shadow: none; }
+ .btn-link, .btn-link:hover, .btn-link:focus, .btn-link:active {
+ border-color: transparent; }
+ .btn-link:hover, .btn-link:focus {
+ color: #2a6596;
+ text-decoration: underline;
+ background-color: transparent; }
+ .btn-link[disabled]:hover, .btn-link[disabled]:focus, fieldset[disabled] .btn-link:hover, fieldset[disabled] .btn-link:focus {
+ color: #999999;
+ text-decoration: none; }
+
+.btn-lg, .btn-group-lg > .btn {
+ padding: 10px 16px;
+ font-size: 18px;
+ line-height: 1.33;
+ border-radius: 6px; }
+
+.btn-sm, .btn-group-sm > .btn {
+ padding: 5px 10px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px; }
+
+.btn-xs, .btn-group-xs > .btn {
+ padding: 1px 5px;
+ font-size: 12px;
+ line-height: 1.5;
+ border-radius: 3px; }
+
+.btn-block {
+ display: block;
+ width: 100%;
+ padding-left: 0;
+ padding-right: 0; }
+
+.btn-block + .btn-block {
+ margin-top: 5px; }
+
+input[type="submit"].btn-block, input[type="reset"].btn-block, input[type="button"].btn-block {
+ width: 100%; }
+
+.fade {
+ opacity: 0;
+ transition: opacity 0.15s linear; }
+ .fade.in {
+ opacity: 1; }
+
+.collapse {
+ display: none; }
+ .collapse.in {
+ display: block; }
+
+tr.collapse.in {
+ display: table-row; }
+
+tbody.collapse.in {
+ display: table-row-group; }
+
+.collapsing {
+ position: relative;
+ height: 0;
+ overflow: hidden;
+ transition: height 0.35s ease; }
+
+.caret {
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: 4px solid;
+ border-right: 4px solid transparent;
+ border-left: 4px solid transparent; }
+
+.dropdown {
+ position: relative; }
+
+.dropdown-toggle:focus {
+ outline: 0; }
+
+.dropdown-menu {
+ position: absolute;
+ top: 100%;
+ left: 0;
+ z-index: 1000;
+ display: none;
+ float: left;
+ min-width: 160px;
+ padding: 5px 0;
+ margin: 2px 0 0;
+ list-style: none;
+ font-size: 14px;
+ text-align: left;
+ background-color: #fff;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.15);
+ border-radius: 4px;
+ box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
+ background-clip: padding-box; }
+ .dropdown-menu.pull-right {
+ right: 0;
+ left: auto; }
+ .dropdown-menu .divider {
+ height: 1px;
+ margin: 9px 0;
+ overflow: hidden;
+ background-color: #e5e5e5; }
+ .dropdown-menu > li > a {
+ display: block;
+ padding: 3px 20px;
+ clear: both;
+ font-weight: normal;
+ line-height: 1.42857;
+ color: #333333;
+ white-space: nowrap; }
+
+.dropdown-menu > li > a:hover, .dropdown-menu > li > a:focus {
+ text-decoration: none;
+ color: #262626;
+ background-color: #f5f5f5; }
+
+.dropdown-menu > .active > a, .dropdown-menu > .active > a:hover, .dropdown-menu > .active > a:focus {
+ color: #fff;
+ text-decoration: none;
+ outline: 0;
+ background-color: #428bca; }
+
+.dropdown-menu > .disabled > a, .dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus {
+ color: #999999; }
+
+.dropdown-menu > .disabled > a:hover, .dropdown-menu > .disabled > a:focus {
+ text-decoration: none;
+ background-color: transparent;
+ background-image: none;
+ filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
+ cursor: not-allowed; }
+
+.open > .dropdown-menu {
+ display: block; }
+.open > a {
+ outline: 0; }
+
+.dropdown-menu-right {
+ left: auto;
+ right: 0; }
+
+.dropdown-menu-left {
+ left: 0;
+ right: auto; }
+
+.dropdown-header {
+ display: block;
+ padding: 3px 20px;
+ font-size: 12px;
+ line-height: 1.42857;
+ color: #999999; }
+
+.dropdown-backdrop {
+ position: fixed;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 0;
+ z-index: 990; }
+
+.pull-right > .dropdown-menu {
+ right: 0;
+ left: auto; }
+
+.dropup .caret, .navbar-fixed-bottom .dropdown .caret {
+ border-top: 0;
+ border-bottom: 4px solid;
+ content: ""; }
+.dropup .dropdown-menu, .navbar-fixed-bottom .dropdown .dropdown-menu {
+ top: auto;
+ bottom: 100%;
+ margin-bottom: 1px; }
+
+@media (min-width: 768px) {
+ .navbar-right .dropdown-menu {
+ right: 0;
+ left: auto; }
+ .navbar-right .dropdown-menu-left {
+ left: 0;
+ right: auto; } }
+
+.btn-group, .btn-group-vertical {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle; }
+ .btn-group > .btn, .btn-group-vertical > .btn {
+ position: relative;
+ float: left; }
+ .btn-group > .btn:hover, .btn-group > .btn:focus, .btn-group > .btn:active, .btn-group > .btn.active, .btn-group-vertical > .btn:hover, .btn-group-vertical > .btn:focus, .btn-group-vertical > .btn:active, .btn-group-vertical > .btn.active {
+ z-index: 2; }
+ .btn-group > .btn:focus, .btn-group-vertical > .btn:focus {
+ outline: 0; }
+
+.btn-group .btn + .btn, .btn-group .btn + .btn-group, .btn-group .btn-group + .btn, .btn-group .btn-group + .btn-group {
+ margin-left: -1px; }
+
+.btn-toolbar {
+ margin-left: -5px; }
+ .btn-toolbar:before, .btn-toolbar:after {
+ content: " ";
+ display: table; }
+ .btn-toolbar:after {
+ clear: both; }
+ .btn-toolbar .btn-group, .btn-toolbar .input-group {
+ float: left; }
+ .btn-toolbar > .btn, .btn-toolbar > .btn-group, .btn-toolbar > .input-group {
+ margin-left: 5px; }
+
+.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {
+ border-radius: 0; }
+
+.btn-group > .btn:first-child {
+ margin-left: 0; }
+ .btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0; }
+
+.btn-group > .btn:last-child:not(:first-child), .btn-group > .dropdown-toggle:not(:first-child) {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group > .btn-group {
+ float: left; }
+
+.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {
+ border-radius: 0; }
+
+.btn-group > .btn-group:first-child > .btn:last-child, .btn-group > .btn-group:first-child > .dropdown-toggle {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0; }
+
+.btn-group > .btn-group:last-child > .btn:first-child {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group .dropdown-toggle:active, .btn-group.open .dropdown-toggle {
+ outline: 0; }
+
+.btn-group > .btn + .dropdown-toggle {
+ padding-left: 8px;
+ padding-right: 8px; }
+
+.btn-group > .btn-lg + .dropdown-toggle, .btn-group > .btn-lg + .btn-group-lg > .btn, .btn-group-lg > .btn-group > .btn-lg + .btn {
+ padding-left: 12px;
+ padding-right: 12px; }
+
+.btn-group.open .dropdown-toggle {
+ box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125); }
+ .btn-group.open .dropdown-toggle.btn-link {
+ box-shadow: none; }
+
+.btn .caret {
+ margin-left: 0; }
+
+.btn-lg .caret, .btn-lg .btn-group-lg > .btn, .btn-group-lg > .btn-lg .btn {
+ border-width: 5px 5px 0;
+ border-bottom-width: 0; }
+
+.dropup .btn-lg .caret, .dropup .btn-lg .btn-group-lg > .btn, .btn-group-lg > .dropup .btn-lg .btn {
+ border-width: 0 5px 5px; }
+
+.btn-group-vertical > .btn, .btn-group-vertical > .btn-group, .btn-group-vertical > .btn-group > .btn {
+ display: block;
+ float: none;
+ width: 100%;
+ max-width: 100%; }
+.btn-group-vertical > .btn-group:before, .btn-group-vertical > .btn-group:after {
+ content: " ";
+ display: table; }
+.btn-group-vertical > .btn-group:after {
+ clear: both; }
+.btn-group-vertical > .btn-group > .btn {
+ float: none; }
+.btn-group-vertical > .btn + .btn, .btn-group-vertical > .btn + .btn-group, .btn-group-vertical > .btn-group + .btn, .btn-group-vertical > .btn-group + .btn-group {
+ margin-top: -1px;
+ margin-left: 0; }
+
+.btn-group-vertical > .btn:not(:first-child):not(:last-child) {
+ border-radius: 0; }
+.btn-group-vertical > .btn:first-child:not(:last-child) {
+ border-top-right-radius: 4px;
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0; }
+.btn-group-vertical > .btn:last-child:not(:first-child) {
+ border-bottom-left-radius: 4px;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {
+ border-radius: 0; }
+
+.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, .btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0; }
+
+.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.btn-group-justified {
+ display: table;
+ width: 100%;
+ table-layout: fixed;
+ border-collapse: separate; }
+ .btn-group-justified > .btn, .btn-group-justified > .btn-group {
+ float: none;
+ display: table-cell;
+ width: 1%; }
+ .btn-group-justified > .btn-group .btn {
+ width: 100%; }
+
+[data-toggle="buttons"] > .btn > input[type="radio"], [data-toggle="buttons"] > .btn > input[type="checkbox"] {
+ position: absolute;
+ z-index: -1;
+ opacity: 0; }
+
+.input-group {
+ position: relative;
+ display: table;
+ border-collapse: separate; }
+ .input-group[class*="col-"] {
+ float: none;
+ padding-left: 0;
+ padding-right: 0; }
+ .input-group .form-control {
+ position: relative;
+ z-index: 2;
+ float: left;
+ width: 100%;
+ margin-bottom: 0; }
+
+.input-group-addon, .input-group-btn, .input-group .form-control {
+ display: table-cell; }
+ .input-group-addon:not(:first-child):not(:last-child), .input-group-btn:not(:first-child):not(:last-child), .input-group .form-control:not(:first-child):not(:last-child) {
+ border-radius: 0; }
+
+.input-group-addon, .input-group-btn {
+ width: 1%;
+ white-space: nowrap;
+ vertical-align: middle; }
+
+.input-group-addon {
+ padding: 6px 12px;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 1;
+ color: #555555;
+ text-align: center;
+ background-color: #eeeeee;
+ border: 1px solid #ccc;
+ border-radius: 4px; }
+ .input-group-addon.input-sm, .input-group-sm > .input-group-addon.form-control, .input-group-sm > .input-group-addon, .input-group-sm > .input-group-btn > .input-group-addon.btn {
+ padding: 5px 10px;
+ font-size: 12px;
+ border-radius: 3px; }
+ .input-group-addon.input-lg, .input-group-lg > .input-group-addon.form-control, .input-group-lg > .input-group-addon, .input-group-lg > .input-group-btn > .input-group-addon.btn {
+ padding: 10px 16px;
+ font-size: 18px;
+ border-radius: 6px; }
+ .input-group-addon input[type="radio"], .input-group-addon input[type="checkbox"] {
+ margin-top: 0; }
+
+.input-group .form-control:first-child, .input-group-addon:first-child, .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group > .btn, .input-group-btn:first-child > .dropdown-toggle, .input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), .input-group-btn:last-child > .btn-group:not(:last-child) > .btn {
+ border-bottom-right-radius: 0;
+ border-top-right-radius: 0; }
+
+.input-group-addon:first-child {
+ border-right: 0; }
+
+.input-group .form-control:last-child, .input-group-addon:last-child, .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group > .btn, .input-group-btn:last-child > .dropdown-toggle, .input-group-btn:first-child > .btn:not(:first-child), .input-group-btn:first-child > .btn-group:not(:first-child) > .btn {
+ border-bottom-left-radius: 0;
+ border-top-left-radius: 0; }
+
+.input-group-addon:last-child {
+ border-left: 0; }
+
+.input-group-btn {
+ position: relative;
+ font-size: 0;
+ white-space: nowrap; }
+ .input-group-btn > .btn {
+ position: relative; }
+ .input-group-btn > .btn + .btn {
+ margin-left: -1px; }
+ .input-group-btn > .btn:hover, .input-group-btn > .btn:focus, .input-group-btn > .btn:active {
+ z-index: 2; }
+ .input-group-btn:first-child > .btn, .input-group-btn:first-child > .btn-group {
+ margin-right: -1px; }
+ .input-group-btn:last-child > .btn, .input-group-btn:last-child > .btn-group {
+ margin-left: -1px; }
+
+.nav {
+ margin-bottom: 0;
+ padding-left: 0;
+ list-style: none; }
+ .nav:before, .nav:after {
+ content: " ";
+ display: table; }
+ .nav:after {
+ clear: both; }
+ .nav > li {
+ position: relative;
+ display: block; }
+ .nav > li > a {
+ position: relative;
+ display: block;
+ padding: 10px 15px; }
+ .nav > li > a:hover, .nav > li > a:focus {
+ text-decoration: none;
+ background-color: #eeeeee; }
+ .nav > li.disabled > a {
+ color: #999999; }
+ .nav > li.disabled > a:hover, .nav > li.disabled > a:focus {
+ color: #999999;
+ text-decoration: none;
+ background-color: transparent;
+ cursor: not-allowed; }
+ .nav .open > a, .nav .open > a:hover, .nav .open > a:focus {
+ background-color: #eeeeee;
+ border-color: #428bca; }
+ .nav .nav-divider {
+ height: 1px;
+ margin: 9px 0;
+ overflow: hidden;
+ background-color: #e5e5e5; }
+ .nav > li > a > img {
+ max-width: none; }
+
+.nav-tabs {
+ border-bottom: 1px solid #ddd; }
+ .nav-tabs > li {
+ float: left;
+ margin-bottom: -1px; }
+ .nav-tabs > li > a {
+ margin-right: 2px;
+ line-height: 1.42857;
+ border: 1px solid transparent;
+ border-radius: 4px 4px 0 0; }
+ .nav-tabs > li > a:hover {
+ border-color: #eeeeee #eeeeee #ddd; }
+ .nav-tabs > li.active > a, .nav-tabs > li.active > a:hover, .nav-tabs > li.active > a:focus {
+ color: #555555;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-bottom-color: transparent;
+ cursor: default; }
+
+.nav-pills > li {
+ float: left; }
+ .nav-pills > li > a {
+ border-radius: 4px; }
+ .nav-pills > li + li {
+ margin-left: 2px; }
+ .nav-pills > li.active > a, .nav-pills > li.active > a:hover, .nav-pills > li.active > a:focus {
+ color: #fff;
+ background-color: #428bca; }
+
+.nav-stacked > li {
+ float: none; }
+ .nav-stacked > li + li {
+ margin-top: 2px;
+ margin-left: 0; }
+
+.nav-justified, .nav-tabs.nav-justified {
+ width: 100%; }
+ .nav-justified > li, .nav-justified > .nav-tabs.nav-justified {
+ float: none; }
+ .nav-justified > li > a, .nav-justified > li > .nav-tabs.nav-justified {
+ text-align: center;
+ margin-bottom: 5px; }
+ .nav-justified > .dropdown .dropdown-menu, .nav-justified > .dropdown .nav-tabs.nav-justified {
+ top: auto;
+ left: auto; }
+ @media (min-width: 768px) {
+ .nav-justified > li, .nav-justified > .nav-tabs.nav-justified {
+ display: table-cell;
+ width: 1%; }
+ .nav-justified > li > a, .nav-justified > li > .nav-tabs.nav-justified {
+ margin-bottom: 0; } }
+
+.nav-tabs-justified, .nav-tabs.nav-justified, .nav-tabs.nav-justified {
+ border-bottom: 0; }
+ .nav-tabs-justified > li > a, .nav-tabs-justified > li > .nav-tabs.nav-justified, .nav-tabs-justified > li > .nav-tabs.nav-justified {
+ margin-right: 0;
+ border-radius: 4px; }
+ .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:focus, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified {
+ border: 1px solid #ddd; }
+ @media (min-width: 768px) {
+ .nav-tabs-justified > li > a, .nav-tabs-justified > li > .nav-tabs.nav-justified, .nav-tabs-justified > li > .nav-tabs.nav-justified {
+ border-bottom: 1px solid #ddd;
+ border-radius: 4px 4px 0 0; }
+ .nav-tabs-justified > .active > a, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:hover, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > a:focus, .nav-tabs-justified > .active > .nav-tabs.nav-justified, .nav-tabs-justified > .active > .nav-tabs.nav-justified {
+ border-bottom-color: #fff; } }
+
+.tab-content > .tab-pane {
+ display: none; }
+.tab-content > .active {
+ display: block; }
+
+.nav-tabs .dropdown-menu {
+ margin-top: -1px;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.navbar {
+ position: relative;
+ min-height: 50px;
+ margin-bottom: 20px;
+ border: 1px solid transparent; }
+ .navbar:before, .navbar:after {
+ content: " ";
+ display: table; }
+ .navbar:after {
+ clear: both; }
+ @media (min-width: 768px) {
+ .navbar {
+ border-radius: 4px; } }
+
+.navbar-header:before, .navbar-header:after {
+ content: " ";
+ display: table; }
+.navbar-header:after {
+ clear: both; }
+@media (min-width: 768px) {
+ .navbar-header {
+ float: left; } }
+
+.navbar-collapse {
+ overflow-x: visible;
+ padding-right: 15px;
+ padding-left: 15px;
+ border-top: 1px solid transparent;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);
+ -webkit-overflow-scrolling: touch; }
+ .navbar-collapse:before, .navbar-collapse:after {
+ content: " ";
+ display: table; }
+ .navbar-collapse:after {
+ clear: both; }
+ .navbar-collapse.in {
+ overflow-y: auto; }
+ @media (min-width: 768px) {
+ .navbar-collapse {
+ width: auto;
+ border-top: 0;
+ box-shadow: none; }
+ .navbar-collapse.collapse {
+ display: block !important;
+ height: auto !important;
+ padding-bottom: 0;
+ overflow: visible !important; }
+ .navbar-collapse.in {
+ overflow-y: visible; }
+ .navbar-fixed-top .navbar-collapse, .navbar-static-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+ padding-left: 0;
+ padding-right: 0; } }
+
+.navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+ max-height: 340px; }
+ @media (max-width: 480px) and (orientation: landscape) {
+ .navbar-fixed-top .navbar-collapse, .navbar-fixed-bottom .navbar-collapse {
+ max-height: 200px; } }
+
+.container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
+ margin-right: -15px;
+ margin-left: -15px; }
+ @media (min-width: 768px) {
+ .container > .navbar-header, .container > .navbar-collapse, .container-fluid > .navbar-header, .container-fluid > .navbar-collapse {
+ margin-right: 0;
+ margin-left: 0; } }
+
+.navbar-static-top {
+ z-index: 1000;
+ border-width: 0 0 1px; }
+ @media (min-width: 768px) {
+ .navbar-static-top {
+ border-radius: 0; } }
+
+.navbar-fixed-top, .navbar-fixed-bottom {
+ position: fixed;
+ right: 0;
+ left: 0;
+ z-index: 1030; }
+ @media (min-width: 768px) {
+ .navbar-fixed-top, .navbar-fixed-bottom {
+ border-radius: 0; } }
+
+.navbar-fixed-top {
+ top: 0;
+ border-width: 0 0 1px; }
+
+.navbar-fixed-bottom {
+ bottom: 0;
+ margin-bottom: 0;
+ border-width: 1px 0 0; }
+
+.navbar-brand {
+ float: left;
+ padding: 15px 15px;
+ font-size: 18px;
+ line-height: 20px;
+ height: 50px; }
+ .navbar-brand:hover, .navbar-brand:focus {
+ text-decoration: none; }
+ @media (min-width: 768px) {
+ .navbar > .container .navbar-brand, .navbar > .container-fluid .navbar-brand {
+ margin-left: -15px; } }
+
+.navbar-toggle {
+ position: relative;
+ float: right;
+ margin-right: 15px;
+ padding: 9px 10px;
+ margin-top: 8px;
+ margin-bottom: 8px;
+ background-color: transparent;
+ background-image: none;
+ border: 1px solid transparent;
+ border-radius: 4px; }
+ .navbar-toggle:focus {
+ outline: 0; }
+ .navbar-toggle .icon-bar {
+ display: block;
+ width: 22px;
+ height: 2px;
+ border-radius: 1px; }
+ .navbar-toggle .icon-bar + .icon-bar {
+ margin-top: 4px; }
+ @media (min-width: 768px) {
+ .navbar-toggle {
+ display: none; } }
+
+.navbar-nav {
+ margin: 7.5px -15px; }
+ .navbar-nav > li > a {
+ padding-top: 10px;
+ padding-bottom: 10px;
+ line-height: 20px; }
+ @media (max-width: 767px) {
+ .navbar-nav .open .dropdown-menu {
+ position: static;
+ float: none;
+ width: auto;
+ margin-top: 0;
+ background-color: transparent;
+ border: 0;
+ box-shadow: none; }
+ .navbar-nav .open .dropdown-menu > li > a, .navbar-nav .open .dropdown-menu .dropdown-header {
+ padding: 5px 15px 5px 25px; }
+ .navbar-nav .open .dropdown-menu > li > a {
+ line-height: 20px; }
+ .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-nav .open .dropdown-menu > li > a:focus {
+ background-image: none; } }
+ @media (min-width: 768px) {
+ .navbar-nav {
+ float: left;
+ margin: 0; }
+ .navbar-nav > li {
+ float: left; }
+ .navbar-nav > li > a {
+ padding-top: 15px;
+ padding-bottom: 15px; }
+ .navbar-nav.navbar-right:last-child {
+ margin-right: -15px; } }
+
+@media (min-width: 768px) {
+ .navbar-left {
+ float: left !important; }
+ .navbar-right {
+ float: right !important; } }
+
+.navbar-form {
+ margin-left: -15px;
+ margin-right: -15px;
+ padding: 10px 15px;
+ border-top: 1px solid transparent;
+ border-bottom: 1px solid transparent;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);
+ margin-top: 8px;
+ margin-bottom: 8px; }
+ @media (max-width: 767px) {
+ .navbar-form .form-group {
+ margin-bottom: 5px; } }
+ @media (min-width: 768px) {
+ .navbar-form {
+ width: auto;
+ border: 0;
+ margin-left: 0;
+ margin-right: 0;
+ padding-top: 0;
+ padding-bottom: 0;
+ box-shadow: none; }
+ .navbar-form.navbar-right:last-child {
+ margin-right: -15px; } }
+
+.navbar-nav > li > .dropdown-menu {
+ margin-top: 0;
+ border-top-right-radius: 0;
+ border-top-left-radius: 0; }
+
+.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {
+ border-bottom-right-radius: 0;
+ border-bottom-left-radius: 0; }
+
+.navbar-btn {
+ margin-top: 8px;
+ margin-bottom: 8px; }
+ .navbar-btn.btn-sm, .btn-group-sm > .navbar-btn.btn {
+ margin-top: 10px;
+ margin-bottom: 10px; }
+ .navbar-btn.btn-xs, .btn-group-xs > .navbar-btn.btn {
+ margin-top: 14px;
+ margin-bottom: 14px; }
+
+.navbar-text {
+ margin-top: 15px;
+ margin-bottom: 15px; }
+ @media (min-width: 768px) {
+ .navbar-text {
+ float: left;
+ margin-left: 15px;
+ margin-right: 15px; }
+ .navbar-text.navbar-right:last-child {
+ margin-right: 0; } }
+
+.navbar-default {
+ background-color: #f8f8f8;
+ border-color: #e7e7e7; }
+ .navbar-default .navbar-brand {
+ color: #777; }
+ .navbar-default .navbar-brand:hover, .navbar-default .navbar-brand:focus {
+ color: #5e5e5e;
+ background-color: transparent; }
+ .navbar-default .navbar-text {
+ color: #777; }
+ .navbar-default .navbar-nav > li > a {
+ color: #777; }
+ .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus {
+ color: #333;
+ background-color: transparent; }
+ .navbar-default .navbar-nav > .active > a, .navbar-default .navbar-nav > .active > a:hover, .navbar-default .navbar-nav > .active > a:focus {
+ color: #555;
+ background-color: #e7e7e7; }
+ .navbar-default .navbar-nav > .disabled > a, .navbar-default .navbar-nav > .disabled > a:hover, .navbar-default .navbar-nav > .disabled > a:focus {
+ color: #ccc;
+ background-color: transparent; }
+ .navbar-default .navbar-toggle {
+ border-color: #ddd; }
+ .navbar-default .navbar-toggle:hover, .navbar-default .navbar-toggle:focus {
+ background-color: #ddd; }
+ .navbar-default .navbar-toggle .icon-bar {
+ background-color: #888; }
+ .navbar-default .navbar-collapse, .navbar-default .navbar-form {
+ border-color: #e7e7e7; }
+ .navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .open > a:hover, .navbar-default .navbar-nav > .open > a:focus {
+ background-color: #e7e7e7;
+ color: #555; }
+ @media (max-width: 767px) {
+ .navbar-default .navbar-nav .open .dropdown-menu > li > a {
+ color: #777; }
+ .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {
+ color: #333;
+ background-color: transparent; }
+ .navbar-default .navbar-nav .open .dropdown-menu > .active > a, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {
+ color: #555;
+ background-color: #e7e7e7; }
+ .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+ color: #ccc;
+ background-color: transparent; } }
+ .navbar-default .navbar-link {
+ color: #777; }
+ .navbar-default .navbar-link:hover {
+ color: #333; }
+ .navbar-default .btn-link {
+ color: #777; }
+ .navbar-default .btn-link:hover, .navbar-default .btn-link:focus {
+ color: #333; }
+ .navbar-default .btn-link[disabled]:hover, .navbar-default .btn-link[disabled]:focus, fieldset[disabled] .navbar-default .btn-link:hover, fieldset[disabled] .navbar-default .btn-link:focus {
+ color: #ccc; }
+
+.navbar-inverse {
+ background-color: #222;
+ border-color: #090909; }
+ .navbar-inverse .navbar-brand {
+ color: #999999; }
+ .navbar-inverse .navbar-brand:hover, .navbar-inverse .navbar-brand:focus {
+ color: #fff;
+ background-color: transparent; }
+ .navbar-inverse .navbar-text {
+ color: #999999; }
+ .navbar-inverse .navbar-nav > li > a {
+ color: #999999; }
+ .navbar-inverse .navbar-nav > li > a:hover, .navbar-inverse .navbar-nav > li > a:focus {
+ color: #fff;
+ background-color: transparent; }
+ .navbar-inverse .navbar-nav > .active > a, .navbar-inverse .navbar-nav > .active > a:hover, .navbar-inverse .navbar-nav > .active > a:focus {
+ color: #fff;
+ background-color: #090909; }
+ .navbar-inverse .navbar-nav > .disabled > a, .navbar-inverse .navbar-nav > .disabled > a:hover, .navbar-inverse .navbar-nav > .disabled > a:focus {
+ color: #444;
+ background-color: transparent; }
+ .navbar-inverse .navbar-toggle {
+ border-color: #333; }
+ .navbar-inverse .navbar-toggle:hover, .navbar-inverse .navbar-toggle:focus {
+ background-color: #333; }
+ .navbar-inverse .navbar-toggle .icon-bar {
+ background-color: #fff; }
+ .navbar-inverse .navbar-collapse, .navbar-inverse .navbar-form {
+ border-color: #101010; }
+ .navbar-inverse .navbar-nav > .open > a, .navbar-inverse .navbar-nav > .open > a:hover, .navbar-inverse .navbar-nav > .open > a:focus {
+ background-color: #090909;
+ color: #fff; }
+ @media (max-width: 767px) {
+ .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {
+ border-color: #090909; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu .divider {
+ background-color: #090909; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {
+ color: #999999; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {
+ color: #fff;
+ background-color: transparent; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {
+ color: #fff;
+ background-color: #090909; }
+ .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {
+ color: #444;
+ background-color: transparent; } }
+ .navbar-inverse .navbar-link {
+ color: #999999; }
+ .navbar-inverse .navbar-link:hover {
+ color: #fff; }
+ .navbar-inverse .btn-link {
+ color: #999999; }
+ .navbar-inverse .btn-link:hover, .navbar-inverse .btn-link:focus {
+ color: #fff; }
+ .navbar-inverse .btn-link[disabled]:hover, .navbar-inverse .btn-link[disabled]:focus, fieldset[disabled] .navbar-inverse .btn-link:hover, fieldset[disabled] .navbar-inverse .btn-link:focus {
+ color: #444; }
+
+.breadcrumb {
+ padding: 8px 15px;
+ margin-bottom: 20px;
+ list-style: none;
+ background-color: #f5f5f5;
+ border-radius: 4px; }
+ .breadcrumb > li {
+ display: inline-block; }
+ .breadcrumb > li + li:before {
+ content: "/\00a0";
+ padding: 0 5px;
+ color: #ccc; }
+ .breadcrumb > .active {
+ color: #999999; }
+
+.pagination {
+ display: inline-block;
+ padding-left: 0;
+ margin: 20px 0;
+ border-radius: 4px; }
+ .pagination > li {
+ display: inline; }
+ .pagination > li > a, .pagination > li > span {
+ position: relative;
+ float: left;
+ padding: 6px 12px;
+ line-height: 1.42857;
+ text-decoration: none;
+ color: #428bca;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ margin-left: -1px; }
+ .pagination > li:first-child > a, .pagination > li:first-child > span {
+ margin-left: 0;
+ border-bottom-left-radius: 4px;
+ border-top-left-radius: 4px; }
+ .pagination > li:last-child > a, .pagination > li:last-child > span {
+ border-bottom-right-radius: 4px;
+ border-top-right-radius: 4px; }
+ .pagination > li > a:hover, .pagination > li > a:focus, .pagination > li > span:hover, .pagination > li > span:focus {
+ color: #2a6596;
+ background-color: #eeeeee;
+ border-color: #ddd; }
+ .pagination > .active > a, .pagination > .active > a:hover, .pagination > .active > a:focus, .pagination > .active > span, .pagination > .active > span:hover, .pagination > .active > span:focus {
+ z-index: 2;
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca;
+ cursor: default; }
+ .pagination > .disabled > span, .pagination > .disabled > span:hover, .pagination > .disabled > span:focus, .pagination > .disabled > a, .pagination > .disabled > a:hover, .pagination > .disabled > a:focus {
+ color: #999999;
+ background-color: #fff;
+ border-color: #ddd;
+ cursor: not-allowed; }
+
+.pagination-lg > li > a, .pagination-lg > li > span {
+ padding: 10px 16px;
+ font-size: 18px; }
+.pagination-lg > li:first-child > a, .pagination-lg > li:first-child > span {
+ border-bottom-left-radius: 6px;
+ border-top-left-radius: 6px; }
+.pagination-lg > li:last-child > a, .pagination-lg > li:last-child > span {
+ border-bottom-right-radius: 6px;
+ border-top-right-radius: 6px; }
+
+.pagination-sm > li > a, .pagination-sm > li > span {
+ padding: 5px 10px;
+ font-size: 12px; }
+.pagination-sm > li:first-child > a, .pagination-sm > li:first-child > span {
+ border-bottom-left-radius: 3px;
+ border-top-left-radius: 3px; }
+.pagination-sm > li:last-child > a, .pagination-sm > li:last-child > span {
+ border-bottom-right-radius: 3px;
+ border-top-right-radius: 3px; }
+
+.pager {
+ padding-left: 0;
+ margin: 20px 0;
+ list-style: none;
+ text-align: center; }
+ .pager:before, .pager:after {
+ content: " ";
+ display: table; }
+ .pager:after {
+ clear: both; }
+ .pager li {
+ display: inline; }
+ .pager li > a, .pager li > span {
+ display: inline-block;
+ padding: 5px 14px;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 15px; }
+ .pager li > a:hover, .pager li > a:focus {
+ text-decoration: none;
+ background-color: #eeeeee; }
+ .pager .next > a, .pager .next > span {
+ float: right; }
+ .pager .previous > a, .pager .previous > span {
+ float: left; }
+ .pager .disabled > a, .pager .disabled > a:hover, .pager .disabled > a:focus, .pager .disabled > span {
+ color: #999999;
+ background-color: #fff;
+ cursor: not-allowed; }
+
+.label {
+ display: inline;
+ padding: 0.2em 0.6em 0.3em;
+ font-size: 75%;
+ font-weight: bold;
+ line-height: 1;
+ color: #fff;
+ text-align: center;
+ white-space: nowrap;
+ vertical-align: baseline;
+ border-radius: 0.25em; }
+ .label:empty {
+ display: none; }
+ .btn .label {
+ position: relative;
+ top: -1px; }
+
+a.label:hover, a.label:focus {
+ color: #fff;
+ text-decoration: none;
+ cursor: pointer; }
+
+.label-default {
+ background-color: #999999; }
+ .label-default[href]:hover, .label-default[href]:focus {
+ background-color: #808080; }
+
+.label-primary {
+ background-color: #428bca; }
+ .label-primary[href]:hover, .label-primary[href]:focus {
+ background-color: #3073a9; }
+
+.label-success {
+ background-color: #5cb85c; }
+ .label-success[href]:hover, .label-success[href]:focus {
+ background-color: #469d44; }
+
+.label-info {
+ background-color: #5bc0de; }
+ .label-info[href]:hover, .label-info[href]:focus {
+ background-color: #31b2d5; }
+
+.label-warning {
+ background-color: #f0ad4e; }
+ .label-warning[href]:hover, .label-warning[href]:focus {
+ background-color: #ec971f; }
+
+.label-danger {
+ background-color: #d9534f; }
+ .label-danger[href]:hover, .label-danger[href]:focus {
+ background-color: #c92e2c; }
+
+.badge {
+ display: inline-block;
+ min-width: 10px;
+ padding: 3px 7px;
+ font-size: 12px;
+ font-weight: bold;
+ color: #fff;
+ line-height: 1;
+ vertical-align: baseline;
+ white-space: nowrap;
+ text-align: center;
+ background-color: #999999;
+ border-radius: 10px; }
+ .badge:empty {
+ display: none; }
+ .btn .badge {
+ position: relative;
+ top: -1px; }
+ .btn-xs .badge, .btn-xs .btn-group-xs > .btn, .btn-group-xs > .btn-xs .btn {
+ top: 0;
+ padding: 1px 5px; }
+ a.list-group-item.active > .badge, .nav-pills > .active > a > .badge {
+ color: #428bca;
+ background-color: #fff; }
+ .nav-pills > li > a > .badge {
+ margin-left: 3px; }
+
+a.badge:hover, a.badge:focus {
+ color: #fff;
+ text-decoration: none;
+ cursor: pointer; }
+
+.jumbotron {
+ padding: 30px;
+ margin-bottom: 30px;
+ color: inherit;
+ background-color: #eeeeee; }
+ .jumbotron h1, .jumbotron .h1 {
+ color: inherit; }
+ .jumbotron p {
+ margin-bottom: 15px;
+ font-size: 21px;
+ font-weight: 200; }
+ .jumbotron > hr {
+ border-top-color: #d5d5d5; }
+ .container .jumbotron {
+ border-radius: 6px; }
+ .jumbotron .container {
+ max-width: 100%; }
+ @media screen and (min-width: 768px) {
+ .jumbotron {
+ padding-top: 48px;
+ padding-bottom: 48px; }
+ .container .jumbotron {
+ padding-left: 60px;
+ padding-right: 60px; }
+ .jumbotron h1, .jumbotron .h1 {
+ font-size: 63px; } }
+
+.thumbnail {
+ display: block;
+ padding: 4px;
+ margin-bottom: 20px;
+ line-height: 1.42857;
+ background-color: #fff;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ transition: all 0.2s ease-in-out; }
+ .thumbnail > img, .thumbnail a > img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ margin-left: auto;
+ margin-right: auto; }
+ .thumbnail .caption {
+ padding: 9px;
+ color: #333333; }
+
+a.thumbnail:hover, a.thumbnail:focus, a.thumbnail.active {
+ border-color: #428bca; }
+
+.alert {
+ padding: 15px;
+ margin-bottom: 20px;
+ border: 1px solid transparent;
+ border-radius: 4px; }
+ .alert h4 {
+ margin-top: 0;
+ color: inherit; }
+ .alert .alert-link {
+ font-weight: bold; }
+ .alert > p, .alert > ul {
+ margin-bottom: 0; }
+ .alert > p + p {
+ margin-top: 5px; }
+
+.alert-dismissable {
+ padding-right: 35px; }
+ .alert-dismissable .close {
+ position: relative;
+ top: -2px;
+ right: -21px;
+ color: inherit; }
+
+.alert-success {
+ background-color: #dff0d8;
+ border-color: #d7e9c6;
+ color: #3c763d; }
+ .alert-success hr {
+ border-top-color: #cae2b3; }
+ .alert-success .alert-link {
+ color: #2b542b; }
+
+.alert-info {
+ background-color: #d9edf7;
+ border-color: #bce9f1;
+ color: #31708f; }
+ .alert-info hr {
+ border-top-color: #a6e2ec; }
+ .alert-info .alert-link {
+ color: #245369; }
+
+.alert-warning {
+ background-color: #fcf8e3;
+ border-color: #faeacc;
+ color: #8a6d3b; }
+ .alert-warning hr {
+ border-top-color: #f7e0b5; }
+ .alert-warning .alert-link {
+ color: #66502c; }
+
+.alert-danger {
+ background-color: #f2dede;
+ border-color: #ebccd1;
+ color: #a94442; }
+ .alert-danger hr {
+ border-top-color: #e4b9c0; }
+ .alert-danger .alert-link {
+ color: #843534; }
+
+@-webkit-keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0; }
+
+ to {
+ background-position: 0 0; } }
+
+@keyframes progress-bar-stripes {
+ from {
+ background-position: 40px 0; }
+
+ to {
+ background-position: 0 0; } }
+
+.progress {
+ overflow: hidden;
+ height: 20px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); }
+
+.progress-bar {
+ float: left;
+ width: 0%;
+ height: 100%;
+ font-size: 12px;
+ line-height: 20px;
+ color: #fff;
+ text-align: center;
+ background-color: #428bca;
+ box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);
+ transition: width 0.6s ease; }
+
+.progress-striped .progress-bar {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
+ background-size: 40px 40px; }
+
+.progress.active .progress-bar {
+ -webkit-animation: progress-bar-stripes 2s linear infinite;
+ animation: progress-bar-stripes 2s linear infinite; }
+
+.progress-bar[aria-valuenow="1"], .progress-bar[aria-valuenow="2"] {
+ min-width: 30px; }
+.progress-bar[aria-valuenow="0"] {
+ color: #999999;
+ min-width: 30px;
+ background-color: transparent;
+ background-image: none;
+ box-shadow: none; }
+
+.progress-bar-success {
+ background-color: #5cb85c; }
+ .progress-striped .progress-bar-success {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.progress-bar-info {
+ background-color: #5bc0de; }
+ .progress-striped .progress-bar-info {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.progress-bar-warning {
+ background-color: #f0ad4e; }
+ .progress-striped .progress-bar-warning {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.progress-bar-danger {
+ background-color: #d9534f; }
+ .progress-striped .progress-bar-danger {
+ background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent); }
+
+.media, .media-body {
+ overflow: hidden;
+ zoom: 1; }
+
+.media, .media .media {
+ margin-top: 15px; }
+
+.media:first-child {
+ margin-top: 0; }
+
+.media-object {
+ display: block; }
+
+.media-heading {
+ margin: 0 0 5px; }
+
+.media > .pull-left {
+ margin-right: 10px; }
+.media > .pull-right {
+ margin-left: 10px; }
+
+.media-list {
+ padding-left: 0;
+ list-style: none; }
+
+.list-group {
+ margin-bottom: 20px;
+ padding-left: 0; }
+
+.list-group-item {
+ position: relative;
+ display: block;
+ padding: 10px 15px;
+ margin-bottom: -1px;
+ background-color: #fff;
+ border: 1px solid #ddd; }
+ .list-group-item:first-child {
+ border-top-right-radius: 4px;
+ border-top-left-radius: 4px; }
+ .list-group-item:last-child {
+ margin-bottom: 0;
+ border-bottom-right-radius: 4px;
+ border-bottom-left-radius: 4px; }
+ .list-group-item > .badge {
+ float: right; }
+ .list-group-item > .badge + .badge {
+ margin-right: 5px; }
+
+a.list-group-item {
+ color: #555; }
+ a.list-group-item .list-group-item-heading {
+ color: #333; }
+ a.list-group-item:hover, a.list-group-item:focus {
+ text-decoration: none;
+ color: #555;
+ background-color: #f5f5f5; }
+
+.list-group-item.disabled, .list-group-item.disabled:hover, .list-group-item.disabled:focus {
+ background-color: #eeeeee;
+ color: #999999; }
+ .list-group-item.disabled .list-group-item-heading, .list-group-item.disabled:hover .list-group-item-heading, .list-group-item.disabled:focus .list-group-item-heading {
+ color: inherit; }
+ .list-group-item.disabled .list-group-item-text, .list-group-item.disabled:hover .list-group-item-text, .list-group-item.disabled:focus .list-group-item-text {
+ color: #999999; }
+.list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus {
+ z-index: 2;
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca; }
+ .list-group-item.active .list-group-item-heading, .list-group-item.active:hover .list-group-item-heading, .list-group-item.active:focus .list-group-item-heading {
+ color: inherit; }
+ .list-group-item.active .list-group-item-text, .list-group-item.active:hover .list-group-item-text, .list-group-item.active:focus .list-group-item-text {
+ color: #e1edf7; }
+
+.list-group-item-success {
+ color: #3c763d;
+ background-color: #dff0d8; }
+
+a.list-group-item-success {
+ color: #3c763d; }
+ a.list-group-item-success .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-success:hover, a.list-group-item-success:focus {
+ color: #3c763d;
+ background-color: #d0e9c6; }
+ a.list-group-item-success.active, a.list-group-item-success.active:hover, a.list-group-item-success.active:focus {
+ color: #fff;
+ background-color: #3c763d;
+ border-color: #3c763d; }
+
+.list-group-item-info {
+ color: #31708f;
+ background-color: #d9edf7; }
+
+a.list-group-item-info {
+ color: #31708f; }
+ a.list-group-item-info .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-info:hover, a.list-group-item-info:focus {
+ color: #31708f;
+ background-color: #c4e4f3; }
+ a.list-group-item-info.active, a.list-group-item-info.active:hover, a.list-group-item-info.active:focus {
+ color: #fff;
+ background-color: #31708f;
+ border-color: #31708f; }
+
+.list-group-item-warning {
+ color: #8a6d3b;
+ background-color: #fcf8e3; }
+
+a.list-group-item-warning {
+ color: #8a6d3b; }
+ a.list-group-item-warning .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-warning:hover, a.list-group-item-warning:focus {
+ color: #8a6d3b;
+ background-color: #faf2cc; }
+ a.list-group-item-warning.active, a.list-group-item-warning.active:hover, a.list-group-item-warning.active:focus {
+ color: #fff;
+ background-color: #8a6d3b;
+ border-color: #8a6d3b; }
+
+.list-group-item-danger {
+ color: #a94442;
+ background-color: #f2dede; }
+
+a.list-group-item-danger {
+ color: #a94442; }
+ a.list-group-item-danger .list-group-item-heading {
+ color: inherit; }
+ a.list-group-item-danger:hover, a.list-group-item-danger:focus {
+ color: #a94442;
+ background-color: #ebcccc; }
+ a.list-group-item-danger.active, a.list-group-item-danger.active:hover, a.list-group-item-danger.active:focus {
+ color: #fff;
+ background-color: #a94442;
+ border-color: #a94442; }
+
+.list-group-item-heading {
+ margin-top: 0;
+ margin-bottom: 5px; }
+
+.list-group-item-text {
+ margin-bottom: 0;
+ line-height: 1.3; }
+
+.panel {
+ margin-bottom: 20px;
+ background-color: #fff;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05); }
+
+.panel-body {
+ padding: 15px; }
+ .panel-body:before, .panel-body:after {
+ content: " ";
+ display: table; }
+ .panel-body:after {
+ clear: both; }
+
+.panel-heading {
+ padding: 10px 15px;
+ border-bottom: 1px solid transparent;
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px; }
+ .panel-heading > .dropdown .dropdown-toggle {
+ color: inherit; }
+
+.panel-title {
+ margin-top: 0;
+ margin-bottom: 0;
+ font-size: 16px;
+ color: inherit; }
+ .panel-title > a {
+ color: inherit; }
+
+.panel-footer {
+ padding: 10px 15px;
+ background-color: #f5f5f5;
+ border-top: 1px solid #ddd;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px; }
+
+.panel > .list-group {
+ margin-bottom: 0; }
+ .panel > .list-group .list-group-item {
+ border-width: 1px 0;
+ border-radius: 0; }
+ .panel > .list-group:first-child .list-group-item:first-child {
+ border-top: 0;
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px; }
+ .panel > .list-group:last-child .list-group-item:last-child {
+ border-bottom: 0;
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px; }
+
+.panel-heading + .list-group .list-group-item:first-child {
+ border-top-width: 0; }
+
+.panel > .table, .panel > .table-responsive > .table {
+ margin-bottom: 0; }
+.panel > .table:first-child, .panel > .table-responsive:first-child > .table:first-child {
+ border-top-right-radius: 3px;
+ border-top-left-radius: 3px; }
+ .panel > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {
+ border-top-left-radius: 3px; }
+ .panel > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, .panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {
+ border-top-right-radius: 3px; }
+.panel > .table:last-child, .panel > .table-responsive:last-child > .table:last-child {
+ border-bottom-right-radius: 3px;
+ border-bottom-left-radius: 3px; }
+ .panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {
+ border-bottom-left-radius: 3px; }
+ .panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, .panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {
+ border-bottom-right-radius: 3px; }
+.panel > .panel-body + .table, .panel > .panel-body + .table-responsive {
+ border-top: 1px solid #ddd; }
+.panel > .table > tbody:first-child > tr:first-child th, .panel > .table > tbody:first-child > tr:first-child td {
+ border-top: 0; }
+.panel > .table-bordered, .panel > .table-responsive > .table-bordered {
+ border: 0; }
+ .panel > .table-bordered > thead > tr > th:first-child, .panel > .table-bordered > thead > tr > td:first-child, .panel > .table-bordered > tbody > tr > th:first-child, .panel > .table-bordered > tbody > tr > td:first-child, .panel > .table-bordered > tfoot > tr > th:first-child, .panel > .table-bordered > tfoot > tr > td:first-child, .panel > .table-responsive > .table-bordered > thead > tr > th:first-child, .panel > .table-responsive > .table-bordered > thead > tr > td:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {
+ border-left: 0; }
+ .panel > .table-bordered > thead > tr > th:last-child, .panel > .table-bordered > thead > tr > td:last-child, .panel > .table-bordered > tbody > tr > th:last-child, .panel > .table-bordered > tbody > tr > td:last-child, .panel > .table-bordered > tfoot > tr > th:last-child, .panel > .table-bordered > tfoot > tr > td:last-child, .panel > .table-responsive > .table-bordered > thead > tr > th:last-child, .panel > .table-responsive > .table-bordered > thead > tr > td:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, .panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, .panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {
+ border-right: 0; }
+ .panel > .table-bordered > thead > tr:first-child > td, .panel > .table-bordered > thead > tr:first-child > th, .panel > .table-bordered > tbody > tr:first-child > td, .panel > .table-bordered > tbody > tr:first-child > th, .panel > .table-responsive > .table-bordered > thead > tr:first-child > td, .panel > .table-responsive > .table-bordered > thead > tr:first-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {
+ border-bottom: 0; }
+ .panel > .table-bordered > tbody > tr:last-child > td, .panel > .table-bordered > tbody > tr:last-child > th, .panel > .table-bordered > tfoot > tr:last-child > td, .panel > .table-bordered > tfoot > tr:last-child > th, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, .panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, .panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {
+ border-bottom: 0; }
+.panel > .table-responsive {
+ border: 0;
+ margin-bottom: 0; }
+
+.panel-group {
+ margin-bottom: 20px; }
+ .panel-group .panel {
+ margin-bottom: 0;
+ border-radius: 4px; }
+ .panel-group .panel + .panel {
+ margin-top: 5px; }
+ .panel-group .panel-heading {
+ border-bottom: 0; }
+ .panel-group .panel-heading + .panel-collapse .panel-body {
+ border-top: 1px solid #ddd; }
+ .panel-group .panel-footer {
+ border-top: 0; }
+ .panel-group .panel-footer + .panel-collapse .panel-body {
+ border-bottom: 1px solid #ddd; }
+
+.panel-default {
+ border-color: #ddd; }
+ .panel-default > .panel-heading {
+ color: #333333;
+ background-color: #f5f5f5;
+ border-color: #ddd; }
+ .panel-default > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #ddd; }
+ .panel-default > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #ddd; }
+
+.panel-primary {
+ border-color: #428bca; }
+ .panel-primary > .panel-heading {
+ color: #fff;
+ background-color: #428bca;
+ border-color: #428bca; }
+ .panel-primary > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #428bca; }
+ .panel-primary > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #428bca; }
+
+.panel-success {
+ border-color: #d7e9c6; }
+ .panel-success > .panel-heading {
+ color: #3c763d;
+ background-color: #dff0d8;
+ border-color: #d7e9c6; }
+ .panel-success > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #d7e9c6; }
+ .panel-success > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #d7e9c6; }
+
+.panel-info {
+ border-color: #bce9f1; }
+ .panel-info > .panel-heading {
+ color: #31708f;
+ background-color: #d9edf7;
+ border-color: #bce9f1; }
+ .panel-info > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #bce9f1; }
+ .panel-info > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #bce9f1; }
+
+.panel-warning {
+ border-color: #faeacc; }
+ .panel-warning > .panel-heading {
+ color: #8a6d3b;
+ background-color: #fcf8e3;
+ border-color: #faeacc; }
+ .panel-warning > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #faeacc; }
+ .panel-warning > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #faeacc; }
+
+.panel-danger {
+ border-color: #ebccd1; }
+ .panel-danger > .panel-heading {
+ color: #a94442;
+ background-color: #f2dede;
+ border-color: #ebccd1; }
+ .panel-danger > .panel-heading + .panel-collapse > .panel-body {
+ border-top-color: #ebccd1; }
+ .panel-danger > .panel-footer + .panel-collapse > .panel-body {
+ border-bottom-color: #ebccd1; }
+
+.embed-responsive {
+ position: relative;
+ display: block;
+ height: 0;
+ padding: 0;
+ overflow: hidden; }
+ .embed-responsive .embed-responsive-item, .embed-responsive iframe, .embed-responsive embed, .embed-responsive object {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ height: 100%;
+ width: 100%;
+ border: 0; }
+ .embed-responsive.embed-responsive-16by9 {
+ padding-bottom: 56.25%; }
+ .embed-responsive.embed-responsive-4by3 {
+ padding-bottom: 75%; }
+
+.well {
+ min-height: 20px;
+ padding: 19px;
+ margin-bottom: 20px;
+ background-color: #f5f5f5;
+ border: 1px solid #e3e3e3;
+ border-radius: 4px;
+ box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); }
+ .well blockquote {
+ border-color: #ddd;
+ border-color: rgba(0, 0, 0, 0.15); }
+
+.well-lg {
+ padding: 24px;
+ border-radius: 6px; }
+
+.well-sm {
+ padding: 9px;
+ border-radius: 3px; }
+
+.close {
+ float: right;
+ font-size: 21px;
+ font-weight: bold;
+ line-height: 1;
+ color: #000;
+ text-shadow: 0 1px 0 #fff;
+ opacity: 0.2;
+ filter: alpha(opacity=20); }
+ .close:hover, .close:focus {
+ color: #000;
+ text-decoration: none;
+ cursor: pointer;
+ opacity: 0.5;
+ filter: alpha(opacity=50); }
+
+button.close {
+ padding: 0;
+ cursor: pointer;
+ background: transparent;
+ border: 0;
+ -webkit-appearance: none; }
+
+.modal-open {
+ overflow: hidden; }
+
+.modal {
+ display: none;
+ overflow: auto;
+ overflow-y: scroll;
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1050;
+ -webkit-overflow-scrolling: touch;
+ outline: 0; }
+ .modal.fade .modal-dialog {
+ -webkit-transform: translate(0, -25%);
+ transform: translate(0, -25%);
+ transition: -webkit-transform 0.3s ease-out;
+ transition: transform 0.3s ease-out; }
+ .modal.in .modal-dialog {
+ -webkit-transform: translate(0, 0);
+ transform: translate(0, 0); }
+
+.modal-dialog {
+ position: relative;
+ width: auto;
+ margin: 10px; }
+
+.modal-content {
+ position: relative;
+ background-color: #fff;
+ border: 1px solid #999;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);
+ background-clip: padding-box;
+ outline: 0; }
+
+.modal-backdrop {
+ position: fixed;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ z-index: 1040;
+ background-color: #000; }
+ .modal-backdrop.fade {
+ opacity: 0;
+ filter: alpha(opacity=0); }
+ .modal-backdrop.in {
+ opacity: 0.5;
+ filter: alpha(opacity=50); }
+
+.modal-header {
+ padding: 15px;
+ border-bottom: 1px solid #e5e5e5;
+ min-height: 16.42857px; }
+
+.modal-header .close {
+ margin-top: -2px; }
+
+.modal-title {
+ margin: 0;
+ line-height: 1.42857; }
+
+.modal-body {
+ position: relative;
+ padding: 15px; }
+
+.modal-footer {
+ padding: 15px;
+ text-align: right;
+ border-top: 1px solid #e5e5e5; }
+ .modal-footer:before, .modal-footer:after {
+ content: " ";
+ display: table; }
+ .modal-footer:after {
+ clear: both; }
+ .modal-footer .btn + .btn {
+ margin-left: 5px;
+ margin-bottom: 0; }
+ .modal-footer .btn-group .btn + .btn {
+ margin-left: -1px; }
+ .modal-footer .btn-block + .btn-block {
+ margin-left: 0; }
+
+.modal-scrollbar-measure {
+ position: absolute;
+ top: -9999px;
+ width: 50px;
+ height: 50px;
+ overflow: scroll; }
+
+@media (min-width: 768px) {
+ .modal-dialog {
+ width: 600px;
+ margin: 30px auto; }
+ .modal-content {
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5); }
+ .modal-sm {
+ width: 300px; } }
+
+@media (min-width: 992px) {
+ .modal-lg {
+ width: 900px; } }
+
+.tooltip {
+ position: absolute;
+ z-index: 1070;
+ display: block;
+ visibility: visible;
+ font-size: 12px;
+ line-height: 1.4;
+ opacity: 0;
+ filter: alpha(opacity=0); }
+ .tooltip.in {
+ opacity: 0.9;
+ filter: alpha(opacity=90); }
+ .tooltip.top {
+ margin-top: -3px;
+ padding: 5px 0; }
+ .tooltip.right {
+ margin-left: 3px;
+ padding: 0 5px; }
+ .tooltip.bottom {
+ margin-top: 3px;
+ padding: 5px 0; }
+ .tooltip.left {
+ margin-left: -3px;
+ padding: 0 5px; }
+
+.tooltip-inner {
+ max-width: 200px;
+ padding: 3px 8px;
+ color: #fff;
+ text-align: center;
+ text-decoration: none;
+ background-color: #000;
+ border-radius: 4px; }
+
+.tooltip-arrow {
+ position: absolute;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid; }
+
+.tooltip.top .tooltip-arrow {
+ bottom: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000; }
+.tooltip.top-left .tooltip-arrow {
+ bottom: 0;
+ left: 5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000; }
+.tooltip.top-right .tooltip-arrow {
+ bottom: 0;
+ right: 5px;
+ border-width: 5px 5px 0;
+ border-top-color: #000; }
+.tooltip.right .tooltip-arrow {
+ top: 50%;
+ left: 0;
+ margin-top: -5px;
+ border-width: 5px 5px 5px 0;
+ border-right-color: #000; }
+.tooltip.left .tooltip-arrow {
+ top: 50%;
+ right: 0;
+ margin-top: -5px;
+ border-width: 5px 0 5px 5px;
+ border-left-color: #000; }
+.tooltip.bottom .tooltip-arrow {
+ top: 0;
+ left: 50%;
+ margin-left: -5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000; }
+.tooltip.bottom-left .tooltip-arrow {
+ top: 0;
+ left: 5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000; }
+.tooltip.bottom-right .tooltip-arrow {
+ top: 0;
+ right: 5px;
+ border-width: 0 5px 5px;
+ border-bottom-color: #000; }
+
+.popover {
+ position: absolute;
+ top: 0;
+ left: 0;
+ z-index: 1060;
+ display: none;
+ max-width: 276px;
+ padding: 1px;
+ text-align: left;
+ background-color: #fff;
+ background-clip: padding-box;
+ border: 1px solid #ccc;
+ border: 1px solid rgba(0, 0, 0, 0.2);
+ border-radius: 6px;
+ box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
+ white-space: normal; }
+ .popover.top {
+ margin-top: -10px; }
+ .popover.right {
+ margin-left: 10px; }
+ .popover.bottom {
+ margin-top: 10px; }
+ .popover.left {
+ margin-left: -10px; }
+
+.popover-title {
+ margin: 0;
+ padding: 8px 14px;
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 18px;
+ background-color: #f7f7f7;
+ border-bottom: 1px solid #ebebeb;
+ border-radius: 5px 5px 0 0; }
+
+.popover-content {
+ padding: 9px 14px; }
+
+.popover > .arrow, .popover > .arrow:after {
+ position: absolute;
+ display: block;
+ width: 0;
+ height: 0;
+ border-color: transparent;
+ border-style: solid; }
+
+.popover > .arrow {
+ border-width: 11px; }
+
+.popover > .arrow:after {
+ border-width: 10px;
+ content: ""; }
+
+.popover.top > .arrow {
+ left: 50%;
+ margin-left: -11px;
+ border-bottom-width: 0;
+ border-top-color: #999999;
+ border-top-color: rgba(0, 0, 0, 0.05);
+ bottom: -11px; }
+ .popover.top > .arrow:after {
+ content: " ";
+ bottom: 1px;
+ margin-left: -10px;
+ border-bottom-width: 0;
+ border-top-color: #fff; }
+.popover.right > .arrow {
+ top: 50%;
+ left: -11px;
+ margin-top: -11px;
+ border-left-width: 0;
+ border-right-color: #999999;
+ border-right-color: rgba(0, 0, 0, 0.05); }
+ .popover.right > .arrow:after {
+ content: " ";
+ left: 1px;
+ bottom: -10px;
+ border-left-width: 0;
+ border-right-color: #fff; }
+.popover.bottom > .arrow {
+ left: 50%;
+ margin-left: -11px;
+ border-top-width: 0;
+ border-bottom-color: #999999;
+ border-bottom-color: rgba(0, 0, 0, 0.05);
+ top: -11px; }
+ .popover.bottom > .arrow:after {
+ content: " ";
+ top: 1px;
+ margin-left: -10px;
+ border-top-width: 0;
+ border-bottom-color: #fff; }
+.popover.left > .arrow {
+ top: 50%;
+ right: -11px;
+ margin-top: -11px;
+ border-right-width: 0;
+ border-left-color: #999999;
+ border-left-color: rgba(0, 0, 0, 0.05); }
+ .popover.left > .arrow:after {
+ content: " ";
+ right: 1px;
+ border-right-width: 0;
+ border-left-color: #fff;
+ bottom: -10px; }
+
+.carousel {
+ position: relative; }
+
+.carousel-inner {
+ position: relative;
+ overflow: hidden;
+ width: 100%; }
+ .carousel-inner > .item {
+ display: none;
+ position: relative;
+ transition: 0.6s ease-in-out left; }
+ .carousel-inner > .item > img, .carousel-inner > .item > a > img {
+ display: block;
+ max-width: 100%;
+ height: auto;
+ line-height: 1; }
+ .carousel-inner > .active, .carousel-inner > .next, .carousel-inner > .prev {
+ display: block; }
+ .carousel-inner > .active {
+ left: 0; }
+ .carousel-inner > .next, .carousel-inner > .prev {
+ position: absolute;
+ top: 0;
+ width: 100%; }
+ .carousel-inner > .next {
+ left: 100%; }
+ .carousel-inner > .prev {
+ left: -100%; }
+ .carousel-inner > .next.left, .carousel-inner > .prev.right {
+ left: 0; }
+ .carousel-inner > .active.left {
+ left: -100%; }
+ .carousel-inner > .active.right {
+ left: 100%; }
+
+.carousel-control {
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 15%;
+ opacity: 0.5;
+ filter: alpha(opacity=50);
+ font-size: 20px;
+ color: #fff;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }
+ .carousel-control.left {
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); }
+ .carousel-control.right {
+ left: auto;
+ right: 0;
+ background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);
+ background-repeat: repeat-x;
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); }
+ .carousel-control:hover, .carousel-control:focus {
+ outline: 0;
+ color: #fff;
+ text-decoration: none;
+ opacity: 0.9;
+ filter: alpha(opacity=90); }
+ .carousel-control .icon-prev, .carousel-control .icon-next, .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right {
+ position: absolute;
+ top: 50%;
+ z-index: 5;
+ display: inline-block; }
+ .carousel-control .icon-prev, .carousel-control .glyphicon-chevron-left {
+ left: 50%;
+ margin-left: -10px; }
+ .carousel-control .icon-next, .carousel-control .glyphicon-chevron-right {
+ right: 50%;
+ margin-right: -10px; }
+ .carousel-control .icon-prev, .carousel-control .icon-next {
+ width: 20px;
+ height: 20px;
+ margin-top: -10px;
+ font-family: serif; }
+ .carousel-control .icon-prev:before {
+ content: '\2039'; }
+ .carousel-control .icon-next:before {
+ content: '\203a'; }
+
+.carousel-indicators {
+ position: absolute;
+ bottom: 10px;
+ left: 50%;
+ z-index: 15;
+ width: 60%;
+ margin-left: -30%;
+ padding-left: 0;
+ list-style: none;
+ text-align: center; }
+ .carousel-indicators li {
+ display: inline-block;
+ width: 10px;
+ height: 10px;
+ margin: 1px;
+ text-indent: -999px;
+ border: 1px solid #fff;
+ border-radius: 10px;
+ cursor: pointer;
+ background-color: #000 \9;
+ background-color: rgba(0, 0, 0, 0); }
+ .carousel-indicators .active {
+ margin: 0;
+ width: 12px;
+ height: 12px;
+ background-color: #fff; }
+
+.carousel-caption {
+ position: absolute;
+ left: 15%;
+ right: 15%;
+ bottom: 20px;
+ z-index: 10;
+ padding-top: 20px;
+ padding-bottom: 20px;
+ color: #fff;
+ text-align: center;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6); }
+ .carousel-caption .btn {
+ text-shadow: none; }
+
+@media screen and (min-width: 768px) {
+ .carousel-control .glyphicon-chevron-left, .carousel-control .glyphicon-chevron-right, .carousel-control .icon-prev, .carousel-control .icon-next {
+ width: 30px;
+ height: 30px;
+ margin-top: -15px;
+ font-size: 30px; }
+ .carousel-control .glyphicon-chevron-left, .carousel-control .icon-prev {
+ margin-left: -15px; }
+ .carousel-control .glyphicon-chevron-right, .carousel-control .icon-next {
+ margin-right: -15px; }
+ .carousel-caption {
+ left: 20%;
+ right: 20%;
+ padding-bottom: 30px; }
+ .carousel-indicators {
+ bottom: 20px; } }
+
+.clearfix:before, .clearfix:after {
+ content: " ";
+ display: table; }
+.clearfix:after {
+ clear: both; }
+
+.center-block {
+ display: block;
+ margin-left: auto;
+ margin-right: auto; }
+
+.pull-right {
+ float: right !important; }
+
+.pull-left {
+ float: left !important; }
+
+.hide {
+ display: none !important; }
+
+.show {
+ display: block !important; }
+
+.invisible {
+ visibility: hidden; }
+
+.text-hide {
+ font: 0/0 a;
+ color: transparent;
+ text-shadow: none;
+ background-color: transparent;
+ border: 0; }
+
+.hidden {
+ display: none !important;
+ visibility: hidden !important; }
+
+.affix {
+ position: fixed; }
+
+@-ms-viewport {
+ width: device-width; }
+
+.visible-xs, .visible-sm, .visible-md, .visible-lg {
+ display: none !important; }
+
+.visible-xs-block, .visible-xs-inline, .visible-xs-inline-block, .visible-sm-block, .visible-sm-inline, .visible-sm-inline-block, .visible-md-block, .visible-md-inline, .visible-md-inline-block, .visible-lg-block, .visible-lg-inline, .visible-lg-inline-block {
+ display: none !important; }
+
+@media (max-width: 767px) {
+ .visible-xs {
+ display: block !important; }
+ table.visible-xs {
+ display: table; }
+ tr.visible-xs {
+ display: table-row !important; }
+ th.visible-xs, td.visible-xs {
+ display: table-cell !important; } }
+
+@media (max-width: 767px) {
+ .visible-xs-block {
+ display: block !important; } }
+
+@media (max-width: 767px) {
+ .visible-xs-inline {
+ display: inline !important; } }
+
+@media (max-width: 767px) {
+ .visible-xs-inline-block {
+ display: inline-block !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm {
+ display: block !important; }
+ table.visible-sm {
+ display: table; }
+ tr.visible-sm {
+ display: table-row !important; }
+ th.visible-sm, td.visible-sm {
+ display: table-cell !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm-block {
+ display: block !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm-inline {
+ display: inline !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .visible-sm-inline-block {
+ display: inline-block !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md {
+ display: block !important; }
+ table.visible-md {
+ display: table; }
+ tr.visible-md {
+ display: table-row !important; }
+ th.visible-md, td.visible-md {
+ display: table-cell !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md-block {
+ display: block !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md-inline {
+ display: inline !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .visible-md-inline-block {
+ display: inline-block !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg {
+ display: block !important; }
+ table.visible-lg {
+ display: table; }
+ tr.visible-lg {
+ display: table-row !important; }
+ th.visible-lg, td.visible-lg {
+ display: table-cell !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg-block {
+ display: block !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg-inline {
+ display: inline !important; } }
+
+@media (min-width: 1200px) {
+ .visible-lg-inline-block {
+ display: inline-block !important; } }
+
+@media (max-width: 767px) {
+ .hidden-xs {
+ display: none !important; } }
+
+@media (min-width: 768px) and (max-width: 991px) {
+ .hidden-sm {
+ display: none !important; } }
+
+@media (min-width: 992px) and (max-width: 1199px) {
+ .hidden-md {
+ display: none !important; } }
+
+@media (min-width: 1200px) {
+ .hidden-lg {
+ display: none !important; } }
+
+.visible-print {
+ display: none !important; }
+
+@media print {
+ .visible-print {
+ display: block !important; }
+ table.visible-print {
+ display: table; }
+ tr.visible-print {
+ display: table-row !important; }
+ th.visible-print, td.visible-print {
+ display: table-cell !important; } }
+
+.visible-print-block {
+ display: none !important; }
+ @media print {
+ .visible-print-block {
+ display: block !important; } }
+
+.visible-print-inline {
+ display: none !important; }
+ @media print {
+ .visible-print-inline {
+ display: inline !important; } }
+
+.visible-print-inline-block {
+ display: none !important; }
+ @media print {
+ .visible-print-inline-block {
+ display: inline-block !important; } }
+
+@media print {
+ .hidden-print {
+ display: none !important; } }
+
+.browsehappy {
+ margin: 0.2em 0;
+ background: #ccc;
+ color: #000;
+ padding: 0.2em 0; }
+
+/* Space out content a bit */
+body {
+ padding-top: 20px;
+ padding-bottom: 20px; }
+
+/* Everything but the jumbotron gets side spacing for mobile first views */
+.header, .marketing, .footer {
+ padding-left: 15px;
+ padding-right: 15px; }
+
+/* Custom page header */
+.header {
+ border-bottom: 1px solid #e5e5e5;
+ /* Make the masthead heading the same height as the navigation */ }
+ .header h3 {
+ margin-top: 0;
+ margin-bottom: 0;
+ line-height: 40px;
+ padding-bottom: 19px; }
+
+/* Custom page footer */
+.footer {
+ padding-top: 19px;
+ color: #777;
+ border-top: 1px solid #e5e5e5; }
+
+.container-narrow > hr {
+ margin: 30px 0; }
+
+/* Main marketing message and sign up button */
+.jumbotron {
+ text-align: center;
+ border-bottom: 1px solid #e5e5e5; }
+ .jumbotron .btn {
+ font-size: 21px;
+ padding: 14px 24px; }
+
+/* Supporting marketing content */
+.marketing {
+ margin: 40px 0; }
+ .marketing p + h4 {
+ margin-top: 28px; }
+
+/* Responsive: Portrait tablets and up */
+@media screen and (min-width: 768px) {
+ /* Remove the padding we set earlier */
+ /* Space out the masthead */
+ /* Remove the bottom border on the jumbotron for visual effect */
+ .container {
+ max-width: 730px; }
+ .header, .marketing, .footer {
+ padding-left: 0;
+ padding-right: 0; }
+ .header {
+ margin-bottom: 30px; }
+ .jumbotron {
+ border-bottom: 300; } }
+
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoibWFpbi5jc3MiLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiIiwic291cmNlcyI6WyJtYWluLnNjc3MiXSwic291cmNlc0NvbnRlbnQiOlsiJGljb24tZm9udC1wYXRoOiBcIi4uL2Jvd2VyX2NvbXBvbmVudHMvYm9vdHN0cmFwLXNhc3Mtb2ZmaWNpYWwvdmVuZG9yL2Fzc2V0cy9mb250cy9ib290c3RyYXAvXCI7XG5cbi8vIGJvd2VyOnNjc3NcbkBpbXBvcnQgXCIuLi9ib3dlcl9jb21wb25lbnRzL2Jvb3RzdHJhcC1zYXNzLW9mZmljaWFsL3ZlbmRvci9hc3NldHMvc3R5bGVzaGVldHMvYm9vdHN0cmFwLnNjc3NcIjtcbi8vIGVuZGJvd2VyXG5cbi5icm93c2VoYXBweSB7XG4gICAgbWFyZ2luOiAwLjJlbSAwO1xuICAgIGJhY2tncm91bmQ6ICNjY2M7XG4gICAgY29sb3I6ICMwMDA7XG4gICAgcGFkZGluZzogMC4yZW0gMDtcbn1cblxuLyogU3BhY2Ugb3V0IGNvbnRlbnQgYSBiaXQgKi9cbmJvZHkge1xuICAgIHBhZGRpbmctdG9wOiAyMHB4O1xuICAgIHBhZGRpbmctYm90dG9tOiAyMHB4O1xufVxuXG4vKiBFdmVyeXRoaW5nIGJ1dCB0aGUganVtYm90cm9uIGdldHMgc2lkZSBzcGFjaW5nIGZvciBtb2JpbGUgZmlyc3Qgdmlld3MgKi9cbi5oZWFkZXIsXG4ubWFya2V0aW5nLFxuLmZvb3RlciB7XG4gICAgcGFkZGluZy1sZWZ0OiAxNXB4O1xuICAgIHBhZGRpbmctcmlnaHQ6IDE1cHg7XG59XG5cbi8qIEN1c3RvbSBwYWdlIGhlYWRlciAqL1xuLmhlYWRlciB7XG4gICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkICNlNWU1ZTU7XG5cbiAgICAvKiBNYWtlIHRoZSBtYXN0aGVhZCBoZWFkaW5nIHRoZSBzYW1lIGhlaWdodCBhcyB0aGUgbmF2aWdhdGlvbiAqL1xuICAgIGgzIHtcbiAgICAgICAgbWFyZ2luLXRvcDogMDtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMDtcbiAgICAgICAgbGluZS1oZWlnaHQ6IDQwcHg7XG4gICAgICAgIHBhZGRpbmctYm90dG9tOiAxOXB4O1xuICAgIH1cbn1cblxuLyogQ3VzdG9tIHBhZ2UgZm9vdGVyICovXG4uZm9vdGVyIHtcbiAgICBwYWRkaW5nLXRvcDogMTlweDtcbiAgICBjb2xvcjogIzc3NztcbiAgICBib3JkZXItdG9wOiAxcHggc29saWQgI2U1ZTVlNTtcbn1cblxuLmNvbnRhaW5lci1uYXJyb3cgPiBociB7XG4gICAgbWFyZ2luOiAzMHB4IDA7XG59XG5cbi8qIE1haW4gbWFya2V0aW5nIG1lc3NhZ2UgYW5kIHNpZ24gdXAgYnV0dG9uICovXG4uanVtYm90cm9uIHtcbiAgICB0ZXh0LWFsaWduOiBjZW50ZXI7XG4gICAgYm9yZGVyLWJvdHRvbTogMXB4IHNvbGlkICNlNWU1ZTU7XG4gICAgLmJ0biB7XG4gICAgICAgIGZvbnQtc2l6ZTogMjFweDtcbiAgICAgICAgcGFkZGluZzogMTRweCAyNHB4O1xuICAgIH1cbn1cblxuLyogU3VwcG9ydGluZyBtYXJrZXRpbmcgY29udGVudCAqL1xuLm1hcmtldGluZyB7XG4gICAgbWFyZ2luOiA0MHB4IDA7XG4gICAgcCArIGg0IHtcbiAgICAgICAgbWFyZ2luLXRvcDogMjhweDtcbiAgICB9XG59XG5cbi8qIFJlc3BvbnNpdmU6IFBvcnRyYWl0IHRhYmxldHMgYW5kIHVwICovXG5AbWVkaWEgc2NyZWVuIGFuZCAobWluLXdpZHRoOiA3NjhweCkge1xuICAgIC5jb250YWluZXIge1xuICAgICAgICBtYXgtd2lkdGg6IDczMHB4O1xuICAgIH1cblxuICAgIC8qIFJlbW92ZSB0aGUgcGFkZGluZyB3ZSBzZXQgZWFybGllciAqL1xuICAgIC5oZWFkZXIsXG4gICAgLm1hcmtldGluZyxcbiAgICAuZm9vdGVyIHtcbiAgICAgICAgcGFkZGluZy1sZWZ0OiAwO1xuICAgICAgICBwYWRkaW5nLXJpZ2h0OiAwO1xuICAgIH1cblxuICAgIC8qIFNwYWNlIG91dCB0aGUgbWFzdGhlYWQgKi9cbiAgICAuaGVhZGVyIHtcbiAgICAgICAgbWFyZ2luLWJvdHRvbTogMzBweDtcbiAgICB9XG5cbiAgICAvKiBSZW1vdmUgdGhlIGJvdHRvbSBib3JkZXIgb24gdGhlIGp1bWJvdHJvbiBmb3IgdmlzdWFsIGVmZmVjdCAqL1xuICAgIC5qdW1ib3Ryb24ge1xuICAgICAgICBib3JkZXItYm90dG9tOiAzMDA7XG4gICAgfVxufVxuXG4vLyB0aGlzIGlzIGEgY29tbWVudC4uLlxuIl0sInNvdXJjZVJvb3QiOiIvc291cmNlLyJ9 */
diff --git a/devtools/client/styleeditor/test/sourcemap-css/test-stylus.css b/devtools/client/styleeditor/test/sourcemap-css/test-stylus.css
new file mode 100644
index 0000000000..0ec51da3b7
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-css/test-stylus.css
@@ -0,0 +1,7 @@
+div {
+ color: #f06;
+}
+span {
+ background-color: #eee;
+}
+/*# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInRlc3Qtc3R5bHVzLnN0eWwiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBRUE7RUFDRSxPQUFPLEtBQVA7O0FBRUY7RUFDRSxrQkFBa0IsS0FBbEIiLCJmaWxlIjoidGVzdC1zdHlsdXMuY3NzIiwic291cmNlc0NvbnRlbnQiOlsicGF1bHJvdWdldHBpbmsgPSAjZjA2O1xuXG5kaXZcbiAgY29sb3I6IHBhdWxyb3VnZXRwaW5rXG5cbnNwYW5cbiAgYmFja2dyb3VuZC1jb2xvcjogI0VFRVxuIl19 */ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss b/devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss
new file mode 100644
index 0000000000..4f1c8f216f
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-sass/media-rules.scss
@@ -0,0 +1,11 @@
+$break-small: 320px;
+$break-large: 1200px;
+
+div {
+ @media screen and (max-width: $break-small) {
+ width: 100px;
+ }
+ @media screen and (min-width: $break-large) {
+ width: 400px;
+ }
+}
diff --git a/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss b/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss
new file mode 100644
index 0000000000..0ff6c471bb
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss
@@ -0,0 +1,10 @@
+
+$paulrougetpink: #f06;
+
+div {
+ color: $paulrougetpink;
+}
+
+span {
+ background-color: #EEE;
+} \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss^headers^ b/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss^headers^
new file mode 100644
index 0000000000..866f3e2fb0
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-sass/sourcemaps.scss^headers^
@@ -0,0 +1,2 @@
+X-Content-Type-Options: nosniff
+Content-Type: text/plain
diff --git a/devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl b/devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl
new file mode 100644
index 0000000000..76ff25c29e
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemap-styl/test-stylus.styl
@@ -0,0 +1,7 @@
+paulrougetpink = #f06;
+
+div
+ color: paulrougetpink
+
+span
+ background-color: #EEE \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sourcemaps-inline.html b/devtools/client/styleeditor/test/sourcemaps-inline.html
new file mode 100644
index 0000000000..45846fe289
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps-inline.html
@@ -0,0 +1,17 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>testcase for testing CSS source maps in inline style</title>
+ <style type="text/css">body {
+ background-color: black;
+}
+body > h1 {
+ color: white;
+}
+/*# sourceMappingURL=data:application/json;base64,ewoidmVyc2lvbiI6IDMsCiJtYXBwaW5ncyI6ICJBQUFBLElBQUs7RUFDSCxnQkFBZ0IsRUFBRSxLQUFLO0VBQ3ZCLFNBQU87SUFDTCxLQUFLLEVBQUUsS0FBSyIsCiJzb3VyY2VzIjogWyJ0ZXN0LnNjc3MiXSwKInNvdXJjZXNDb250ZW50IjogWyJib2R5IHtcbiAgYmFja2dyb3VuZC1jb2xvcjogYmxhY2s7XG4gICYgPiBoMSB7XG4gICAgY29sb3I6IHdoaXRlO1xuICB9XG59XG4iXSwKIm5hbWVzIjogW10sCiJmaWxlIjogInRlc3QuY3NzIgp9Cg== */</style>
+</head>
+<body>
+ <h1>Source maps testcase</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemaps-large.html b/devtools/client/styleeditor/test/sourcemaps-large.html
new file mode 100644
index 0000000000..b8c92e0c9e
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps-large.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="UTF-8">
+ <title>testcase for a loading error with CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/test-bootstrap-scss.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span> (see Bug 1128747)</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemaps-watching.html b/devtools/client/styleeditor/test/sourcemaps-watching.html
new file mode 100644
index 0000000000..fc9909ea57
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps-watching.html
@@ -0,0 +1,11 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/sourcemaps.css?test=1"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sourcemaps.html b/devtools/client/styleeditor/test/sourcemaps.html
new file mode 100644
index 0000000000..887e0ed989
--- /dev/null
+++ b/devtools/client/styleeditor/test/sourcemaps.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+<head>
+ <title>testcase for testing CSS source maps</title>
+ <link rel="stylesheet" type="text/css" href="simple.css"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/sourcemaps.css?test=1"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/contained.css"/>
+ <link rel="stylesheet" type="text/css" href="sourcemap-css/test-stylus.css"/>
+</head>
+<body>
+ <div>source maps <span>testcase</span></div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sync.html b/devtools/client/styleeditor/test/sync.html
new file mode 100644
index 0000000000..83da8c57e9
--- /dev/null
+++ b/devtools/client/styleeditor/test/sync.html
@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>simple testcase</title>
+ <style type="text/css">
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ font-size: 4em;
+ }
+ </style>
+</head>
+<body>
+ <div id="testid">simple testcase</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/sync_with_csp.css b/devtools/client/styleeditor/test/sync_with_csp.css
new file mode 100644
index 0000000000..18f1b89682
--- /dev/null
+++ b/devtools/client/styleeditor/test/sync_with_csp.css
@@ -0,0 +1,10 @@
+
+ body {
+ border-width: 15px;
+ color: red;
+ }
+
+ #testid {
+ font-size: 4em;
+ }
+ \ No newline at end of file
diff --git a/devtools/client/styleeditor/test/sync_with_csp.html b/devtools/client/styleeditor/test/sync_with_csp.html
new file mode 100644
index 0000000000..cdab014f59
--- /dev/null
+++ b/devtools/client/styleeditor/test/sync_with_csp.html
@@ -0,0 +1,12 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <meta http-equiv="Content-Security-Policy" content="style-src *">
+ <title>simple testcase with content security policy</title>
+ <link rel="stylesheet" type="text/css" href="sync_with_csp.css" />
+</head>
+<body>
+ <div id="testid">simple testcase with content security policy</div>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/test_private.css b/devtools/client/styleeditor/test/test_private.css
new file mode 100644
index 0000000000..e8f24f94a3
--- /dev/null
+++ b/devtools/client/styleeditor/test/test_private.css
@@ -0,0 +1,3 @@
+body {
+ background-color: red;
+}
diff --git a/devtools/client/styleeditor/test/test_private.html b/devtools/client/styleeditor/test/test_private.html
new file mode 100644
index 0000000000..bfde3520ef
--- /dev/null
+++ b/devtools/client/styleeditor/test/test_private.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+<link rel="stylesheet" href="test_private.css"></link>
+</head>
+<body>
+</body>
+</html>
diff --git a/devtools/client/styleeditor/test/utf-16.css b/devtools/client/styleeditor/test/utf-16.css
new file mode 100644
index 0000000000..92ff5eac53
--- /dev/null
+++ b/devtools/client/styleeditor/test/utf-16.css
Binary files differ
diff --git a/devtools/client/styleeditor/test/veryveryverylongnamethatcanbreakthestyleeditor.css b/devtools/client/styleeditor/test/veryveryverylongnamethatcanbreakthestyleeditor.css
new file mode 100644
index 0000000000..4d737f305e
--- /dev/null
+++ b/devtools/client/styleeditor/test/veryveryverylongnamethatcanbreakthestyleeditor.css
@@ -0,0 +1,7 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* ☺ */
+
+body {
+ margin: 0;
+}