Bug 1120616 - Part 1: Implement filter styles in rule view r=bgrins

This commit is contained in:
Gabriel Luong 2015-04-16 14:15:09 -04:00
parent 464cf5515d
commit 41ccb8bf7b
15 changed files with 406 additions and 48 deletions

View File

@ -23,7 +23,7 @@ Cu.import("resource://gre/modules/devtools/Templater.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
"resource://gre/modules/PluralForm.jsm");
const FILTER_CHANGED_TIMEOUT = 300;
const FILTER_CHANGED_TIMEOUT = 150;
const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

View File

@ -35,4 +35,21 @@
}
</script>
</head>
<body>
<div id="root" class="devtools-monospace">
<div class="devtools-toolbar">
<div class="devtools-searchbox">
<input id="ruleview-searchbox"
class="devtools-searchinput devtools-rule-searchbox"
type="search" placeholder="&userStylesSearch;"/>
<button id="ruleview-searchinput-clear" class="devtools-searchinput-clear"></button>
</div>
</div>
</div>
<div id="ruleview-container" class="ruleview devtools-monospace">
</div>
</body>
</html>

View File

@ -25,6 +25,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const PREF_UA_STYLES = "devtools.inspector.showUserAgentStyles";
const PREF_DEFAULT_COLOR_UNIT = "devtools.defaultColorUnit";
const FILTER_CHANGED_TIMEOUT = 150;
/**
* These regular expressions are adapted from firebug's css.js, and are
@ -1113,21 +1114,31 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
this.doc = aDoc;
this.store = aStore || {};
this.pageStyle = aPageStyle;
this.element = this.doc.createElementNS(HTML_NS, "div");
this.element.className = "ruleview devtools-monospace";
this.element.flex = 1;
this._highlightedElements = [];
this._outputParser = new OutputParser();
this._buildContextMenu = this._buildContextMenu.bind(this);
this._onContextMenu = this._onContextMenu.bind(this);
this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
this._onAddRule = this._onAddRule.bind(this);
this._onSelectAll = this._onSelectAll.bind(this);
this._onCopy = this._onCopy.bind(this);
this._onCopyColor = this._onCopyColor.bind(this);
this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
this._onFilterStyles = this._onFilterStyles.bind(this);
this._onClearSearch = this._onClearSearch.bind(this);
this.element = this.doc.getElementById("ruleview-container");
this.searchField = this.doc.getElementById("ruleview-searchbox");
this.searchClearButton = this.doc.getElementById("ruleview-searchinput-clear");
this.searchClearButton.hidden = true;
this.element.addEventListener("copy", this._onCopy);
this.element.addEventListener("contextmenu", this._onContextMenu);
this.searchField.addEventListener("input", this._onFilterStyles);
this.searchClearButton.addEventListener("click", this._onClearSearch);
this._handlePrefChange = this._handlePrefChange.bind(this);
this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
@ -1163,6 +1174,9 @@ CssRuleView.prototype = {
// The element that we're inspecting.
_viewedElement: null,
// Used for cancelling timeouts in the style filter.
_filterChangedTimeout: null,
/**
* Build the context menu.
*/
@ -1420,6 +1434,21 @@ CssRuleView.prototype = {
return true;
},
/**
* Context menu handler.
*/
_onContextMenu: function(event) {
try {
// In the sidebar we do not have this.doc.popupNode so we need to save
// the node ourselves.
this.doc.popupNode = event.explicitOriginalTarget;
this.doc.defaultView.focus();
this._contextmenu.openPopupAtScreen(event.screenX, event.screenY, true);
} catch(e) {
console.error(e);
}
},
/**
* Select all text.
*/
@ -1567,6 +1596,44 @@ CssRuleView.prototype = {
}
},
/**
* Called when the user enters a search term in the filter style search box.
*/
_onFilterStyles: function() {
if (this._filterChangedTimeout) {
clearTimeout(this._filterChangedTimeout);
}
let filterTimeout = (this.searchField.value.length > 0)
? FILTER_CHANGED_TIMEOUT : 0;
this.searchClearButton.hidden = this.searchField.value.length === 0;
this._filterChangedTimeout = setTimeout(() => {
if (this.searchField.value.length > 0) {
this.searchField.setAttribute("filled", true);
} else {
this.searchField.removeAttribute("filled");
}
this._clearRules();
this._createEditors();
this.inspector.emit("ruleview-filtered");
this._filterChangeTimeout = null;
}, filterTimeout);
},
/**
* Called when the user clicks on the clear button in the filter style search
* box.
*/
_onClearSearch: function() {
this.searchField.value = "";
this.searchField.focus();
this._onFilterStyles();
},
destroy: function() {
this.isDestroyed = true;
this.clear();
@ -1578,10 +1645,8 @@ CssRuleView.prototype = {
this._prefObserver.off(PREF_DEFAULT_COLOR_UNIT, this._handlePrefChange);
this._prefObserver.destroy();
this.element.removeEventListener("copy", this._onCopy);
this._onCopy = null;
this._outputParser = null;
this._highlightedElements = null;
// Remove context menu
if (this._contextmenu) {
@ -1616,6 +1681,14 @@ CssRuleView.prototype = {
this.tooltips.destroy();
this.highlighters.destroy();
// Remove bound listeners
this.element.removeEventListener("copy", this._onCopy);
this.element.removeEventListener("contextmenu", this._onContextMenu);
this.searchField.removeEventListener("input", this._onFilterStyles);
this.searchClearButton.removeEventListener("click", this._onClearSearch);
this.searchField = null;
this.searchClearButton = null;
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
@ -1855,17 +1928,38 @@ CssRuleView.prototype = {
let lastKeyframes = null;
let seenPseudoElement = false;
let seenNormalElement = false;
let seenSearchTerm = false;
let container = null;
let searchTerm = this.searchField.value.toLowerCase();
let isValidSearchTerm = searchTerm.trim().length > 0;
if (!this._elementStyle.rules) {
return;
}
if (this._highlightedElements.length > 0) {
this.clearHighlight();
}
for (let rule of this._elementStyle.rules) {
if (rule.domRule.system) {
continue;
}
// Initialize rule editor
if (!rule.editor) {
rule.editor = new RuleEditor(this, rule);
}
// Filter the rules and highlight any matches if there is a search input
if (isValidSearchTerm) {
if (this.highlightRules(rule, searchTerm)) {
seenSearchTerm = true;
} else if (rule.domRule.type !== ELEMENT_STYLE) {
continue;
}
}
// Only print header for this element if there are pseudo elements
if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
seenNormalElement = true;
@ -1895,16 +1989,95 @@ CssRuleView.prototype = {
container = this.createExpandableContainer(rule.keyframesName);
}
if (!rule.editor) {
rule.editor = new RuleEditor(this, rule);
}
if (container && (rule.pseudoElement || keyframes)) {
container.appendChild(rule.editor.element);
} else {
this.element.appendChild(rule.editor.element);
}
}
if (searchTerm && !seenSearchTerm) {
this.searchField.classList.add("devtools-style-searchbox-no-match");
} else {
this.searchField.classList.remove("devtools-style-searchbox-no-match");
}
},
/**
* Highlight rules that matches the given search value and returns a boolean
* indicating whether or not rules were highlighted.
*
* @param {Rule} aRule
* The rule object we're highlighting if its rule selectors or property
* values match the search value.
* @param {String} aValue
* The search value.
*/
highlightRules: function(aRule, aValue) {
let isHighlighted = false;
let selectorNodes = [...aRule.editor.selectorText.childNodes];
if (aRule.domRule.type === Ci.nsIDOMCSSRule.KEYFRAME_RULE) {
selectorNodes = [aRule.editor.selectorText];
} else if (aRule.domRule.type === ELEMENT_STYLE) {
selectorNodes = [];
}
aValue = aValue.trim();
// Highlight search matches in the rule selectors
for (let selectorNode of selectorNodes) {
if (selectorNode.textContent.toLowerCase().contains(aValue)) {
selectorNode.classList.add("ruleview-highlight");
this._highlightedElements.push(selectorNode);
isHighlighted = true;
}
}
// Parse search value as a single property line and extract the property
// name and value. Otherwise, use the search value as both the name and
// value.
let propertyMatch = CSS_PROP_RE.exec(aValue);
let name = propertyMatch ? propertyMatch[1] : aValue;
let value = propertyMatch ? propertyMatch[2] : aValue;
// Highlight search matches in the rule properties
for (let textProp of aRule.textProps) {
// Get the actual property value displayed in the rule view
let propertyValue = textProp.editor.valueSpan.textContent.toLowerCase();
let propertyName = textProp.name.toLowerCase();
// If the input value matches a property line like `font-family: arial`,
// then check to make sure the name and value match. Otherwise, just
// compare the input string directly against the name and value elements.
let matches = false;
if (propertyMatch && name && value) {
matches = propertyName.contains(name) && propertyValue.contains(value);
} else {
matches = (name && propertyName.contains(name)) ||
(value && propertyValue.contains(value));
}
if (matches) {
// if (matchTextProperty || matchNameOrValue) {
textProp.editor.element.classList.add("ruleview-highlight");
this._highlightedElements.push(textProp.editor.element);
isHighlighted = true;
}
}
return isHighlighted;
},
/**
* Clear all search filter highlights in the panel.
*/
clearHighlight: function() {
for (let element of this._highlightedElements) {
element.classList.remove("ruleview-highlight");
}
this._highlightedElements = [];
}
};
@ -2035,22 +2208,6 @@ RuleEditor.prototype = {
textContent: "}"
});
this.element.addEventListener("contextmenu", event => {
try {
// In the sidebar we do not have this.doc.popupNode so we need to save
// the node ourselves.
this.doc.popupNode = event.explicitOriginalTarget;
let win = this.doc.defaultView;
win.focus();
this.ruleView._contextmenu.openPopupAtScreen(
event.screenX, event.screenY, true);
} catch(e) {
console.error(e);
}
}, false);
if (this.isEditable) {
code.addEventListener("click", () => {
let selection = this.doc.defaultView.getSelection();

View File

@ -2,7 +2,30 @@
* 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/. */
#root {
* {
box-sizing: border-box;
}
:root {
height: 100%;
}
body {
margin: 0;
display: flex;
flex-direction: column;
height: 100%;
}
#ruleview-container {
-moz-user-select: text;
overflow: auto;
min-height: 0;
flex: 1;
}
#root .devtools-toolbar {
width: 100%;
display: -moz-box;
}

View File

@ -24,7 +24,6 @@ function RuleViewTool(inspector, window, iframe) {
this.doc = window.document;
this.view = new RuleView.CssRuleView(inspector, this.doc);
this.doc.documentElement.appendChild(this.view.element);
this.onLinkClicked = this.onLinkClicked.bind(this);
this.onSelected = this.onSelected.bind(this);
@ -152,8 +151,6 @@ RuleViewTool.prototype = {
this.view.off("ruleview-changed", this.onPropertyChanged);
this.view.off("ruleview-refreshed", this.onViewRefreshed);
this.doc.documentElement.removeChild(this.view.element);
this.view.destroy();
this.view = this.doc = this.inspector = null;

View File

@ -15,7 +15,7 @@
<!-- LOCALIZATION NOTE (userStylesSearch): This is the placeholder that goes in
- the search box when no search term has been entered. -->
<!ENTITY userStylesSearch "Search">
<!ENTITY userStylesSearch "Filter Styles">
<!-- LOCALIZATION NOTE (selectedElementLabel): This is the label for the path of
- the highlighted element in the web page. This path is based on the document

View File

@ -403,6 +403,9 @@ browser.jar:
skin/classic/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)
skin/classic/browser/devtools/app-manager/default-app-icon.png (../shared/devtools/app-manager/images/default-app-icon.png)
skin/classic/browser/devtools/search-clear-failed.svg (../shared/devtools/images/search-clear-failed.svg)
skin/classic/browser/devtools/search-clear-light.svg (../shared/devtools/images/search-clear-light.svg)
skin/classic/browser/devtools/search-clear-dark.svg (../shared/devtools/images/search-clear-dark.svg)
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/sync-16.png
skin/classic/browser/sync-32.png

View File

@ -536,7 +536,9 @@ browser.jar:
skin/classic/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)
skin/classic/browser/devtools/app-manager/default-app-icon.png (../shared/devtools/app-manager/images/default-app-icon.png)
skin/classic/browser/devtools/search-clear-failed.svg (../shared/devtools/images/search-clear-failed.svg)
skin/classic/browser/devtools/search-clear-light.svg (../shared/devtools/images/search-clear-light.svg)
skin/classic/browser/devtools/search-clear-dark.svg (../shared/devtools/images/search-clear-dark.svg)
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/sync-16.png
skin/classic/browser/sync-32.png

View File

@ -153,7 +153,6 @@ body {
#root .devtools-toolbar {
width: 100%;
border-bottom-width: 0;
}
.link {

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0"
y="0"
width="32"
height="16"
viewBox="0 0 32 16">
<defs>
<path id="glyphShape-clear" d="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0 z M11.9,10.5l-1.4,1.4L8,9.4l-2.4,2.4l-1.4-1.4L6.6,8L4.2,5.6l1.4-1.4L8,6.6l2.4-2.4l1.4,1.4L9.4,8L11.9,10.5z"/>
<style type="text/css">
.icon-state-default { fill: #f5f7fa; fill-opacity: .6; }
.icon-state-pressed { fill: #7d7e80; fill-opacity: .8; }
</style>
</defs>
<use xlink:href="#glyphShape-clear" class="icon-state-default" />
<use xlink:href="#glyphShape-clear" class="icon-state-pressed" transform="translate(16)" />
</svg>

After

Width:  |  Height:  |  Size: 985 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0"
y="0"
width="32"
height="16"
viewBox="0 0 32 16">
<defs>
<path id="glyphShape-clear" d="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0 z M11.9,10.5l-1.4,1.4L8,9.4l-2.4,2.4l-1.4-1.4L6.6,8L4.2,5.6l1.4-1.4L8,6.6l2.4-2.4l1.4,1.4L9.4,8L11.9,10.5z"/>
<style type="text/css">
.icon-state-default { fill: #cc3d3d; fill-opacity: 1; }
.icon-state-pressed { fill: #802d2d; fill-opacity: 1; }
</style>
</defs>
<use xlink:href="#glyphShape-clear" class="icon-state-default" />
<use xlink:href="#glyphShape-clear" class="icon-state-pressed" transform="translate(16)" />
</svg>

After

Width:  |  Height:  |  Size: 983 B

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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/. -->
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0"
y="0"
width="32"
height="16"
viewBox="0 0 32 16">
<defs>
<path id="glyphShape-clear" d="M8,0C3.6,0,0,3.6,0,8c0,4.4,3.6,8,8,8s8-3.6,8-8C16,3.6,12.4,0,8,0 z M11.9,10.5l-1.4,1.4L8,9.4l-2.4,2.4l-1.4-1.4L6.6,8L4.2,5.6l1.4-1.4L8,6.6l2.4-2.4l1.4,1.4L9.4,8L11.9,10.5z"/>
<style type="text/css">
.icon-state-default { fill: #1d2126; fill-opacity: .5; }
.icon-state-pressed { fill: #1d2126; fill-opacity: .8; }
</style>
</defs>
<use xlink:href="#glyphShape-clear" class="icon-state-default" />
<use xlink:href="#glyphShape-clear" class="icon-state-pressed" transform="translate(16)" />
</svg>

After

Width:  |  Height:  |  Size: 985 B

View File

@ -2,6 +2,15 @@
* 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/. */
/* CSS Variables specific to this panel that aren't defined by the themes */
.theme-light {
--rule-highlight-background-color: #ffee99;
}
.theme-dark {
--rule-highlight-background-color: #594724;
}
.ruleview {
height: 100%;
}
@ -218,6 +227,10 @@
border-left-color: var(--theme-highlight-green);
}
.ruleview-highlight {
background-color: var(--rule-highlight-background-color);
}
.ruleview-namecontainer > .ruleview-propertyname,
.ruleview-propertycontainer > .ruleview-propertyvalue {
border-bottom: 1px dashed transparent;

View File

@ -9,6 +9,21 @@
%define solidSeparatorDark linear-gradient(#2d5b7d, #2d5b7d)
%define solidSeparatorLight linear-gradient(#aaa, #aaa)
/* CSS Variables specific to the devtools toolbar that aren't defined by the themes */
.theme-light {
--searchbox-background-color: #ffee99;
--searchbox-border-color: #ffbf00;
--searcbox-no-match-background-color: #ffe5e5;
--searcbox-no-match-border-color: #e52e2e;
}
.theme-dark {
--searchbox-background-color: #4d4222;
--searchbox-border-color: #d99f2b;
--searcbox-no-match-background-color: #402325;
--searcbox-no-match-border-color: #cc3d3d;
}
/* Toolbars */
.devtools-toolbar,
.devtools-sidebar-tabs tabs,
@ -327,7 +342,7 @@
margin-bottom: 1px;
padding: 0;
-moz-padding-start: 22px;
-moz-padding-end: 12px;
-moz-padding-end: 4px;
background-position: 8px center;
background-size: 11px 11px;
background-repeat: no-repeat;
@ -342,6 +357,85 @@
background-image: url(magnifying-glass-light.png);
}
.devtools-searchinput:-moz-locale-dir(rtl) {
background-position: calc(100% - 8px) center;
}
.devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-icon {
visibility: hidden;
}
/* Searchbox is a div container element for a search input element */
.devtools-searchbox {
display: -moz-box;
-moz-box-flex: 1;
position: relative;
}
.devtools-rule-searchbox {
-moz-box-flex: 1;
padding-right: 23px;
width: 100%;
font: inherit;
}
.devtools-rule-searchbox[filled] {
background-color: var(--searchbox-background-color);
border-color: var(--searchbox-border-color);
}
.devtools-style-searchbox-no-match {
background-color: var(--searcbox-no-match-background-color) !important;
border-color: var(--searcbox-no-match-border-color) !important;
}
.devtools-no-search-result {
border-color: var(--theme-highlight-red) !important;
}
.devtools-searchinput-clear {
position: absolute;
top: 3.5px;
right: 7px;
padding: 0;
border: 0;
width: 16px;
height: 16px;
background-position: 0 0;
background-repeat: no-repeat;
background-color: transparent;
}
.theme-dark .devtools-searchinput-clear {
background-image: url("chrome://browser/skin/devtools/search-clear-dark.svg");
}
.theme-light .devtools-searchinput-clear {
background-image: url("chrome://browser/skin/devtools/search-clear-light.svg");
}
.devtools-style-searchbox-no-match + .devtools-searchinput-clear {
background-image: url("chrome://browser/skin/devtools/search-clear-failed.svg") !important;
}
.devtools-searchinput-clear:hover {
background-position: -16px 0;
}
.theme-dark .devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
list-style-image: url("chrome://browser/skin/devtools/search-clear-dark.svg");
-moz-image-region: rect(0, 16px, 16px, 0);
}
.theme-light .devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear {
list-style-image: url("chrome://browser/skin/devtools/search-clear-light.svg");
-moz-image-region: rect(0, 16px, 16px, 0);
}
.devtools-searchinput > .textbox-input-box > .textbox-search-icons > .textbox-search-clear:hover {
-moz-image-region: rect(0, 32px, 16px, 16px);
}
@media (min-resolution: 2dppx) {
.theme-dark .devtools-searchinput {
background-image: url(magnifying-glass@2x.png);
@ -352,18 +446,6 @@
}
}
.devtools-searchinput:-moz-locale-dir(rtl) {
background-position: calc(100% - 8px) center;
}
.devtools-searchinput > .textbox-input-box > .textbox-search-icons {
display: none;
}
.devtools-no-search-result {
border-color: var(--theme-highlight-red) !important;
}
/* Close button */
.devtools-closebutton {

View File

@ -484,7 +484,9 @@ browser.jar:
skin/classic/browser/devtools/app-manager/rocket.svg (../shared/devtools/app-manager/images/rocket.svg)
skin/classic/browser/devtools/app-manager/noise.png (../shared/devtools/app-manager/images/noise.png)
skin/classic/browser/devtools/app-manager/default-app-icon.png (../shared/devtools/app-manager/images/default-app-icon.png)
skin/classic/browser/devtools/search-clear-failed.svg (../shared/devtools/images/search-clear-failed.svg)
skin/classic/browser/devtools/search-clear-light.svg (../shared/devtools/images/search-clear-light.svg)
skin/classic/browser/devtools/search-clear-dark.svg (../shared/devtools/images/search-clear-dark.svg)
#ifdef MOZ_SERVICES_SYNC
skin/classic/browser/sync-16.png
skin/classic/browser/sync-32.png