/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* 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 PREF_CSS_EXPLAINERS = "devtools.inspector.css-explainers"; const FILTER_CHANGED_TIMEOUT = 150; // Removes the flash-out class from an element after 1 second (100ms in tests so they // don't take too long to run). const PROPERTY_FLASHING_DURATION = flags.testing ? 100 : 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*$/;
// List of all container IDs, order by typical order of display const PSEUDO_ELEMENTS_CONTAINER_ID = "pseudo-elements-container"; const ELEMENT_CONTAINER_ID = "element-container"; const REGISTERED_PROPERTIES_CONTAINER_ID = "registered-properties-container"; const POSITION_TRY_CONTAINER_ID = "position-try-container";
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, signal }
);
}
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();
// References to all active rule containers DOM Elements. // Containers can be: "pseudo element", "inherited by", "keyframes",... // Map(String => Object { header: DOM Element, container: DOM Element} ) // Map(Container ID => Header and container DOM Elements)
#containers = new Map();
// 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
#childHasDragged = false;
// The element that we're inspecting. // (Used from RuleViewTool class)
selectedNodeFront = null;
// Used for cancelling timeouts in the style filter.
#filterChangedTimeout = null;
// Empty, unconnected element of the same type as this selected node, // used to figure out how shorthand properties will be parsed.
#dummyElement = null;
#popup;
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;
}
#classListPreviewer;
get classListPreviewer() { if (!this.#classListPreviewer) { this.#classListPreviewer = new ClassListPreviewer( this.inspector, this.classPanel
);
}
returnthis.#classListPreviewer;
}
#contextMenu;
get contextMenu() { if (!this.#contextMenu) { this.#contextMenu = new StyleInspectorMenu(this, { isRuleView: true });
}
returnthis.#contextMenu;
}
// Get the dummy element.
get dummyElement() { returnthis.#dummyElement;
}
#refreshDummyElement() { // Only update the dummy element if the selected element's tag is different. if (
!this.selectedNodeFront || this.#dummyElement?.tagName === this.selectedNodeFront.tagName
) { return;
}
// 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. try { // ::before and ::after do not have a namespaceURI const namespaceURI = this.selectedNodeFront.namespaceURI || this.styleDocument.documentElement.namespaceURI; this.#dummyElement = this.styleDocument.createElementNS(
namespaceURI, this.selectedNodeFront.tagName
);
} catch (e) {
console.error("Error while creating dummy element", e);
}
}
// Get the highlighters overlay from the Inspector.
#highlighters;
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/unhighlightallthenodesthatmatchagivenrule'sselector *insidethedocumentofthecurrentselectednode. *Onlyoneselectorcanbehighlightedatatime,socallingthemethoda *secondtimewithadifferentrulewillfirstunhighlightthepreviously *highlightednodes. *Callingthemethodasecondtimewiththesamerulewilljust *unhighlightthehighlightednodes. * *@param{Rule}rule *@param{string}selector *Elementsmatchingthisselectorwillbehighlightedonthepage. *@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
);
}
}
// 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"
);
}
const valueSpan = target.closest(".ruleview-propertyvalue"); if (valueSpan) { if (this.#elementsWithPendingClicks.has(valueSpan)) { // When we start handling a drag in the TextPropertyEditor valueSpan, // we make the valueSpan capture the pointer. Then, `click` event target is always // the valueSpan with the latest spec of Pointer Events. // Therefore, we should stop immediate propagation of the `click` event // if we've handled a drag to prevent moving focus to the inplace editor.
event.stopImmediatePropagation(); return;
}
// Handle link click in RuleEditor property value if (target.nodeName === "a") {
event.stopPropagation();
event.preventDefault();
openContentLink(target.href, {
relatedToCurrent: true,
inBackground:
event.button === 1 ||
(lazy.AppConstants.platform === "macosx"
? event.metaKey
: event.ctrlKey),
});
}
}
}
/** *Delegatehandlerforhighlighterevents. * *Thisistheplacetoobserveforhighlighterevents,checkthehighlightertypeand *eventname,thenreacttospecificevents,forexamplebymodifyingtheDOM. * *@param{string}eventName *Highlightereventname.Oneof:"highlighter-hidden","highlighter-shown" *@param{object}data *Objectwithdataassociatedwiththehighlighterevent.
*/
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;
}
}
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");
}
/** *Addanewruletothecurrentelement.
*/
addNewRule() { // Clear the search input so the new rule is visible this.#onClearSearch({ focusSearchField: false });
#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");
};
// If the search is cleared update the UI directly so calls to this function (or any // callsite of it) can assume the UI is up to date directly after the call. if (isSearchEmpty) { this.#doFilterStyles();
} else { this.#filterChangedTimeout = setTimeout(
() => this.#doFilterStyles(),
FILTER_CHANGED_TIMEOUT
);
}
};
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;
}
/** *UpdatepageStylereferenceandlistenforstylesheetupdates.
*/
#refreshPageStyle() { const newPageStyle = this.selectedNodeFront?.inspectorFront.pageStyle; if (this.pageStyle == newPageStyle) { return;
} // If we were already selecting an element from a different process, // we should unregister the PageStyle Actor event listener if (this.pageStyle) { this.pageStyle.off("stylesheet-updated", this.refreshPanel); this.pageStyle = null;
} // If we are selecting a new element, we should also start listening // for event from its process and its related Page Style Actor. if (newPageStyle) { this.pageStyle = newPageStyle; this.pageStyle.on("stylesheet-updated", this.refreshPanel);
}
}
// 1/3 All cleanups that do not depend on former/new selected element if (this.#popup && this.#popup.isOpen) { this.#popup.hidePopup();
}
// 2/3 Important step: actually switch to a new or empty element this.selectedNodeFront = element;
// 3/3 Now update based on the newly selected element
// Wipe the whole rule view content when deselecting or selecting another element // as there is little chance we would display the same rules. It is faster // to render everything from scratch than trying to do incremental updates. if (!sameElementSelected) { this.#clearRules();
}
// Destroy the ElementStyle *after* having cleared the rule view // (earlier call to `#clearRules`), as it may destroy each DOM element // for each rule individually and cause unecessary reflows if (this.elementStyle) { this.elementStyle.destroy(); this.elementStyle = null;
} return;
}
const elementStyle = new ElementStyle(
element, this, this.store, this.pageStyle, this.#showUserAgentStyles
);
let previousElementStyle = this.elementStyle; this.elementStyle = elementStyle;
this.#startSelectingElement();
try { // Bug 2016127: This is historical, but unfortunately breaks some tests if removed
await Promise.resolve(null);
await this.#populate(); if (this.elementStyle !== elementStyle) {
done(); return;
} this.elementStyle.onChanged = () => { this.#onElementStyleChanged();
}; // Cleanup the previous ElementStyle model only after the refresh // as it will destroy each rule individually and may cause uncessary reflows // if entire containers are removed. if (previousElementStyle) {
previousElementStyle.destroy();
previousElementStyle = null;
// We need to update containers as some may now be empty this.#updateContainers();
} this.#stopSelectingElement(); if (isProfilerActive && this.elementStyle.rules) {
let declarations = 0; for (const rule of this.elementStyle.rules) {
declarations += rule.textProps.length;
}
ChromeUtils.addProfilerMarker( "DevTools:CssRuleView.selectElement",
startTime,
`${declarations} CSS declarations in ${this.elementStyle.rules.length} rules`
);
}
} catch (e) { if (this.elementStyle === elementStyle) { this.#stopSelectingElement(); this.#clearRules();
}
console.error("Error while updating the rule view", e);
}
done();
}
/** *Updatetherulesforthecurrentlyhighlightedelement.
*/
async 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;
}
// 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);
}
}
for (const [pseudo, elementTypes] of Object.entries(
ELEMENT_SPECIFIC_PSEUDO_CLASSES
)) { if (elementTypes.has(tagName)) {
applicablePseudoClasses.push(pseudo);
}
}
return applicablePseudoClasses;
}
/** *Updatethepseudoclassoptionsforthecurrentlyhighlightedelement.
*/
#refreshPseudoClassPanel() { if (
!this.selectedNodeFront ||
!this.inspector.canTogglePseudoClassForSelectedNode()
) { for (const checkbox of [
...this.pseudoClassCheckboxes,
...this.elementSpecificPseudoClassCheckboxes,
]) {
checkbox.disabled = true;
} this.#updateElementSpecificPseudoClassPanel(); return;
}
const pseudoClassLocks = this.selectedNodeFront.pseudoClassLocks; for (const checkbox of this.pseudoClassCheckboxes) {
checkbox.disabled = false;
checkbox.checked = pseudoClassLocks.includes(checkbox.value);
}
const applicablePseudoClasses = this.#getApplicableElementSpecificPseudoClasses(); for (const checkbox of this.elementSpecificPseudoClassCheckboxes) { const isApplicable = applicablePseudoClasses.includes(checkbox.value);
checkbox.disabled = !isApplicable; if (isApplicable) {
checkbox.checked = pseudoClassLocks.includes(checkbox.value);
}
}
if (this.elementStyle !== elementStyle || this.isDestroyed) { return;
}
await this.#createEditors();
// Notify anyone that cares that we refreshed. this.inspector.emit("rule-view-refreshed");
} catch (e) {
console.error("Exception while populating the rule view", e); throw e;
}
}
/** *Textforheaderthatshowsaboverulesforthiselement
*/
#selectedElementLabel;
get selectedElementLabel() { if (this.#selectedElementLabel) { returnthis.#selectedElementLabel;
} this.#selectedElementLabel = l10n("rule.selectedElement"); returnthis.#selectedElementLabel;
}
/** *Textforheaderthatshowsaboverulesforpseudoelements
*/
#pseudoElementLabel;
get pseudoElementLabel() { if (this.#pseudoElementLabel) { returnthis.#pseudoElementLabel;
} this.#pseudoElementLabel = l10n("rule.pseudoElement"); returnthis.#pseudoElementLabel;
}
#showPseudoElements;
get showPseudoElements() { if (this.#showPseudoElements === undefined) { this.#showPseudoElements = Services.prefs.getBoolPref( "devtools.inspector.show_pseudo_elements"
);
} returnthis.#showPseudoElements;
}
/** *Createsasimple,non-expandablecontainerintheruleview * *@param{string}label *Thelabelforthecontainerheader *@param{string}containerId *Theidthatwillbesetonthecontainer *@return{Object{header:DOMElement,container:DOMElement}} *Objectcontainingboththecontainerelementanditsrelatedheader.
*/
createSimpleContainer(label, containerId) { const header = this.styleDocument.createElementNS(HTML_NS, "div");
header.className = RULE_VIEW_HEADER_CLASSNAME;
header.setAttribute("role", "heading"); // Element container is only shown when pseudo element container exists // which can only be computed later after having processed all rules. if (containerId == ELEMENT_CONTAINER_ID) {
header.hidden = true;
}
header.append(label);
const { signal } = this.#abortController;
toggleButton.addEventListener( "click", this.#toggleContainerVisibility.bind(this, containerId),
{ signal }
);
// All containers are expanded by default, but pseudo elements // are only expanded if the related preference is true. // So manually collapse the pseudo container if this pref is false. const isPseudo = containerId == PSEUDO_ELEMENTS_CONTAINER_ID; if (isPseudo && !this.showPseudoElements) { this.#toggleContainerVisibility(containerId);
}
// Memoize the state in the pref for pseudo elements const isPseudo = containerId == PSEUDO_ELEMENTS_CONTAINER_ID; if (isPseudo) { this.#showPseudoElements = shouldExpand;
Services.prefs.setBoolPref( "devtools.inspector.show_pseudo_elements", this.#showPseudoElements
);
}
/** *CreateseditorUIforeachoftherulesinelementStyle.
*/
#createEditors() { // Run through the current list of rules, attaching // their editors in order. Create editors if needed.
let seenSearchTerm = false;
if (!this.elementStyle.rules) { return Promise.resolve();
}
// Transient list of containers added to the DOM // while processing this method. const currentContainers = [];
const editorReadyPromises = []; const lastElementPerContainer = new Map(); for (const rule of this.elementStyle.rules) { // Initialize rule editor if this is a new rule if (!rule.editor) {
editorReadyPromises.push(this.#createEditorForRule(rule));
}
// 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;
}
}
// Ensure adding the rule editor at the right location const lastElement = lastElementPerContainer.get(container); // Also avoid any DOM mutation if the rule already exists and wasn't moved if (
lastElement &&
lastElement.nextElementSibling != rule.editor.element
) {
lastElement.insertAdjacentElement("afterend", rule.editor.element);
} elseif (
!lastElement &&
container.firstElementChild != rule.editor.element
) {
container.insertAdjacentElement("afterbegin", rule.editor.element);
}
lastElementPerContainer.set(container, rule.editor.element);
}
this.#createRegisteredPropertyEditors();
this.#updateContainers();
// Automatically select the selector input when we are adding a user-added rule. // (Focus after having updated all the rules to prevent the focus from being // lost because of possible following DOM updates) if (this.#focusNextUserAddedRule) { const rule = this.elementStyle.rules.find(r => r.domRule.userAdded); if (rule) {
rule.editor.selectorText.click(); this.emitForTests("new-rule-added", rule);
} this.#focusNextUserAddedRule = null;
}
// Don't display inherited pseudo element rules (e.g. ::details-content) inside // the pseudo element container const isNonInheritedPseudo = !!rule.pseudoElement && !rule.inherited; const keyframes = rule.keyframes;
if (isNonInheritedPseudo) {
id = PSEUDO_ELEMENTS_CONTAINER_ID;
label = this.pseudoElementLabel;
expandable = true;
} elseif (keyframes) { // See bug 1042036 and Bug 1894873 we are showing all keyframes with the same name. // We may use ${keyframes.name}, but all rule's content would be merged // into a unique rule/container. // (only use part of the actorID to have a valid DOM id)
id = `keyframes-container-${rule.keyframes.actorID.match(/\w+$/)[0]}`;
label = rule.keyframesName;
expandable = true;
} elseif (rule.domRule.className === "CSSPositionTryRule") {
id = POSITION_TRY_CONTAINER_ID;
label = "@position-try";
expandable = true;
} elseif (rule.inherited) { // We need to check both `inherited` (a NodeFront) and `pseudoElement` (string), // as element-backed pseudo element rules (e.g. `::details-content`) can have the same // `inherited` property as a regular rule (e.g. on `<details>`), but the element is // to be considered as a child of the binding element. // // e.g. we want to have: // This element // Inherited by details::details-content // Inherited by details
id = `inherited-${rule.inherited.actorID}-${rule.pseudoElement}`;
label = rule.inheritedSectionLabel;
} else {
id = ELEMENT_CONTAINER_ID;
label = this.selectedElementLabel;
}
let entry = this.#containers.get(id); // Create the DOM for the container if it doesn't exist yet if (!entry) { if (expandable) {
entry = this.createExpandableContainer(label, id);
} else {
entry = this.createSimpleContainer(label, id);
}
}
const { header, container } = entry;
// Ensure displaying the containers in the new order processed in #createEditors. if (!currentContainers.includes(container)) { const lastContainer = currentContainers.at(-1); // Pseudo element rules are sorted **after** element matching rules and inherited rules // in ElementStyle.rules, but we want the container to always be shown at the top. // // Also avoid any DOM mutation if the rule already exists and wasn't moved if (id == PSEUDO_ELEMENTS_CONTAINER_ID || !lastContainer) { if (this.element.firstElementChild != header) { this.element.insertAdjacentElement("afterbegin", header);
header.insertAdjacentElement("afterend", container);
}
} elseif (lastContainer.nextElementSibling != header) {
lastContainer.insertAdjacentElement("afterend", header);
header.insertAdjacentElement("afterend", container);
}
currentContainers.push(container);
}
return container;
}
/** *Wheneverwemayaddorremoverulesinthelist, *wehavetoeventuallyupdatecontainersbyremovingtheemptyones *andupdatethevisibilityof"Element"header.
*/
#updateContainers() { // Clear containers which no longer contain any rule for (const [
containerId,
{ header, container },
] of this.#containers.entries()) { if (!container.children.length) {
header.remove();
container.remove(); this.#containers.delete(containerId);
}
}
// Only print header for "This element" if there are pseudo elements displayed before const elementEntry = this.#containers.get(ELEMENT_CONTAINER_ID); if (elementEntry) { const hasPseudoElementRules = this.#containers.has(
PSEUDO_ELEMENTS_CONTAINER_ID
); // Show the header element, which is before the container element
elementEntry.header.hidden = !hasPseudoElementRules;
}
}
#createRegisteredPropertyEditors() { const targetRegisteredProperties = this.getRegisteredPropertiesForSelectedNodeTarget(); if (!targetRegisteredProperties?.size) { // Wipe the list, but only if the properties container was populated const entry = this.#containers.get(REGISTERED_PROPERTIES_CONTAINER_ID); if (entry) {
entry.container.replaceChildren();
} return;
}
const registeredPropertiesContainer = this.getOrCreateRegisteredPropertiesExpandableContainer(); // Always wipe and rebuild the list of properties from scratch
registeredPropertiesContainer.replaceChildren();
// 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 search matches in the rule properties for (const textProp of rule.textProps) { if (!textProp.invisible && this.#highlightProperty(textProp)) {
isHighlighted = true;
}
}
// Expand the computed list if a computed property is highlighted and the // property rule is not highlighted if (
!isPropertyHighlighted &&
isComputedHighlighted &&
!textProperty.editor.computed.hasAttribute("user-open")
) {
textProperty.editor.expandForFilter();
}
/** *Highlightstherulepropertythatmatchesthefiltersearchvalue *andreturnsabooleanindicatingwhetherornotthepropertywas *highlighted. * *@param{TextProperty}textProperty *Therulepropertyobject. *@returns{boolean}trueiftherulepropertywashighlighted, *falseotherwise.
*/ #highlightRuleProperty(textProperty) { const propertyName = textProperty.name.toLowerCase(); // Get the actual property value displayed in the rule view if we have an editor for // it (that might not be the case for unused CSS custom properties). const propertyValue = textProperty.editor
? textProperty.editor.valueSpan.textContent.toLowerCase()
: textProperty.value.toLowerCase();
// Highlight search matches in the computed list of properties
textProperty.editor.populateComputed(); for (const computed of textProperty.computed) { if (computed.element) { // Get the actual property value displayed in the computed list const computedName = computed.name.toLowerCase(); const computedValue = computed.parsedValue.toLowerCase();
// 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);
};
// We might not have an element when the prop is an unused custom css property. if (!element && textProperty?.isUnusedVariable) { const editor =
textProperty.rule.editor.showUnusedCssVariable(textProperty);
// The editor couldn't be created, bail (shouldn't happen) if (!editor) { returnfalse;
}
element = editor.container;
}
element.classList.add("ruleview-highlight");
return true;
}
/** *Clearallsearchfilterhighlightsinthepanel,andclosethecomputed *listiftoggledopened
*/ #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();
}
}
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) {
elementToScrollTo = declaration;
}
}
// Ensure that smooth scrolling is disabled when the user prefers reduced motion. const win = elementToScrollTo.documentGlobal; const reducedMotion = win.matchMedia("(prefers-reduced-motion)").matches;
scrollBehavior = reducedMotion ? "instant" : scrollBehavior;
elementToScrollTo.scrollIntoView({
behavior: scrollBehavior,
});
}
/** *FindsthespecifiedTextPropertynameintheruleview.Iffound,scrolltoand *flashtheTextProperty. * *@param{string}name *Thepropertynametoscrolltoandhighlight. *@param{object}options *@param{StyleRuleFront|undefined}options.ruleFront *AnoptionalStyleRuleFront.Whenthisisset,wewillonlylookfortheproperty *inthisexactrule. *@param{Function|undefined}options.ruleValidator *Anoptionalfunctionthatcanbeusedtofilteroutrulesweshouldn'tlook *intotofindthepropertyname.ThefunctioniscalledwithaRuleobject, *andtherulewillbeskippedifthefunctionreturnsafalsyvalue. *@param{true|undefined}options.focusValue *Anoptionalbooleanthatindicatethatthedeclarationvalueshouldbefocused. *Iffalse(thedefaultvalue),thedeclarationnamewillbefocused. *@returns{boolean}trueiftheTextPropertynameisfound,andfalseotherwise.
*/
highlightProperty = async (
name,
{ ruleValidator, ruleFront, focusValue = false } = {}
) => { // First, let's clear any search we might have, as the property could be hidden
this.#onClearSearch({ focusSearchField: false });
if (ruleFront) { const rule = this.rules.find(r => r.domRule === ruleFront); if (!rule) {
console.error("Unable to find a rule for actor", ruleFront); returnfalse;
}
const highlighted = await this.#maybeHighlightPropertyInRule({
name,
rule,
ruleValidator, // If ruleFront is passed, we might need to highlight a declaration that is actually // overridden (e.g. when calling this from a "matched selector" item in the // computed panel).
matchOverridden: true,
focusValue,
}); if (!highlighted) {
console.error("Unable to highlight rule", name, rule); returnfalse;
}
return true;
}
for (const rule of this.rules) { if (
await this.#maybeHighlightPropertyInRule({
name,
rule,
ruleValidator,
focusValue,
})
) { return true;
}
} // If the property is a CSS variable and we didn't find its declaration, it might // be a registered property if (this.#maybeHighlightCssRegisteredProperty(name)) { return true;
}
let matchingTextPropComputed; // hasHigherPriorityThanEarlierProp (that we use in the loop), is expecting // the props to be iterated through in reverse. const textProps = rule.textProps.toReversed(); for (const textProp of textProps) { for (const computed of textProp.computed) { if (
computed.name === name &&
!textProp.invisible &&
textProp.enabled &&
(!computed.overridden || matchOverridden) &&
(!matchingTextPropComputed ||
this.elementStyle.hasHigherPriorityThanEarlierProp(
computed,
matchingTextPropComputed
))
) {
matchingTextPropComputed = computed;
}
}
}
if (!matchingTextPropComputed) { returnfalse;
}
await this.#selectRuleViewIfNeeded();
let scrollBehavior;
// If the property is being applied by a pseudo element rule, expand the pseudo // element list container. if (rule.pseudoElement.length && !this.showPseudoElements) { // Set the scroll behavior to "instant" to avoid timing issues between toggling // the pseudo element container and scrolling smoothly to the rule.
scrollBehavior = "instant";
this.#toggleContainerVisibility(PSEUDO_ELEMENTS_CONTAINER_ID);
}
// If we're jumping to an unused CSS variable, it might not be visible, so show // it here. if (!textProp.editor && textProp.isUnusedVariable) {
textProp.rule.editor.showUnusedCssVariable(textProp);
}
// If the textProp is a shorthand, we need to expand the computed list so we can // highlight the proper element. const isTextPropAShorthand =
textProp.name !== matchingTextPropComputed.name; if (isTextPropAShorthand) {
textProp.editor.expandForFilter();
}
this.#highlightElementInRule({
rule,
elementToHighlight: isTextPropAShorthand
? // if the prop is a shorthand, we hand to highlight the exact longhand property
matchingTextPropComputed.element
: textProp.editor.element,
elementToFocus: focusValue
? textProp.editor.valueSpan
: textProp.editor.nameSpan,
scrollBehavior,
}); return true;
}
if (elementToFocus) {
elementToFocus.focus({
focusVisible: true, // Don't scroll when focusing, this is taken care of by the call to #scrollToElement
preventScroll: true,
});
}
async #selectRuleViewIfNeeded() { // If three-pane mode is enable, or if the rule view is already the currently selected // tab, there's no need to do anything. if (
this.inspector.isThreePaneModeEnabled ||
this.inspector.sidebar.getCurrentTabID() === "ruleview"
) { return;
}
// Otherwise, we need to select the ruleview from the sidebar. This will update the // panel, so we need to wait for it be populatedr const onRefreshed = this.inspector.once("rule-view-refreshed");
await this.inspector.sidebar.select("ruleview");
await onRefreshed;
}
}
// We do want to get already existing registered properties, so we need to watch // them separately
this.inspector.commands.resourceCommand
.watchResources(
[
this.inspector.commands.resourceCommand.TYPES
.CSS_REGISTERED_PROPERTIES,
],
{
onAvailable: this.#onResourceAvailable,
onUpdated: this.#onResourceUpdated,
onDestroyed: this.#onResourceDestroyed,
ignoreExistingResources: false,
}
)
.catch(e => { // watchResources is async and even making it's resulting promise part of // this.readyPromise still causes test failures, so simply ignore the rejection // if the view was already destroyed. if (!this.view) { return;
} throw e;
});
// At the moment `readyPromise` is only consumed in tests (see `openRuleView`) to be // notified when the ruleview was first populated to match the initial selected node.
this.readyPromise = this.onSelected();
}
#abortController;
isPanelVisible() { if (!this.view) { returnfalse;
} return this.view.isPanelVisible();
}
onDetachedFront() {
this.onSelected(false);
}
async onSelected(selectElement = true) { // Ignore the event if the view has been destroyed, or if it's inactive. // But only if the current selection isn't null. If it's been set to null, // let the update go through as this is needed to empty the view on // navigation. if (!this.view) { return;
}
refresh() { if (this.isPanelVisible()) {
this.view.refreshPanel();
}
}
#onResourceAvailable = resources => { if (!this.inspector) { return;
}
let hasNewStylesheet = false; const addedRegisteredProperties = []; for (const resource of resources) { if (
resource.resourceType ===
this.inspector.commands.resourceCommand.TYPES.DOCUMENT_EVENT &&
resource.name === "will-navigate"
) {
this.view.cssRegisteredPropertiesByTarget.delete(resource.targetFront); if (resource.targetFront.isTopLevel) {
this.clearUserProperties();
} continue;
}
if (
resource.resourceType ===
this.inspector.commands.resourceCommand.TYPES.STYLESHEET && // resource.isNew is only true when the stylesheet was added from DevTools, // for example when adding a rule in the rule view. In such cases, we're already // updating the rule view, so ignore those.
!resource.isNew
) {
hasNewStylesheet = true;
}
if (
resource.resourceType ===
this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
) { if (
!this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
) {
this.view.cssRegisteredPropertiesByTarget.set(
resource.targetFront, new Map()
);
}
this.view.cssRegisteredPropertiesByTarget
.get(resource.targetFront)
.set(resource.name, resource); // Only add properties from the same target as the selected node if (
this.view.inspector.selection?.nodeFront?.targetFront ===
resource.targetFront
) {
addedRegisteredProperties.push(resource);
}
}
}
// Then add all new registered properties const names = new Set(); for (const propertyDefinition of addedRegisteredProperties) { const editor = new RegisteredPropertyEditor(
this.view,
propertyDefinition
);
names.add(propertyDefinition.name);
// We need to insert the element at the right position so we keep the list of // properties alphabetically sorted.
let referenceNode = null; for (const child of registeredPropertiesContainer.children) { if (child.getAttribute("data-name") > propertyDefinition.name) {
referenceNode = child; break;
}
}
registeredPropertiesContainer.insertBefore(
editor.element,
referenceNode
);
}
// Finally, update textProps that might rely on those new properties
this.#updateElementStyleRegisteredProperties(names);
}
if (hasNewStylesheet) {
this.refresh();
}
};
#onResourceUpdated = updates => { const updatedProperties = []; for (const update of updates) { if (
update.resource.resourceType ===
this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
) { const { resource } = update; if (
!this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
) { continue;
}
// Only consider properties from the same target as the selected node if (
this.view.inspector.selection?.nodeFront?.targetFront ===
resource.targetFront
) {
updatedProperties.push(resource);
}
}
}
const names = new Set(); if (updatedProperties.length) { const registeredPropertiesContainer =
this.view.styleDocument.getElementById(
REGISTERED_PROPERTIES_CONTAINER_ID
); for (const resource of updatedProperties) { // Replace the existing registered property editor element with a new one, // so we don't have to compute which elements should be updated. const name = resource.name; const el = this.view.getRegisteredPropertyElement(name); const editor = new RegisteredPropertyEditor(this.view, resource);
registeredPropertiesContainer.replaceChild(editor.element, el);
names.add(resource.name);
} // Finally, update textProps that might rely on those new properties
this.#updateElementStyleRegisteredProperties(names);
}
};
#onResourceDestroyed = resources => { const destroyedPropertiesNames = new Set(); for (const resource of resources) { if (
resource.resourceType ===
this.inspector.commands.resourceCommand.TYPES.CSS_REGISTERED_PROPERTIES
) { if (
!this.view.cssRegisteredPropertiesByTarget.has(resource.targetFront)
) { continue;
}
// Only consider properties from the same target as the selected node if (
this.view.inspector.selection?.nodeFront?.targetFront ===
resource.targetFront
) {
destroyedPropertiesNames.add(resourceName);
}
}
} if (destroyedPropertiesNames.size > 0) { for (const name of destroyedPropertiesNames) {
this.view.getRegisteredPropertyElement(name)?.remove();
} // Finally, update textProps that were relying on those removed properties
this.#updateElementStyleRegisteredProperties(destroyedPropertiesNames);
}
};
¤ 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.0.98Bemerkung:
¤
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.