blob: 29dd513c0cdc2669c786b1ce3562080f9ad8cfd8 [file] [log] [blame]
/*
* Copyright (C) 2007 Apple Inc. All rights reserved.
* Copyright (C) 2009 Joseph Pecoraro
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import {ComputedStyle, ComputedStyleModel, Events} from './ComputedStyleModel.js'; // eslint-disable-line no-unused-vars
import {PlatformFontsWidget} from './PlatformFontsWidget.js';
import {StylePropertiesSection, StylesSidebarPane, StylesSidebarPropertyRenderer} from './StylesSidebarPane.js';
/**
* @unrestricted
*/
export class ComputedStyleWidget extends UI.ThrottledWidget {
constructor() {
super(true);
this.registerRequiredCSS('elements/computedStyleSidebarPane.css');
this._alwaysShowComputedProperties = {'display': true, 'height': true, 'width': true};
this._computedStyleModel = new ComputedStyleModel();
this._computedStyleModel.addEventListener(Events.ComputedStyleChanged, this.update, this);
this._showInheritedComputedStylePropertiesSetting =
Common.settings.createSetting('showInheritedComputedStyleProperties', false);
this._showInheritedComputedStylePropertiesSetting.addChangeListener(
this._showInheritedComputedStyleChanged.bind(this));
const hbox = this.contentElement.createChild('div', 'hbox styles-sidebar-pane-toolbar');
const filterContainerElement = hbox.createChild('div', 'styles-sidebar-pane-filter-box');
const filterInput = StylesSidebarPane.createPropertyFilterElement(ls`Filter`, hbox, filterCallback.bind(this));
UI.ARIAUtils.setAccessibleName(filterInput, Common.UIString('Filter Computed Styles'));
filterContainerElement.appendChild(filterInput);
this.setDefaultFocusedElement(filterInput);
const toolbar = new UI.Toolbar('styles-pane-toolbar', hbox);
toolbar.appendToolbarItem(new UI.ToolbarSettingCheckbox(
this._showInheritedComputedStylePropertiesSetting, undefined, Common.UIString('Show all')));
this._noMatchesElement = this.contentElement.createChild('div', 'gray-info-message');
this._noMatchesElement.textContent = ls`No matching property`;
this._propertiesOutline = new UI.TreeOutlineInShadow();
this._propertiesOutline.hideOverflow();
this._propertiesOutline.setShowSelectionOnKeyboardFocus(true);
this._propertiesOutline.setFocusable(true);
this._propertiesOutline.registerRequiredCSS('elements/computedStyleWidgetTree.css');
this._propertiesOutline.element.classList.add('monospace', 'computed-properties');
this.contentElement.appendChild(this._propertiesOutline.element);
this._linkifier = new Components.Linkifier(_maxLinkLength);
/**
* @param {?RegExp} regex
* @this {ComputedStyleWidget}
*/
function filterCallback(regex) {
this._filterRegex = regex;
this._updateFilter(regex);
}
const fontsWidget = new PlatformFontsWidget(this._computedStyleModel);
fontsWidget.show(this.contentElement);
}
/**
* @override
*/
onResize() {
const isNarrow = this.contentElement.offsetWidth < 260;
this._propertiesOutline.contentElement.classList.toggle('computed-narrow', isNarrow);
}
_showInheritedComputedStyleChanged() {
this.update();
}
/**
* @override
* @return {!Promise.<?>}
*/
doUpdate() {
const promises = [this._computedStyleModel.fetchComputedStyle(), this._fetchMatchedCascade()];
return Promise.all(promises).spread(this._innerRebuildUpdate.bind(this));
}
/**
* @return {!Promise.<?SDK.CSSMatchedStyles>}
*/
_fetchMatchedCascade() {
const node = this._computedStyleModel.node();
if (!node || !this._computedStyleModel.cssModel()) {
return Promise.resolve(/** @type {?SDK.CSSMatchedStyles} */ (null));
}
return this._computedStyleModel.cssModel().cachedMatchedCascadeForNode(node).then(validateStyles.bind(this));
/**
* @param {?SDK.CSSMatchedStyles} matchedStyles
* @return {?SDK.CSSMatchedStyles}
* @this {ComputedStyleWidget}
*/
function validateStyles(matchedStyles) {
return matchedStyles && matchedStyles.node() === this._computedStyleModel.node() ? matchedStyles : null;
}
}
/**
* @param {string} text
* @return {!Node}
*/
_processColor(text) {
const color = Common.Color.parse(text);
if (!color) {
return createTextNode(text);
}
const swatch = InlineEditor.ColorSwatch.create();
swatch.setColor(color);
swatch.setFormat(Common.Color.detectColorFormat(color));
return swatch;
}
/**
* @param {?ComputedStyle} nodeStyle
* @param {?SDK.CSSMatchedStyles} matchedStyles
*/
_innerRebuildUpdate(nodeStyle, matchedStyles) {
/** @type {!Set<string>} */
const expandedProperties = new Set();
for (const treeElement of this._propertiesOutline.rootElement().children()) {
if (!treeElement.expanded) {
continue;
}
const propertyName = treeElement[_propertySymbol].name;
expandedProperties.add(propertyName);
}
const hadFocus = this._propertiesOutline.element.hasFocus();
this._propertiesOutline.removeChildren();
this._linkifier.reset();
const cssModel = this._computedStyleModel.cssModel();
if (!nodeStyle || !matchedStyles || !cssModel) {
this._noMatchesElement.classList.remove('hidden');
return;
}
const uniqueProperties = nodeStyle.computedStyle.keysArray();
uniqueProperties.sort(propertySorter);
const propertyTraces = this._computePropertyTraces(matchedStyles);
const inhertiedProperties = this._computeInheritedProperties(matchedStyles);
const showInherited = this._showInheritedComputedStylePropertiesSetting.get();
for (let i = 0; i < uniqueProperties.length; ++i) {
const propertyName = uniqueProperties[i];
const propertyValue = nodeStyle.computedStyle.get(propertyName);
const canonicalName = SDK.cssMetadata().canonicalPropertyName(propertyName);
const inherited = !inhertiedProperties.has(canonicalName);
if (!showInherited && inherited && !(propertyName in this._alwaysShowComputedProperties)) {
continue;
}
if (!showInherited && propertyName.startsWith('--')) {
continue;
}
if (propertyName !== canonicalName && propertyValue === nodeStyle.computedStyle.get(canonicalName)) {
continue;
}
const propertyElement = createElement('div');
propertyElement.classList.add('computed-style-property');
propertyElement.classList.toggle('computed-style-property-inherited', inherited);
const renderer =
new StylesSidebarPropertyRenderer(null, nodeStyle.node, propertyName, /** @type {string} */ (propertyValue));
renderer.setColorHandler(this._processColor.bind(this));
const propertyNameElement = renderer.renderName();
propertyNameElement.classList.add('property-name');
propertyElement.appendChild(propertyNameElement);
const colon = createElementWithClass('span', 'delimeter');
colon.textContent = ': ';
propertyNameElement.appendChild(colon);
const propertyValueElement = propertyElement.createChild('span', 'property-value');
const propertyValueText = renderer.renderValue();
propertyValueText.classList.add('property-value-text');
propertyValueElement.appendChild(propertyValueText);
const semicolon = createElementWithClass('span', 'delimeter');
semicolon.textContent = ';';
propertyValueElement.appendChild(semicolon);
const treeElement = new UI.TreeElement();
treeElement.title = propertyElement;
treeElement[_propertySymbol] = {name: propertyName, value: propertyValue};
const isOdd = this._propertiesOutline.rootElement().children().length % 2 === 0;
treeElement.listItemElement.classList.toggle('odd-row', isOdd);
this._propertiesOutline.appendChild(treeElement);
if (!this._propertiesOutline.selectedTreeElement) {
treeElement.select(!hadFocus);
}
const trace = propertyTraces.get(propertyName);
if (trace) {
const activeProperty = this._renderPropertyTrace(cssModel, matchedStyles, nodeStyle.node, treeElement, trace);
treeElement.listItemElement.addEventListener('mousedown', e => e.consume(), false);
treeElement.listItemElement.addEventListener('dblclick', e => e.consume(), false);
treeElement.listItemElement.addEventListener('click', handleClick.bind(null, treeElement), false);
treeElement.listItemElement.addEventListener(
'contextmenu', this._handleContextMenuEvent.bind(this, matchedStyles, activeProperty));
const gotoSourceElement = UI.Icon.create('mediumicon-arrow-in-circle', 'goto-source-icon');
gotoSourceElement.addEventListener('click', this._navigateToSource.bind(this, activeProperty));
propertyValueElement.appendChild(gotoSourceElement);
if (expandedProperties.has(propertyName)) {
treeElement.expand();
}
}
}
this._updateFilter(this._filterRegex);
/**
* @param {string} a
* @param {string} b
* @return {number}
*/
function propertySorter(a, b) {
if (a.startsWith('--') ^ b.startsWith('--')) {
return a.startsWith('--') ? 1 : -1;
}
if (a.startsWith('-webkit') ^ b.startsWith('-webkit')) {
return a.startsWith('-webkit') ? 1 : -1;
}
const canonical1 = SDK.cssMetadata().canonicalPropertyName(a);
const canonical2 = SDK.cssMetadata().canonicalPropertyName(b);
return canonical1.compareTo(canonical2);
}
/**
* @param {!UI.TreeElement} treeElement
* @param {!Event} event
*/
function handleClick(treeElement, event) {
if (!treeElement.expanded) {
treeElement.expand();
} else {
treeElement.collapse();
}
event.consume();
}
}
/**
* @param {!SDK.CSSProperty} cssProperty
* @param {!Event} event
*/
_navigateToSource(cssProperty, event) {
Common.Revealer.reveal(cssProperty);
event.consume(true);
}
/**
* @param {!SDK.CSSModel} cssModel
* @param {!SDK.CSSMatchedStyles} matchedStyles
* @param {!SDK.DOMNode} node
* @param {!UI.TreeElement} rootTreeElement
* @param {!Array<!SDK.CSSProperty>} tracedProperties
* @return {!SDK.CSSProperty}
*/
_renderPropertyTrace(cssModel, matchedStyles, node, rootTreeElement, tracedProperties) {
let activeProperty = null;
for (const property of tracedProperties) {
const trace = createElement('div');
trace.classList.add('property-trace');
if (matchedStyles.propertyState(property) === SDK.CSSMatchedStyles.PropertyState.Overloaded) {
trace.classList.add('property-trace-inactive');
} else {
activeProperty = property;
}
const renderer =
new StylesSidebarPropertyRenderer(null, node, property.name, /** @type {string} */ (property.value));
renderer.setColorHandler(this._processColor.bind(this));
const valueElement = renderer.renderValue();
valueElement.classList.add('property-trace-value');
valueElement.addEventListener('click', this._navigateToSource.bind(this, property), false);
const gotoSourceElement = UI.Icon.create('mediumicon-arrow-in-circle', 'goto-source-icon');
gotoSourceElement.addEventListener('click', this._navigateToSource.bind(this, property));
valueElement.insertBefore(gotoSourceElement, valueElement.firstChild);
trace.appendChild(valueElement);
const rule = property.ownerStyle.parentRule;
const selectorElement = trace.createChild('span', 'property-trace-selector');
selectorElement.textContent = rule ? rule.selectorText() : 'element.style';
selectorElement.title = selectorElement.textContent;
if (rule) {
const linkSpan = trace.createChild('span', 'trace-link');
linkSpan.appendChild(StylePropertiesSection.createRuleOriginNode(matchedStyles, this._linkifier, rule));
}
const traceTreeElement = new UI.TreeElement();
traceTreeElement.title = trace;
traceTreeElement.listItemElement.addEventListener(
'contextmenu', this._handleContextMenuEvent.bind(this, matchedStyles, property));
rootTreeElement.appendChild(traceTreeElement);
}
return /** @type {!SDK.CSSProperty} */ (activeProperty);
}
/**
* @param {!SDK.CSSMatchedStyles} matchedStyles
* @param {!SDK.CSSProperty} property
* @param {!Event} event
*/
_handleContextMenuEvent(matchedStyles, property, event) {
const contextMenu = new UI.ContextMenu(event);
const rule = property.ownerStyle.parentRule;
if (rule) {
const header = rule.styleSheetId ? matchedStyles.cssModel().styleSheetHeaderForId(rule.styleSheetId) : null;
if (header && !header.isAnonymousInlineStyleSheet()) {
contextMenu.defaultSection().appendItem(ls`Navigate to selector source`, () => {
StylePropertiesSection.tryNavigateToRuleLocation(matchedStyles, rule);
});
}
}
contextMenu.defaultSection().appendItem(ls`Navigate to style`, () => Common.Revealer.reveal(property));
contextMenu.show();
}
/**
* @param {!SDK.CSSMatchedStyles} matchedStyles
* @return {!Map<string, !Array<!SDK.CSSProperty>>}
*/
_computePropertyTraces(matchedStyles) {
const result = new Map();
for (const style of matchedStyles.nodeStyles()) {
const allProperties = style.allProperties();
for (const property of allProperties) {
if (!property.activeInStyle() || !matchedStyles.propertyState(property)) {
continue;
}
if (!result.has(property.name)) {
result.set(property.name, []);
}
result.get(property.name).push(property);
}
}
return result;
}
/**
* @param {!SDK.CSSMatchedStyles} matchedStyles
* @return {!Set<string>}
*/
_computeInheritedProperties(matchedStyles) {
const result = new Set();
for (const style of matchedStyles.nodeStyles()) {
for (const property of style.allProperties()) {
if (!matchedStyles.propertyState(property)) {
continue;
}
result.add(SDK.cssMetadata().canonicalPropertyName(property.name));
}
}
return result;
}
/**
* @param {?RegExp} regex
*/
_updateFilter(regex) {
const children = this._propertiesOutline.rootElement().children();
let hasMatch = false;
for (const child of children) {
const property = child[_propertySymbol];
const matched = !regex || regex.test(property.name) || regex.test(property.value);
child.hidden = !matched;
hasMatch |= matched;
}
this._noMatchesElement.classList.toggle('hidden', !!hasMatch);
}
}
const _maxLinkLength = 30;
const _propertySymbol = Symbol('property');
ComputedStyleWidget._propertySymbol = _propertySymbol;