/* 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/. */
const HTML_NS = "http://www.w3.org/1999/xhtml"; const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles"; const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit"; const PREF_DRAGGABLE = "devtools.inspector.draggable_properties"; const PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER = "devtools.inspector.rule-view.focusNextOnEnter"; const FILTER_CHANGED_TIMEOUT = 150; // Removes the flash-out class from an element after 1 second. const PROPERTY_FLASHING_DURATION = 1000;
// This is used to parse user input when filtering. const FILTER_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*;?$/; // This is used to parse the filter search value to see if the filter // should be strict or not const FILTER_STRICT_RE = /\s*`(.*?)`\s*$/;
/** * Our model looks like this: * * ElementStyle: * Responsible for keeping track of which properties are overridden. * Maintains a list of Rule objects that apply to the element. * Rule: * Manages a single style declaration or rule. * Responsible for applying changes to the properties in a rule. * Maintains a list of TextProperty objects. * TextProperty: * Manages a single property from the authoredText attribute of the * relevant declaration. * Maintains a list of computed properties that come from this * property declaration. * Changes to the TextProperty are sent to its related Rule for * application. * * View hierarchy mostly follows the model hierarchy. * * CssRuleView: * Owns an ElementStyle and creates a list of RuleEditors for its * Rules. * RuleEditor: * Owns a Rule object and creates a list of TextPropertyEditors * for its TextProperties. * Manages creation of new text properties. * TextPropertyEditor: * Owns a TextProperty object. * Manages changes to the TextProperty. * Can be expanded to display computed properties. * Can mark a property disabled or enabled.
*/
/** * CssRuleView is a view of the style rules and declarations that * apply to a given element. After construction, the 'element' * property will be available with the user interface. * * @param {Inspector} inspector * Inspector toolbox panel * @param {Document} document * The document that will contain the rule view. * @param {Object} store * The CSS rule view can use this object to store metadata * that might outlast the rule view, particularly the current * set of disabled properties.
*/ function CssRuleView(inspector, document, store) {
EventEmitter.decorate(this);
// Allow tests to override debouncing behavior, as this can cause intermittents. this.debounce = debounce;
// Variable used to stop the propagation of mouse events to children // when we are updating a value by dragging the mouse and we then release it this.childHasDragged = false;
this._outputParser = new OutputParser(document, this.cssProperties); this._abortController = newthis.styleWindow.AbortController();
const doc = this.styleDocument; // Delegate bulk handling of events happening within the DOM tree of the Rules view // to this.handleEvent(). Listening on the capture phase of the event bubbling to be // able to stop event propagation on a case-by-case basis and prevent event target // ancestor nodes from handling them. this.styleDocument.addEventListener("click", this, { capture: true }); this.element = doc.getElementById("ruleview-container-focusable"); this.addRuleButton = doc.getElementById("ruleview-add-rule-button"); this.searchField = doc.getElementById("ruleview-searchbox"); this.searchClearButton = doc.getElementById("ruleview-searchinput-clear"); this.pseudoClassPanel = doc.getElementById("pseudo-class-panel"); this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle"); this.classPanel = doc.getElementById("ruleview-class-panel"); this.classToggle = doc.getElementById("class-panel-toggle"); this.colorSchemeLightSimulationButton = doc.getElementById( "color-scheme-simulation-light-toggle"
); this.colorSchemeDarkSimulationButton = doc.getElementById( "color-scheme-simulation-dark-toggle"
); this.printSimulationButton = doc.getElementById("print-simulation-toggle");
this._initSimulationFeatures();
this.searchClearButton.hidden = true;
this.onHighlighterShown = data => this.handleHighlighterEvent("highlighter-shown", data); this.onHighlighterHidden = data => this.handleHighlighterEvent("highlighter-hidden", data); this.inspector.highlighters.on("highlighter-shown", this.onHighlighterShown); this.inspector.highlighters.on( "highlighter-hidden", this.onHighlighterHidden
);
if (flags.testing) { // In tests, we start listening immediately to avoid having to simulate a mousemove. this.highlighters.addToView(this);
} else { this.element.addEventListener( "mousemove",
() => { this.highlighters.addToView(this);
},
{ once: true }
);
}
this._prefObserver = new PrefObserver("devtools."); this._prefObserver.on(PREF_UA_STYLES, this._handleUAStylePrefChange); this._prefObserver.on(
PREF_DEFAULT_COLOR_UNIT, this._handleDefaultColorUnitPrefChange
); this._prefObserver.on(PREF_DRAGGABLE, this._handleDraggablePrefChange); // Initialize value of this.draggablePropertiesEnabled this._handleDraggablePrefChange();
this._prefObserver.on(
PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER, this._handleInplaceEditorFocusNextOnEnterPrefChange
); // Initialize value of this.inplaceEditorFocusNextOnEnter this._handleInplaceEditorFocusNextOnEnterPrefChange();
// Add the tooltips and highlighters to the view this.tooltips = new TooltipsOverlay(this);
this.cssRegisteredPropertiesByTarget = new Map();
}
CssRuleView.prototype = { // The element that we're inspecting.
_viewedElement: null,
// Used for cancelling timeouts in the style filter.
_filterChangedTimeout: null,
// Empty, unconnected element of the same type as this node, used // to figure out how shorthand properties will be parsed.
_dummyElement: null,
get popup() { if (!this._popup) { // The popup will be attached to the toolbox document. this._popup = new AutocompletePopup(this.inspector.toolbox.doc, {
autoSelect: true,
});
}
returnthis._popup;
},
get classListPreviewer() { if (!this._classListPreviewer) { this._classListPreviewer = new ClassListPreviewer( this.inspector, this.classPanel
);
}
returnthis._classListPreviewer;
},
get contextMenu() { if (!this._contextMenu) { this._contextMenu = new StyleInspectorMenu(this, { isRuleView: true });
}
returnthis._contextMenu;
},
// Get the dummy elemenet.
get dummyElement() { returnthis._dummyElement;
},
// Get the highlighters overlay from the Inspector.
get highlighters() { if (!this._highlighters) { // highlighters is a lazy getter in the inspector. this._highlighters = this.inspector.highlighters;
}
returnthis._highlighters;
},
// Get the filter search value.
get searchValue() { returnthis.searchField.value.toLowerCase();
},
get rules() { returnthis._elementStyle ? this._elementStyle.rules : [];
},
get currentTarget() { returnthis.inspector.toolbox.target;
},
/** * Highlight/unhighlight all the nodes that match a given rule's selector * inside the document of the current selected node. * Only one selector can be highlighted at a time, so calling the method a * second time with a different rule will first unhighlight the previously * highlighted nodes. * Calling the method a second time with the same rule will just * unhighlight the highlighted nodes. * * @param {Rule} rule * @param {String} selector * Elements matching this selector will be highlighted on the page. * @param {Boolean} highlightFromRulesSelector
*/
async toggleSelectorHighlighter(
rule,
selector,
highlightFromRulesSelector = true
) { if (this.isSelectorHighlighted(selector)) {
await this.inspector.highlighters.hideHighlighterType( this.inspector.highlighters.TYPES.SELECTOR
);
} else { const options = {
hideInfoBar: true,
hideGuides: true, // we still pass the selector (which can be the StyleRuleFront#computedSelector) // even if highlightFromRulesSelector is set to true, as it's how we keep track // of which selector is highlighted.
selector,
}; if (highlightFromRulesSelector) {
options.ruleActorID = rule.domRule.actorID;
}
await this.inspector.highlighters.showHighlighterTypeForNode( this.inspector.highlighters.TYPES.SELECTOR, this.inspector.selection.nodeFront,
options
);
}
},
/** * Check whether a SelectorHighlighter is active for the given selector text. * * @param {String} selector * @return {Boolean}
*/
isSelectorHighlighted(selector) { const options = this.inspector.highlighters.getOptionsForActiveHighlighter( this.inspector.highlighters.TYPES.SELECTOR
);
return options?.selector === selector;
},
/** * Delegate handler for events happening within the DOM tree of the Rules view. * Itself delegates to specific handlers by event type. * * Use this instead of attaching specific event handlers when: * - there are many elements with the same event handler (eases memory pressure) * - you want to avoid having to remove event handlers manually * - elements are added/removed from the DOM tree arbitrarily over time * * @param {MouseEvent|UIEvent} event
*/
handleEvent(event) { if (this.childHasDragged) { this.childHasDragged = false;
event.stopPropagation(); return;
} switch (event.type) { case"click": this.handleClickEvent(event); break; default:
}
},
/** * Delegate handler for click events happening within the DOM tree of the Rules view. * Stop propagation of click event wrapping a CSS rule or CSS declaration to avoid * triggering the prompt to add a new CSS declaration or to edit the existing one. * * @param {MouseEvent} event
*/
async handleClickEvent(event) { const target = event.target;
// Handle click on the icon next to a CSS selector. if (target.classList.contains("js-toggle-selector-highlighter")) {
event.stopPropagation();
let selector = target.dataset.computedSelector; const highlightFromRulesSelector =
!!selector && !target.dataset.isUniqueSelector; // dataset.computedSelector will be initially empty for inline styles (inherited or not) // Rules associated with a regular selector should have this data-attribute // set in devtools/client/inspector/rules/views/rule-editor.js const rule = getRuleFromNode(target, this._elementStyle); if (selector === "") { try { if (rule.inherited) { // This is an inline style from an inherited rule. Need to resolve the // unique selector from the node which this rule is inherited from.
selector = await rule.inherited.getUniqueSelector();
} else { // This is an inline style from the current node.
selector =
await this.inspector.selection.nodeFront.getUniqueSelector();
}
// Now that the selector was computed, we can store it for subsequent usage.
target.dataset.computedSelector = selector;
target.dataset.isUniqueSelector = true;
} finally { // Could not resolve a unique selector for the inline style.
}
}
// Handle click on swatches next to flex and inline-flex CSS properties if (target.classList.contains("js-toggle-flexbox-highlighter")) {
event.stopPropagation(); this.inspector.highlighters.toggleFlexboxHighlighter( this.inspector.selection.nodeFront, "rule"
);
}
// Handle click on swatches next to grid CSS properties if (target.classList.contains("js-toggle-grid-highlighter")) {
event.stopPropagation(); this.inspector.highlighters.toggleGridHighlighter( this.inspector.selection.nodeFront, "rule"
);
}
},
/** * Delegate handler for highlighter events. * * This is the place to observe for highlighter events, check the highlighter type and * event name, then react to specific events, for example by modifying the DOM. * * @param {String} eventName * Highlighter event name. One of: "highlighter-hidden", "highlighter-shown" * @param {Object} data * Object with data associated with the highlighter event.
*/
handleHighlighterEvent(eventName, data) { switch (data.type) { // Toggle the "highlighted" class on selector icons in the Rules view when // the SelectorHighlighter is shown/hidden for a certain CSS selector. casethis.inspector.highlighters.TYPES.SELECTOR:
{ const selector = data?.options?.selector; if (!selector) { return;
}
// Toggle the "aria-pressed" attribute on swatches next to flex and inline-flex CSS properties // when the FlexboxHighlighter is shown/hidden for the currently selected node. casethis.inspector.highlighters.TYPES.FLEXBOX:
{ const query = ".js-toggle-flexbox-highlighter"; for (const node of this.styleDocument.querySelectorAll(query)) {
node.setAttribute("aria-pressed", eventName == "highlighter-shown");
}
} break;
// Toggle the "aria-pressed" class on swatches next to grid CSS properties // when the GridHighlighter is shown/hidden for the currently selected node. casethis.inspector.highlighters.TYPES.GRID:
{ const query = ".js-toggle-grid-highlighter"; for (const node of this.styleDocument.querySelectorAll(query)) { // From the Layout panel, we can toggle grid highlighters for nodes which are // not currently selected. The Rules view shows `display: grid` declarations // only for the selected node. Avoid mistakenly marking them as "active". if (data.nodeFront === this.inspector.selection.nodeFront) {
node.setAttribute( "aria-pressed",
eventName == "highlighter-shown"
);
}
// When the max limit of grid highlighters is reached (default 3), // mark inactive grid swatches as disabled.
node.toggleAttribute( "disabled",
!this.inspector.highlighters.canGridHighlighterToggle( this.inspector.selection.nodeFront
)
);
}
} break;
}
},
/** * Enables the print and color scheme simulation only for local and remote tab debugging.
*/
async _initSimulationFeatures() { if (!this.inspector.commands.descriptorFront.isTabDescriptor) { return;
} this.colorSchemeLightSimulationButton.removeAttribute("hidden"); this.colorSchemeDarkSimulationButton.removeAttribute("hidden"); this.printSimulationButton.removeAttribute("hidden"); this.printSimulationButton.addEventListener( "click", this._onTogglePrintSimulation
); this.colorSchemeLightSimulationButton.addEventListener( "click", this._onToggleLightColorSchemeSimulation
); this.colorSchemeDarkSimulationButton.addEventListener( "click", this._onToggleDarkColorSchemeSimulation
); const { rfpCSSColorScheme } = this.inspector.walker; if (rfpCSSColorScheme) { this.colorSchemeLightSimulationButton.setAttribute("disabled", true); this.colorSchemeDarkSimulationButton.setAttribute("disabled", true);
console.warn("Color scheme simulation is disabled in RFP mode.");
}
},
/** * Get the type of a given node in the rule-view * * @param {DOMNode} node * The node which we want information about * @return {Object|null} containing the following props: * - type {String} One of the VIEW_NODE_XXX_TYPE const in * client/inspector/shared/node-types. * - rule {Rule} The Rule object. * - value {Object} Depends on the type of the node. * Otherwise, returns null if the node isn't anything we care about.
*/
getNodeInfo(node) { return getNodeInfo(node, this._elementStyle);
},
/** * Get the node's compatibility issues * * @param {DOMNode} node * The node which we want information about * @return {Object|null} containing the following props: * - type {String} Compatibility issue type. * - property {string} The incompatible rule * - alias {Array} The browser specific alias of rule * - url {string} Link to MDN documentation * - deprecated {bool} True if the rule is deprecated * - experimental {bool} True if rule is experimental * - unsupportedBrowsers {Array} Array of unsupported browser * Otherwise, returns null if the node has cross-browser compatible CSS
*/
async getNodeCompatibilityInfo(node) { const compatibilityInfo = await getNodeCompatibilityInfo(
node, this._elementStyle
);
return compatibilityInfo;
},
/** * Context menu handler.
*/
_onContextMenu(event) { if (
event.originalTarget.closest("input[type=text]") ||
event.originalTarget.closest("input:not([type])") ||
event.originalTarget.closest("textarea")
) { return;
}
event.stopPropagation();
event.preventDefault();
this.contextMenu.show(event);
},
/** * Callback for copy event. Copy the selected text. * * @param {Event} event * copy event object.
*/
_onCopy(event) { if (event) { this.copySelection(event.target);
event.preventDefault();
event.stopPropagation();
}
},
/** * Copy the current selection. The current target is necessary * if the selection is inside an input or a textarea * * @param {DOMNode} target * DOMNode target of the copy action
*/
copySelection(target) { try {
let text = "";
if ( // The target can be the enable/disable rule checkbox here (See Bug 1680893).
(nodeName === "input" && targetType !== "checkbox") ||
nodeName == "textarea"
) { const start = Math.min(target.selectionStart, target.selectionEnd); const end = Math.max(target.selectionStart, target.selectionEnd); const count = end - start;
text = target.value.substr(start, count);
} else {
text = this.styleWindow.getSelection().toString();
// Remove any double newlines.
text = text.replace(/(\r?\n)\r?\n/g, "$1");
}
/** * Add a new rule to the current element.
*/
async _onAddRule() { const elementStyle = this._elementStyle; const element = elementStyle.element; const pseudoClasses = element.pseudoClassLocks;
/** * Return {Boolean} true if the rule view currently has an input * editor visible.
*/
get isEditing() { return ( this.tooltips.isEditing ||
!!this.element.querySelectorAll(".styleinspector-propertyeditor").length
);
},
_handleDraggablePrefChange() { this.draggablePropertiesEnabled = Services.prefs.getBoolPref(
PREF_DRAGGABLE, false
); // This event is consumed by text-property-editor instances in order to // update their draggable behavior. Preferences observer are costly, so // we are forwarding the preference update via the EventEmitter. this.emit("draggable-preference-updated");
},
_handlePrefChange(pref) { // Reselect the currently selected element const refreshOnPrefs = [
PREF_UA_STYLES,
PREF_DEFAULT_COLOR_UNIT,
PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER,
]; if (this._viewedElement && refreshOnPrefs.includes(pref)) { this.selectElement(this._viewedElement, true);
}
},
/** * Set the filter style search value. * @param {String} value * The search value.
*/
setFilterStyles(value = "") { this.searchField.value = value; this.searchField.focus(); this._onFilterStyles();
},
/** * Called when the user enters a search term in the filter style search box.
*/
_onFilterStyles() { if (this._filterChangedTimeout) {
clearTimeout(this._filterChangedTimeout);
}
if (this.searchData.searchPropertyMatch) { // Parse search value as a single property line and extract the // property name and value. If the parsed property name or value is // contained in backquotes (`), extract the value within the backquotes // and set the corresponding strict search for the property to true. if (FILTER_STRICT_RE.test(this.searchData.searchPropertyMatch[1])) { this.searchData.strictSearchPropertyName = true; this.searchData.searchPropertyName = FILTER_STRICT_RE.exec( this.searchData.searchPropertyMatch[1]
)[1];
} else { this.searchData.searchPropertyName = this.searchData.searchPropertyMatch[1];
}
// Strict search for stylesheets will match the property line regex. // Extract the search value within the backquotes to be used // in the strict search for stylesheets in _highlightStyleSheet. if (FILTER_STRICT_RE.test(this.searchValue)) { this.searchData.strictSearchValue = FILTER_STRICT_RE.exec( this.searchValue
)[1];
}
} elseif (FILTER_STRICT_RE.test(this.searchValue)) { // If the search value does not correspond to a property line and // is contained in backquotes, extract the search value within the // backquotes and set the flag to perform a strict search for all // the values (selector, stylesheet, property and computed values). const searchValue = FILTER_STRICT_RE.exec(this.searchValue)[1]; this.searchData.strictSearchAllValues = true; this.searchData.searchPropertyName = searchValue; this.searchData.searchPropertyValue = searchValue; this.searchData.strictSearchValue = searchValue;
}
/** * Called when the user clicks on the clear button in the filter style search * box. Returns true if the search box is cleared and false otherwise.
*/
_onClearSearch() { if (this.searchField.value) { this.setFilterStyles(""); returntrue;
}
this._dummyElement = null; // off handlers must have the same reference as their on handlers this._prefObserver.off(PREF_UA_STYLES, this._handleUAStylePrefChange); this._prefObserver.off(
PREF_DEFAULT_COLOR_UNIT, this._handleDefaultColorUnitPrefChange
); this._prefObserver.off(PREF_DRAGGABLE, this._handleDraggablePrefChange); this._prefObserver.off(
PREF_INPLACE_EDITOR_FOCUS_NEXT_ON_ENTER, this._handleInplaceEditorFocusNextOnEnterPrefChange
); this._prefObserver.destroy();
this._outputParser = null;
if (this._classListPreviewer) { this._classListPreviewer.destroy(); this._classListPreviewer = null;
}
if (this._contextMenu) { this._contextMenu.destroy(); this._contextMenu = null;
}
if (this._highlighters) { this._highlighters.removeFromView(this); this._highlighters = null;
}
if (this.element.parentNode) { this.element.remove();
}
if (this._elementStyle) { this._elementStyle.destroy();
}
if (this._popup) { this._popup.destroy(); this._popup = null;
}
},
/** * Mark the view as selecting an element, disabling all interaction, and * visually clearing the view after a few milliseconds to avoid confusion * about which element's styles the rule view shows.
*/
_startSelectingElement() { this.element.classList.add("non-interactive");
},
/** * Mark the view as no longer selecting an element, re-enabling interaction.
*/
_stopSelectingElement() { this.element.classList.remove("non-interactive");
},
/** * Update the view with a new selected element. * * @param {NodeActor} element * The node whose style rules we'll inspect. * @param {Boolean} allowRefresh * Update the view even if the element is the same as last time.
*/
selectElement(element, allowRefresh = false) { const refresh = this._viewedElement === element; if (refresh && !allowRefresh) { return Promise.resolve(undefined);
}
if (this._popup && this.popup.isOpen) { this.popup.hidePopup();
}
// To figure out how shorthand properties are interpreted by the // engine, we will set properties on a dummy element and observe // how their .style attribute reflects them as computed values. const dummyElementPromise = Promise.resolve(this.styleDocument)
.then(document => { // ::before and ::after do not have a namespaceURI const namespaceURI = this.element.namespaceURI || document.documentElement.namespaceURI; this._dummyElement = document.createElementNS(
namespaceURI, this.element.tagName
);
})
.catch(promiseWarn);
/** * Update the rules for the currently highlighted element.
*/
refreshPanel() { // Ignore refreshes when the panel is hidden, or during editing or when no element is selected. if (!this.isPanelVisible() || this.isEditing || !this._elementStyle) { return Promise.resolve(undefined);
}
// Repopulate the element style once the current modifications are done. const promises = []; for (const rule of this._elementStyle.rules) { if (rule._applyingModifications) {
promises.push(rule._applyingModifications);
}
}
/** * Clear the pseudo class options panel by removing the checked and disabled * attributes for each checkbox.
*/
clearPseudoClassPanel() { this.pseudoClassCheckboxes.forEach(checkbox => {
checkbox.checked = false;
checkbox.disabled = false;
});
},
/** * For each item in PSEUDO_CLASSES, create a checkbox input element for toggling a * pseudo-class on the selected element and append it to the pseudo-class panel. * * Returns an array with the checkbox input elements for pseudo-classes. * * @return {Array}
*/
_createPseudoClassCheckboxes() { const doc = this.styleDocument; const fragment = doc.createDocumentFragment();
for (const pseudo of PSEUDO_CLASSES) { const label = doc.createElement("label"); const checkbox = doc.createElement("input");
checkbox.setAttribute("tabindex", "-1");
checkbox.setAttribute("type", "checkbox");
checkbox.setAttribute("value", pseudo);
/** * Update the pseudo class options for the currently highlighted element.
*/
refreshPseudoClassPanel() { if (!this._elementStyle || !this.inspector.selection.isElementNode()) { this.pseudoClassCheckboxes.forEach(checkbox => {
checkbox.disabled = true;
}); return;
}
// Notify anyone that cares that we refreshed. return onEditorsReady.then(() => { this.emit("ruleview-refreshed");
}, console.error);
})
.catch(promiseWarn);
},
/** * Show the user that the rule view has no node selected.
*/
_showEmpty() { if (this.styleDocument.getElementById("ruleview-no-results")) { return;
}
/** * Clear the rule view.
*/
clear(clearDom = true) { if (clearDom) { this._clearRules();
} this._viewedElement = null;
if (this._elementStyle) { this._elementStyle.destroy(); this._elementStyle = null;
}
if (this.pageStyle) { this.pageStyle.off("stylesheet-updated", this.refreshPanel); this.pageStyle = null;
}
},
/** * Called when the user has made changes to the ElementStyle. * Emits an event that clients can listen to.
*/
_changed() { this.emit("ruleview-changed");
},
/** * Text for header that shows above rules for this element
*/
get selectedElementLabel() { if (this._selectedElementLabel) { returnthis._selectedElementLabel;
} this._selectedElementLabel = l10n("rule.selectedElement"); returnthis._selectedElementLabel;
},
/** * Text for header that shows above rules for pseudo elements
*/
get pseudoElementLabel() { if (this._pseudoElementLabel) { returnthis._pseudoElementLabel;
} this._pseudoElementLabel = l10n("rule.pseudoElement"); returnthis._pseudoElementLabel;
},
get showPseudoElements() { if (this._showPseudoElements === undefined) { this._showPseudoElements = Services.prefs.getBoolPref( "devtools.inspector.show_pseudo_elements"
);
} returnthis._showPseudoElements;
},
/** * Creates an expandable container in the rule view * * @param {String} label * The label for the container header * @param {String} containerId * The id that will be set on the container * @param {Boolean} isPseudo * Whether or not the container will hold pseudo element rules * @return {DOMNode} The container element
*/
createExpandableContainer(label, containerId, isPseudo = false) { const header = this.styleDocument.createElementNS(HTML_NS, "div");
header.classList.add(
RULE_VIEW_HEADER_CLASSNAME, "ruleview-expandable-header"
);
header.setAttribute("role", "heading");
/** * Return the RegisteredPropertyEditor element for a given property name * * @param {String} registeredPropertyName * @returns {Element|null}
*/
getRegisteredPropertyElement(registeredPropertyName) { returnthis.styleDocument.querySelector(
`#${REGISTERED_PROPERTIES_CONTAINER_ID} [data-name="${registeredPropertyName}"]`
);
},
/** * Toggle the visibility of an expandable container * * @param {DOMNode} twisty * Clickable toggle DOM Node * @param {DOMNode} container * Expandable container DOM Node * @param {Boolean} isPseudo * Whether or not the container will hold pseudo element rules * @param {Boolean} showPseudo * Whether or not pseudo element rules should be displayed
*/
_toggleContainerVisibility(toggleButton, container, isPseudo, showPseudo) {
let isOpen = toggleButton.getAttribute("aria-expanded") === "true";
if (isPseudo) { this._showPseudoElements = !!showPseudo;
/** * Creates editor UI for each of the rules in _elementStyle.
*/ // eslint-disable-next-line complexity
_createEditors() { // Run through the current list of rules, attaching // their editors in order. Create editors if needed.
let lastInheritedSource = "";
let lastKeyframes = null;
let seenPseudoElement = false;
let seenNormalElement = false;
let seenSearchTerm = false;
let container = null;
if (!this._elementStyle.rules) { return Promise.resolve();
}
const editorReadyPromises = []; for (const rule of this._elementStyle.rules) { if (rule.domRule.system) { continue;
}
// Initialize rule editor if (!rule.editor) {
rule.editor = new RuleEditor(this, rule);
editorReadyPromises.push(rule.editor.once("source-link-updated"));
}
// Filter the rules and highlight any matches if there is a search input if (this.searchValue && this.searchData) { if (this.highlightRule(rule)) {
seenSearchTerm = true;
} elseif (rule.domRule.type !== ELEMENT_STYLE) { continue;
}
}
// Only print header for this element if there are pseudo elements if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
seenNormalElement = true; const div = this.styleDocument.createElementNS(HTML_NS, "div");
div.className = RULE_VIEW_HEADER_CLASSNAME;
div.setAttribute("role", "heading");
div.textContent = this.selectedElementLabel; this.element.appendChild(div);
}
// Automatically select the selector input when we are adding a user-added rule if (this._focusNextUserAddedRule && rule.domRule.userAdded) { this._focusNextUserAddedRule = null;
rule.editor.selectorText.click(); this.emitForTests("new-rule-added", rule);
}
}
// Sort properties by their name, as we want to display them in alphabetical order const propertyDefinitions = Array.from(
targetRegisteredProperties.values()
).sort((a, b) => (a.name < b.name ? -1 : 1)); for (const propertyDefinition of propertyDefinitions) { const registeredPropertyEditor = new RegisteredPropertyEditor( this,
propertyDefinition
);
/** * Highlight rules that matches the filter search value and returns a * boolean indicating whether or not rules were highlighted. * * @param {Rule} rule * The rule object we're highlighting if its rule selectors or * property values match the search value. * @return {Boolean} true if the rule was highlighted, false otherwise.
*/
highlightRule(rule) { const isRuleSelectorHighlighted = this._highlightRuleSelector(rule); const isStyleSheetHighlighted = this._highlightStyleSheet(rule); const isAncestorRulesHighlighted = this._highlightAncestorRules(rule);
let isHighlighted =
isRuleSelectorHighlighted ||
isStyleSheetHighlighted ||
isAncestorRulesHighlighted;
// Highlight search matches in the rule properties for (const textProp of rule.textProps) { if (!textProp.invisible && this._highlightProperty(textProp.editor)) {
isHighlighted = true;
}
}
return isHighlighted;
},
/** * Highlights the rule selector that matches the filter search value and * returns a boolean indicating whether or not the selector was highlighted. * * @param {Rule} rule * The Rule object. * @return {Boolean} true if the rule selector was highlighted, * false otherwise.
*/
_highlightRuleSelector(rule) {
let isSelectorHighlighted = false;
// Highlight search matches in the rule selectors for (const selectorNode of selectorNodes) { const selector = selectorNode.textContent.toLowerCase(); if (
(this.searchData.strictSearchAllValues &&
selector === this.searchData.strictSearchValue) ||
(!this.searchData.strictSearchAllValues &&
selector.includes(this.searchValue))
) {
selectorNode.classList.add("ruleview-highlight");
isSelectorHighlighted = true;
}
}
return isSelectorHighlighted;
},
/** * Highlights the ancestor rules data (@media / @layer) that matches the filter search * value and returns a boolean indicating whether or not element was highlighted. * * @return {Boolean} true if the element was highlighted, false otherwise.
*/
_highlightAncestorRules(rule) { const element = rule.editor.ancestorDataEl; if (!element) { returnfalse;
}
let isHighlighted = false; for (const child of ancestorSelectors) { const dataText = child.innerText.toLowerCase(); const matches = this.searchData.strictSearchValue
? dataText === this.searchData.strictSearchValue
: dataText.includes(this.searchValue); if (matches) {
isHighlighted = true;
child.classList.add("ruleview-highlight");
}
}
return isHighlighted;
},
/** * Highlights the stylesheet source that matches the filter search value and * returns a boolean indicating whether or not the stylesheet source was * highlighted. * * @return {Boolean} true if the stylesheet source was highlighted, false * otherwise.
*/
_highlightStyleSheet(rule) { const styleSheetSource = rule.title.toLowerCase(); const isStyleSheetHighlighted = this.searchData.strictSearchValue
? styleSheetSource === this.searchData.strictSearchValue
: styleSheetSource.includes(this.searchValue);
if (isStyleSheetHighlighted) {
rule.editor.source.classList.add("ruleview-highlight");
}
return isStyleSheetHighlighted;
},
/** * Highlights the rule properties and computed properties that match the * filter search value and returns a boolean indicating whether or not the * property or computed property was highlighted. * * @param {TextPropertyEditor} editor * The rule property TextPropertyEditor object. * @return {Boolean} true if the property or computed property was * highlighted, false otherwise.
*/
_highlightProperty(editor) { const isPropertyHighlighted = this._highlightRuleProperty(editor); const isComputedHighlighted = this._highlightComputedProperty(editor);
// Expand the computed list if a computed property is highlighted and the // property rule is not highlighted if (
!isPropertyHighlighted &&
isComputedHighlighted &&
!editor.computed.hasAttribute("user-open")
) {
editor.expandForFilter();
}
/** * Called when TextPropertyEditor is updated and updates the rule property * highlight. * * @param {TextPropertyEditor} editor * The rule property TextPropertyEditor object.
*/
_updatePropertyHighlight(editor) { if (!this.searchValue || !this.searchData) { return;
}
this._clearHighlight(editor.element);
if (this._highlightProperty(editor)) { this.searchField.classList.remove("devtools-style-searchbox-no-match");
}
},
/** * Highlights the rule property that matches the filter search value * and returns a boolean indicating whether or not the property was * highlighted. * * @param {TextPropertyEditor} editor * The rule property TextPropertyEditor object. * @return {Boolean} true if the rule property was highlighted, * false otherwise.
*/
_highlightRuleProperty(editor) { // Get the actual property value displayed in the rule view const propertyName = editor.prop.name.toLowerCase(); const propertyValue = editor.valueSpan.textContent.toLowerCase();
/** * Highlights the computed property that matches the filter search value and * returns a boolean indicating whether or not the computed property was * highlighted. * * @param {TextPropertyEditor} editor * The rule property TextPropertyEditor object. * @return {Boolean} true if the computed property was highlighted, false * otherwise.
*/
_highlightComputedProperty(editor) {
let isComputedHighlighted = false;
// Highlight search matches in the computed list of properties
editor._populateComputed(); for (const computed of editor.prop.computed) { if (computed.element) { // Get the actual property value displayed in the computed list const computedName = computed.name.toLowerCase(); const computedValue = computed.parsedValue.toLowerCase();
/** * Helper function for highlightRules that carries out highlighting the given * element if the search terms match the property, and returns a boolean * indicating whether or not the search terms match. * * @param {DOMNode} element * The node to highlight if search terms match * @param {String} propertyName * The property name of a rule * @param {String} propertyValue * The property value of a rule * @return {Boolean} true if the given search terms match the property, false * otherwise.
*/
_highlightMatches(element, propertyName, propertyValue) { const {
searchPropertyName,
searchPropertyValue,
searchPropertyMatch,
strictSearchPropertyName,
strictSearchPropertyValue,
strictSearchAllValues,
} = this.searchData;
let matches = false;
// If the inputted search value matches a property line like // `font-family: arial`, then check to make sure the name and value match. // Otherwise, just compare the inputted search string directly against the // name and value of the rule property. const hasNameAndValue =
searchPropertyMatch && searchPropertyName && searchPropertyValue; const isMatch = (value, query, isStrict) => { return isStrict ? value === query : query && value.includes(query);
};
if (matches) {
element.classList.add("ruleview-highlight");
}
return matches;
},
/** * Clear all search filter highlights in the panel, and close the computed * list if toggled opened
*/
_clearHighlight(element) { for (const el of element.querySelectorAll(".ruleview-highlight")) {
el.classList.remove("ruleview-highlight");
}
for (const computed of element.querySelectorAll( ".ruleview-computedlist[filter-open]"
)) {
computed.parentNode._textPropertyEditor.collapseForFilter();
}
},
/** * Called when the pseudo class panel button is clicked and toggles * the display of the pseudo class panel.
*/
_onTogglePseudoClassPanel() { if (this.pseudoClassPanel.hidden) { this.showPseudoClassPanel();
} else { this.hidePseudoClassPanel();
}
},
/** * Called when a pseudo class checkbox is clicked and toggles * the pseudo class for the current selected element.
*/
_onTogglePseudoClass(event) { const target = event.target; this.inspector.togglePseudoClass(target.value);
},
/** * Called when the class panel button is clicked and toggles the display of the class * panel.
*/
_onToggleClassPanel() { if (this.classPanel.hidden) { this.showClassPanel();
} else { this.hideClassPanel();
}
},
// Emit "scrolled-to-property" for use by tests. this.emit("scrolled-to-element");
}, PROPERTY_FLASHING_DURATION);
},
/** * Scrolls to the top of either the rule or declaration. The view will try to scroll to * the rule if both can fit in the viewport. If not, then scroll to the declaration. * * @param {Element} rule * The rule to scroll to. * @param {Element|null} declaration * Optional. The declaration to scroll to. * @param {String} scrollBehavior * Optional. The transition animation when scrolling. If prefers-reduced-motion * system pref is set, then the scroll behavior will be overridden to "auto".
*/
_scrollToElement(rule, declaration, scrollBehavior = "smooth") {
let elementToScrollTo = rule;
if (declaration) { const { offsetTop, offsetHeight } = declaration; // Get the distance between both the rule and declaration. If the distance is // greater than the height of the rule view, then only scroll to the declaration. const distance = offsetTop + offsetHeight - rule.offsetTop;
if (this.element.parentNode.offsetHeight <= distance) {
--> --------------------
--> maximum size reached
--> --------------------
Messung V0.5
¤ Dauer der Verarbeitung: 0.24 Sekunden
(vorverarbeitet)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.