зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1306054 - Use custom tooltip for inactive properties r=jdescottes,flod,rcaliman
### Changes Probably the most important change apart from the tooltips is that we now only support one property at a time. This allows us to short circuit at the first invalid property and improve performance. This was previously agreed with Razvan but there were some relics left in the code. `toolbox.xul` - Added tooltips.ftl `devtools/client/inspector/markup/test/helper_events_test_runner.js`: - Had to change to synthesizeMouseAtCenter because CSS changes caused the original to fail. `devtools/client/inspector/rules/rules.js`: - Added `VIEW_NODE_INACTIVE_CSS` to node types and sorted alphabetically. - Added new nodeInfo data for Inactive CSS icons. `devtools/client/inspector/rules/test/browser_rules_inactive_css_flexbox.js` & `devtools/client/inspector/rules/test/browser_rules_inactive_css_grid.js`: - removed some listeners that are no longer needed `devtools/client/inspector/rules/test/head.js`: - Refactored `getPropertiesForRuleIndex()` in order to pass along information needed for testing our Fluent strings. - Refactored `checkDeclarationIsInactive()` to check tooltip contnts using a new method. - Added `checkInteractiveTooltip()` for checking the tooltip contents themselves. - Simple changes to `runInactiveCSSTests()`. `devtools/client/inspector/rules/views/text-property-editor.js`: - We no longer create the tooltip by adding the title attribute. `devtools/client/inspector/shared/node-types.js`: - Changed the enum to use strings to simplify debugging. - Added `VIEW_NODE_INACTIVE_CSS`. - Sorted alphabetically. `devtools/client/inspector/shared/tooltips-overlay.js`: - Introduced a new tooltip type called `interactiveTooltip`. `devtools/client/locales/en-US/inspector.properties`: - Removed strings. `devtools/client/locales/en-US/tooltips.ftl`: - Added structured versions of the properties from `inspector.properties`. `devtools/client/shared/widgets/tooltip/HTMLTooltip.js`: - Made the tooltips obey the "prevent popup autohide" option in the browser debugger. `devtools/client/shared/widgets/tooltip/InactiveCSSTooltipHelper.js`: - Main file for handling InactiveCSS Tooltips. `devtools/client/themes/tooltips.css`: - Made arrow tooltips follow the Proton theme. `devtools/server/actors/utils/inactive-property-helper.js`: - General changes to support Fluent. - Bail on first inactive property found. ### Latest Try (expecting green) https://treeherder.mozilla.org/#/jobs?repo=try&revision=de28939206d444dc4b534a3e5cc7a84b8797bec3 Differential Revision: https://phabricator.services.mozilla.com/D29372 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
31429a113a
Коммит
a58a67412c
|
@ -21,6 +21,8 @@ add_task(async function() {
|
|||
// available in the toolbox top window.
|
||||
toolbox.topWindow.MozXULElement.insertFTLIfNeeded("toolkit/main-window/editmenu.ftl");
|
||||
|
||||
loadFTL(toolbox, "toolkit/main-window/editmenu.ftl");
|
||||
|
||||
await testMenuItems();
|
||||
await testMenuPopup(toolbox);
|
||||
await testSubmenu(toolbox);
|
||||
|
|
|
@ -427,3 +427,19 @@ function setupPreferencesForBrowserToolbox() {
|
|||
|
||||
return SpecialPowers.pushPrefEnv(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load FTL.
|
||||
*
|
||||
* @param {Toolbox} toolbox
|
||||
* Toolbox instance.
|
||||
* @param {String} path
|
||||
* Path to the FTL file.
|
||||
*/
|
||||
function loadFTL(toolbox, path) {
|
||||
const win = toolbox.doc.ownerGlobal;
|
||||
|
||||
if (win.MozXULElement) {
|
||||
win.MozXULElement.insertFTLIfNeeded(path);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@
|
|||
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
|
||||
xmlns:html="http://www.w3.org/1999/xhtml">
|
||||
<linkset>
|
||||
<html:link rel="localization" href="devtools/tooltips.ftl"/>
|
||||
</linkset>
|
||||
|
||||
<html:link href="chrome://browser/skin/window.svg" rel="shortcut icon"/>
|
||||
<script src="chrome://devtools/content/shared/theme-switching.js"/>
|
||||
|
|
|
@ -16,15 +16,16 @@ const {PrefObserver} = require("devtools/client/shared/prefs");
|
|||
const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
|
||||
const RuleEditor = require("devtools/client/inspector/rules/views/rule-editor");
|
||||
const {
|
||||
VIEW_NODE_SELECTOR_TYPE,
|
||||
VIEW_NODE_PROPERTY_TYPE,
|
||||
VIEW_NODE_VALUE_TYPE,
|
||||
VIEW_NODE_FONT_TYPE,
|
||||
VIEW_NODE_IMAGE_URL_TYPE,
|
||||
VIEW_NODE_INACTIVE_CSS,
|
||||
VIEW_NODE_LOCATION_TYPE,
|
||||
VIEW_NODE_PROPERTY_TYPE,
|
||||
VIEW_NODE_SELECTOR_TYPE,
|
||||
VIEW_NODE_SHAPE_POINT_TYPE,
|
||||
VIEW_NODE_SHAPE_SWATCH,
|
||||
VIEW_NODE_VALUE_TYPE,
|
||||
VIEW_NODE_VARIABLE_TYPE,
|
||||
VIEW_NODE_FONT_TYPE,
|
||||
} = require("devtools/client/inspector/shared/node-types");
|
||||
const TooltipsOverlay = require("devtools/client/inspector/shared/tooltips-overlay");
|
||||
const {createChild, promiseWarn} = require("devtools/client/inspector/shared/utils");
|
||||
|
@ -425,6 +426,9 @@ CssRuleView.prototype = {
|
|||
toggleActive: getShapeToggleActive(node),
|
||||
point: getShapePoint(node),
|
||||
};
|
||||
} else if (classes.contains("ruleview-unused-warning") && prop) {
|
||||
type = VIEW_NODE_INACTIVE_CSS;
|
||||
value = prop.isUsed();
|
||||
} else if (classes.contains("ruleview-shapeswatch") && prop) {
|
||||
type = VIEW_NODE_SHAPE_SWATCH;
|
||||
value = {
|
||||
|
|
|
@ -44,7 +44,6 @@ const BEFORE = [
|
|||
selector: "#self-aligned",
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.grid.or.flex.item",
|
||||
declaration: {
|
||||
"align-self": "stretch",
|
||||
},
|
||||
|
@ -72,7 +71,6 @@ const BEFORE = [
|
|||
],
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.container",
|
||||
declaration: {
|
||||
"flex-direction": "row",
|
||||
},
|
||||
|
@ -101,14 +99,12 @@ const BEFORE = [
|
|||
],
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.item",
|
||||
declaration: {
|
||||
"order": "1",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
],
|
||||
waitFor: "inspector-updated",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -117,35 +113,30 @@ const AFTER = [
|
|||
selector: ".item-2",
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.item",
|
||||
declaration: {
|
||||
"order": "2",
|
||||
},
|
||||
ruleIndex: 0,
|
||||
},
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.item",
|
||||
declaration: {
|
||||
"flex-basis": "auto",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.item",
|
||||
declaration: {
|
||||
"flex-grow": "1",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.item",
|
||||
declaration: {
|
||||
"flex-shrink": "1",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.container",
|
||||
declaration: {
|
||||
"flex-direction": "row",
|
||||
},
|
||||
|
@ -165,6 +156,5 @@ add_task(async function() {
|
|||
await toggleDeclaration(inspector, view, 0, {
|
||||
display: "flex",
|
||||
});
|
||||
await view.once("ruleview-refreshed");
|
||||
await runInactiveCSSTests(view, inspector, AFTER);
|
||||
});
|
||||
|
|
|
@ -47,7 +47,6 @@ const BEFORE = [
|
|||
selector: "#self-aligned",
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.grid.or.flex.item",
|
||||
declaration: {
|
||||
"align-self": "stretch",
|
||||
},
|
||||
|
@ -70,7 +69,6 @@ const BEFORE = [
|
|||
],
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.flex.container",
|
||||
declaration: {
|
||||
"flex-direction": "row",
|
||||
},
|
||||
|
@ -100,14 +98,12 @@ const BEFORE = [
|
|||
],
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.grid.or.flex.item",
|
||||
declaration: {
|
||||
"align-self": "start",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
],
|
||||
waitFor: "inspector-updated",
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -131,21 +127,18 @@ const AFTER = [
|
|||
],
|
||||
inactiveDeclarations: [
|
||||
{
|
||||
l10n: "rule.inactive.css.not.grid.container",
|
||||
declaration: {
|
||||
"column-gap": "10px",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
{
|
||||
l10n: "rule.inactive.css.not.grid.container",
|
||||
declaration: {
|
||||
"row-gap": "10px",
|
||||
},
|
||||
ruleIndex: 1,
|
||||
},
|
||||
{
|
||||
l10n: "rule.inactive.css.not.grid.or.flex.item",
|
||||
declaration: {
|
||||
"align-self": "start",
|
||||
},
|
||||
|
|
|
@ -622,7 +622,7 @@ async function openEyedropper(view, swatch) {
|
|||
* {
|
||||
* propertyName: "color",
|
||||
* propertyValue: "red",
|
||||
* warnings: "This won't work",
|
||||
* warning: "This won't work",
|
||||
* used: true,
|
||||
* }
|
||||
* },
|
||||
|
@ -635,12 +635,15 @@ function getPropertiesForRuleIndex(view, ruleIndex) {
|
|||
|
||||
for (const currProp of ruleEditor.rule.textProps) {
|
||||
const icon = currProp.editor.unusedState;
|
||||
const unused = currProp.editor.element.classList.contains("unused");
|
||||
|
||||
declaration.set(`${currProp.name}:${currProp.value}`, {
|
||||
propertyName: currProp.name,
|
||||
propertyValue: currProp.value,
|
||||
warnings: icon.title ? icon.title.split("\n") : [],
|
||||
used: !currProp.editor.element.classList.contains("unused"),
|
||||
icon: icon,
|
||||
data: currProp.isUsed(),
|
||||
warning: unused,
|
||||
used: !unused,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -679,6 +682,7 @@ async function toggleDeclaration(inspector, view, ruleIndex, declaration) {
|
|||
info(`Toggling declaration "${dec}" of rule ${ruleIndex} to ${newStatus}`);
|
||||
|
||||
await togglePropStatus(view, textProp);
|
||||
info("Toggled successfully.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -691,20 +695,17 @@ async function toggleDeclaration(inspector, view, ruleIndex, declaration) {
|
|||
* The index we expect the rule to have in the rule-view.
|
||||
* @param {Object} declaration
|
||||
* An object representing the declaration e.g. { color: "red" }.
|
||||
* @param {String} warningL10nString
|
||||
* l10n string representing an expected warning.
|
||||
*/
|
||||
function checkDeclarationIsInactive(view, ruleIndex, declaration, warningL10nString) {
|
||||
async function checkDeclarationIsInactive(view, ruleIndex, declaration) {
|
||||
const declarations = getPropertiesForRuleIndex(view, ruleIndex);
|
||||
const [[ name, value ]] = Object.entries(declaration);
|
||||
const dec = `${name}:${value}`;
|
||||
const { used, warnings } = declarations.get(dec);
|
||||
const { used, warning } = declarations.get(dec);
|
||||
|
||||
ok(!used, `"${dec}" is inactive`);
|
||||
is(warnings.length, 1, `"${dec}" has a warning`);
|
||||
ok(warning, `"${dec}" has a warning`);
|
||||
|
||||
const warning = INSPECTOR_L10N.getFormatStr(warningL10nString, name);
|
||||
is(warnings[0], warning, `The warning on "${dec}" is correct`);
|
||||
await checkInteractiveTooltip(view, ruleIndex, declaration);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -721,10 +722,59 @@ function checkDeclarationIsActive(view, ruleIndex, declaration) {
|
|||
const declarations = getPropertiesForRuleIndex(view, ruleIndex);
|
||||
const [[ name, value ]] = Object.entries(declaration);
|
||||
const dec = `${name}:${value}`;
|
||||
const { used, warnings } = declarations.get(dec);
|
||||
const { used, warning } = declarations.get(dec);
|
||||
|
||||
ok(used, `${dec} is active`);
|
||||
is(warnings.length, 0, `${dec} has no warnings`);
|
||||
ok(!warning, `${dec} has no warning`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that a tooltip contains the correct value.
|
||||
*
|
||||
* @param {ruleView} view
|
||||
* The rule-view instance.
|
||||
* @param {Number} ruleIndex
|
||||
* The index we expect the rule to have in the rule-view.
|
||||
* @param {Object} declaration
|
||||
* An object representing the declaration e.g. { color: "red" }.
|
||||
*/
|
||||
async function checkInteractiveTooltip(view, ruleIndex, declaration) {
|
||||
// Get the declaration
|
||||
const declarations = getPropertiesForRuleIndex(view, ruleIndex);
|
||||
const [[ name, value ]] = Object.entries(declaration);
|
||||
const dec = `${name}:${value}`;
|
||||
const { icon, data } = declarations.get(dec);
|
||||
|
||||
// Get the tooltip.
|
||||
const tooltip = view.tooltips.getTooltip("interactiveTooltip");
|
||||
|
||||
// Get the HTML template.
|
||||
const inactiveCssTooltipHelper = view.tooltips.inactiveCssTooltipHelper;
|
||||
const template = inactiveCssTooltipHelper.getTemplate(data, tooltip);
|
||||
|
||||
// Translate the template using Fluent.
|
||||
const { doc } = tooltip;
|
||||
await doc.l10n.translateFragment(template);
|
||||
|
||||
// Get the expected HTML content of the now translated template.
|
||||
const expected = template.firstElementChild.outerHTML;
|
||||
|
||||
// Show the tooltip for the correct icon.
|
||||
const onTooltipReady = tooltip.once("shown");
|
||||
await view.tooltips.onInteractiveTooltipTargetHover(icon);
|
||||
tooltip.show(icon);
|
||||
await onTooltipReady;
|
||||
|
||||
// Get the tooltip's actual HTML content.
|
||||
const actual = tooltip.panel.firstElementChild.outerHTML;
|
||||
|
||||
// Hide the tooltip.
|
||||
const onTooltipHidden = tooltip.once("hidden");
|
||||
tooltip.hide();
|
||||
await onTooltipHidden;
|
||||
|
||||
// Finally, check the values.
|
||||
is(actual, expected, "Tooltip contains the correct value.");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -757,36 +807,25 @@ function checkDeclarationIsActive(view, ruleIndex, declaration) {
|
|||
* ],
|
||||
* inactiveDeclarations: [
|
||||
* {
|
||||
* l10n: "rule.inactive.css.not.flex.container",
|
||||
* declaration: {
|
||||
* "flex-direction": "row",
|
||||
* },
|
||||
* ruleIndex: 1,
|
||||
* },
|
||||
* ],
|
||||
* waitFor: "markupmutation",
|
||||
* },
|
||||
* ...
|
||||
* ]
|
||||
*/
|
||||
async function runInactiveCSSTests(view, inspector, tests) {
|
||||
for (const test of tests) {
|
||||
let event = null;
|
||||
|
||||
if (test.waitFor) {
|
||||
event = inspector.once(test.waitFor);
|
||||
}
|
||||
|
||||
if (test.selector) {
|
||||
await selectNode(test.selector, inspector);
|
||||
}
|
||||
|
||||
if (test.waitFor) {
|
||||
await event;
|
||||
}
|
||||
|
||||
if (test.activeDeclarations) {
|
||||
// Check whether declarations are marked as used.
|
||||
info("Checking whether declarations are marked as used.");
|
||||
|
||||
for (const activeDeclarations of test.activeDeclarations) {
|
||||
for (const [name, value] of Object.entries(activeDeclarations.declarations)) {
|
||||
checkDeclarationIsActive(view, activeDeclarations.ruleIndex, {
|
||||
|
@ -797,12 +836,12 @@ async function runInactiveCSSTests(view, inspector, tests) {
|
|||
}
|
||||
|
||||
if (test.inactiveDeclarations) {
|
||||
info("Checking that declarations are unused and have a warning.");
|
||||
|
||||
for (const inactiveDeclaration of test.inactiveDeclarations) {
|
||||
// Check that declaration is unused and has a warning.
|
||||
checkDeclarationIsInactive(view,
|
||||
inactiveDeclaration.ruleIndex,
|
||||
inactiveDeclaration.declaration,
|
||||
inactiveDeclaration.l10n);
|
||||
await checkDeclarationIsInactive(view,
|
||||
inactiveDeclaration.ruleIndex,
|
||||
inactiveDeclaration.declaration);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -634,14 +634,13 @@ TextPropertyEditor.prototype = {
|
|||
this.element.classList.remove("ruleview-overridden");
|
||||
}
|
||||
|
||||
const { used, reasons } = this.prop.isUsed();
|
||||
const { used } = this.prop.isUsed();
|
||||
|
||||
if (this.editing || this.prop.overridden || !this.prop.enabled || used) {
|
||||
this.element.classList.remove("unused");
|
||||
this.unusedState.hidden = true;
|
||||
} else {
|
||||
this.element.classList.add("unused");
|
||||
this.unusedState.title = reasons.join("\n");
|
||||
this.unusedState.hidden = false;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -10,12 +10,13 @@
|
|||
* Types of nodes used in the rule and computed view.
|
||||
*/
|
||||
|
||||
exports.VIEW_NODE_SELECTOR_TYPE = 1;
|
||||
exports.VIEW_NODE_PROPERTY_TYPE = 2;
|
||||
exports.VIEW_NODE_VALUE_TYPE = 3;
|
||||
exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
|
||||
exports.VIEW_NODE_LOCATION_TYPE = 5;
|
||||
exports.VIEW_NODE_SHAPE_POINT_TYPE = 6;
|
||||
exports.VIEW_NODE_VARIABLE_TYPE = 7;
|
||||
exports.VIEW_NODE_FONT_TYPE = 8;
|
||||
exports.VIEW_NODE_SHAPE_SWATCH = 9;
|
||||
exports.VIEW_NODE_FONT_TYPE = "font-type";
|
||||
exports.VIEW_NODE_IMAGE_URL_TYPE = "image-url-type";
|
||||
exports.VIEW_NODE_INACTIVE_CSS = "inactive-css";
|
||||
exports.VIEW_NODE_LOCATION_TYPE = "location-type";
|
||||
exports.VIEW_NODE_PROPERTY_TYPE = "property-type";
|
||||
exports.VIEW_NODE_SELECTOR_TYPE = "selector-type";
|
||||
exports.VIEW_NODE_SHAPE_POINT_TYPE = "shape-point-type";
|
||||
exports.VIEW_NODE_SHAPE_SWATCH = "shape-swatch";
|
||||
exports.VIEW_NODE_VALUE_TYPE = "value-type";
|
||||
exports.VIEW_NODE_VARIABLE_TYPE = "variable-type";
|
||||
|
|
|
@ -17,6 +17,7 @@ const {
|
|||
VIEW_NODE_VALUE_TYPE,
|
||||
VIEW_NODE_FONT_TYPE,
|
||||
VIEW_NODE_IMAGE_URL_TYPE,
|
||||
VIEW_NODE_INACTIVE_CSS,
|
||||
VIEW_NODE_VARIABLE_TYPE,
|
||||
} = require("devtools/client/inspector/shared/node-types");
|
||||
|
||||
|
@ -32,12 +33,15 @@ loader.lazyRequireGetter(this, "setBrokenImageTooltip",
|
|||
"devtools/client/shared/widgets/tooltip/ImageTooltipHelper", true);
|
||||
loader.lazyRequireGetter(this, "setVariableTooltip",
|
||||
"devtools/client/shared/widgets/tooltip/VariableTooltipHelper", true);
|
||||
loader.lazyRequireGetter(this, "InactiveCssTooltipHelper",
|
||||
"devtools/client/shared/widgets/tooltip/inactive-css-tooltip-helper", false);
|
||||
|
||||
const PREF_IMAGE_TOOLTIP_SIZE = "devtools.inspector.imagePreviewTooltipSize";
|
||||
|
||||
// Types of existing tooltips
|
||||
const TOOLTIP_IMAGE_TYPE = "image";
|
||||
const TOOLTIP_FONTFAMILY_TYPE = "font-family";
|
||||
const TOOLTIP_INACTIVE_CSS = "inactive-css";
|
||||
const TOOLTIP_VARIABLE_TYPE = "variable";
|
||||
|
||||
/**
|
||||
|
@ -81,16 +85,21 @@ TooltipsOverlay.prototype = {
|
|||
|
||||
this._isStarted = true;
|
||||
|
||||
// Instantiate the preview tooltip when the rule/computed view is hovered over in
|
||||
// order to call tooltip.starTogglingOnHover. This will allow the preview tooltip
|
||||
// to be shown when an appropriate element is hovered over.
|
||||
if (flags.testing) {
|
||||
this.getTooltip("previewTooltip");
|
||||
} else {
|
||||
// Lazily get the preview tooltip to avoid loading HTMLTooltip.
|
||||
this.view.element.addEventListener("mousemove", () => {
|
||||
this.getTooltip("previewTooltip");
|
||||
}, { once: true });
|
||||
this.inactiveCssTooltipHelper = new InactiveCssTooltipHelper();
|
||||
|
||||
// Instantiate the interactiveTooltip and preview tooltip when the
|
||||
// rule/computed view is hovered over in order to call
|
||||
// `tooltip.startTogglingOnHover`. This will allow the tooltip to be shown
|
||||
// when an appropriate element is hovered over.
|
||||
for (const type of ["interactiveTooltip", "previewTooltip"]) {
|
||||
if (flags.testing) {
|
||||
this.getTooltip(type);
|
||||
} else {
|
||||
// Lazily get the preview tooltip to avoid loading HTMLTooltip.
|
||||
this.view.element.addEventListener("mousemove", () => {
|
||||
this.getTooltip(type);
|
||||
}, { once: true });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -126,6 +135,16 @@ TooltipsOverlay.prototype = {
|
|||
tooltip = new SwatchFilterTooltip(doc,
|
||||
this._cssProperties.getValidityChecker(this.view.inspector.panelDoc));
|
||||
break;
|
||||
case "interactiveTooltip":
|
||||
tooltip = new HTMLTooltip(doc, {
|
||||
type: "doorhanger",
|
||||
useXulWrapper: true,
|
||||
});
|
||||
tooltip.startTogglingOnHover(this.view.element,
|
||||
this.onInteractiveTooltipTargetHover.bind(this), {
|
||||
interactive: true,
|
||||
});
|
||||
break;
|
||||
case "previewTooltip":
|
||||
tooltip = new HTMLTooltip(doc, {
|
||||
type: "arrow",
|
||||
|
@ -154,6 +173,8 @@ TooltipsOverlay.prototype = {
|
|||
tooltip.destroy();
|
||||
}
|
||||
|
||||
this.inactiveCssTooltipHelper.destroy();
|
||||
|
||||
this._isStarted = false;
|
||||
},
|
||||
|
||||
|
@ -181,6 +202,11 @@ TooltipsOverlay.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
// Inactive CSS tooltip
|
||||
if (type === VIEW_NODE_INACTIVE_CSS) {
|
||||
tooltipType = TOOLTIP_INACTIVE_CSS;
|
||||
}
|
||||
|
||||
// Variable preview tooltip
|
||||
if (type === VIEW_NODE_VARIABLE_TYPE) {
|
||||
tooltipType = TOOLTIP_VARIABLE_TYPE;
|
||||
|
@ -252,6 +278,54 @@ TooltipsOverlay.prototype = {
|
|||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Executed by the tooltip when the pointer hovers over an element of the
|
||||
* view. Used to decide whether the tooltip should be shown or not and to
|
||||
* actually put content in it.
|
||||
* Checks if the hovered target is a css value we support tooltips for.
|
||||
*
|
||||
* @param {DOMNode} target
|
||||
* The currently hovered node
|
||||
* @return {Boolean}
|
||||
* true if shown, false otherwise.
|
||||
*/
|
||||
async onInteractiveTooltipTargetHover(target) {
|
||||
const nodeInfo = this.view.getNodeInfo(target);
|
||||
if (!nodeInfo) {
|
||||
// The hovered node isn't something we care about.
|
||||
return false;
|
||||
}
|
||||
|
||||
const type = this._getTooltipType(nodeInfo);
|
||||
if (!type) {
|
||||
// There is no tooltip type defined for the hovered node.
|
||||
return false;
|
||||
}
|
||||
|
||||
// Remove previous tooltip instances.
|
||||
for (const [, tooltip] of this._instances) {
|
||||
if (tooltip.isVisible()) {
|
||||
if (tooltip.revert) {
|
||||
tooltip.revert();
|
||||
}
|
||||
tooltip.hide();
|
||||
}
|
||||
}
|
||||
|
||||
if (type === TOOLTIP_INACTIVE_CSS) {
|
||||
// Ensure this is the correct node and not a parent.
|
||||
if (!target.classList.contains("ruleview-unused-warning")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.inactiveCssTooltipHelper.setContent(
|
||||
nodeInfo.value, this.getTooltip("interactiveTooltip"));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Set the content of the preview tooltip to display an image preview. The image URL can
|
||||
* be relative, a call will be made to the debuggee to retrieve the image content as an
|
||||
|
|
|
@ -488,33 +488,3 @@ markupView.scrollableBadge.tooltip=This element has scrollable overflow.
|
|||
# LOCALIZATION NOTE (rulePreviewTooltip.noAssociatedRule): This is the text displayed inside
|
||||
# the RulePreviewTooltip when a rule cannot be found for a CSS property declaration.
|
||||
rulePreviewTooltip.noAssociatedRule=No associated rule
|
||||
|
||||
# LOCALIZATION NOTE (rule.inactive.css.not.flex.container): These properties
|
||||
# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
|
||||
# property is not active. %S will be replaced with a property name.
|
||||
rule.inactive.css.not.flex.container=“%S” has no effect on this element since it’s not a flex container (try adding “display:flex” or “display:inline-flex”)
|
||||
|
||||
# LOCALIZATION NOTE (rule.inactive.css.not.flex.item): These properties
|
||||
# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
|
||||
# property is not active. %S will be replaced with a property name.
|
||||
rule.inactive.css.not.flex.item=“%S” has no effect on this element since it’s not a flex item (try adding “display:flex” or “display:inline-flex” to the item’s parent)
|
||||
|
||||
# LOCALIZATION NOTE (rule.inactive.css.not.grid.container): These properties
|
||||
# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
|
||||
# property is not active. %S will be replaced with a property name.
|
||||
rule.inactive.css.not.grid.container=“%S” has no effect on this element since it’s not a grid container (try adding “display:grid” or “display:inline-grid”)
|
||||
|
||||
# LOCALIZATION NOTE (rule.inactive.css.not.grid.item): These properties
|
||||
# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
|
||||
# property is not active. %S will be replaced with a property name.
|
||||
rule.inactive.css.not.grid.item=“%S” has no effect on this element since it’s not a grid item (try adding “display:grid” or “display:inline-grid” to the item’s parent)
|
||||
|
||||
# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.item): These properties
|
||||
# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
|
||||
# property is not active. %S will be replaced with a property name.
|
||||
rule.inactive.css.not.grid.or.flex.item=“%S” has no effect on this element since it’s not a grid or flex item (try adding “display:grid”, “display:flex”, “display:inline-grid” or “display:inline-flex” to the item’s parent)
|
||||
|
||||
# LOCALIZATION NOTE (rule.inactive.css.not.grid.or.flex.container): These properties
|
||||
# contain the text displayed inside the Rule View’s Inactive CSS Tooltip when a
|
||||
# property is not active. %S will be replaced with a property name.
|
||||
rule.inactive.css.not.grid.or.flex.container=“%S” has no effect on this element since it’s neither a flex container nor a grid container (try adding “display:grid” or “display:flex”)
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
# 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/.
|
||||
|
||||
### Localization for Developer Tools tooltips.
|
||||
|
||||
learn-more = <span data-l10n-name="link">Learn more</span>
|
||||
|
||||
## In the Rule View when a CSS property cannot be successfully applied we display
|
||||
## an icon. When this icon is hovered this message is displayed to explain why
|
||||
## the property is not applied.
|
||||
## Variables:
|
||||
## $property (string) - A CSS property name e.g. "color".
|
||||
|
||||
inactive-css-not-grid-or-flex-container = <strong>{ $property }</strong> has no effect on this element since it’s neither a flex container nor a grid container.
|
||||
|
||||
inactive-css-not-grid-or-flex-item = <strong>{ $property }</strong> has no effect on this element since it’s not a grid or flex item.
|
||||
|
||||
inactive-css-not-grid-item = <strong>{ $property }</strong> has no effect on this element since it’s not a grid item.
|
||||
|
||||
inactive-css-not-grid-container = <strong>{ $property }</strong> has no effect on this element since it’s not a grid container.
|
||||
|
||||
inactive-css-not-flex-item = <strong>{ $property }</strong> has no effect on this element since it’s not a flex item.
|
||||
|
||||
inactive-css-not-flex-container = <strong>{ $property }</strong> has no effect on this element since it’s not a flex container.
|
||||
|
||||
## In the Rule View when a CSS property cannot be successfully applied we display
|
||||
## an icon. When this icon is hovered this message is displayed to explain how
|
||||
## the problem can be solved.
|
||||
|
||||
inactive-css-not-grid-or-flex-container-fix = Try adding <strong>display:grid</strong> or <strong>display:flex</strong>. { learn-more }
|
||||
|
||||
inactive-css-not-grid-or-flex-item-fix = Try adding <strong>display:grid</strong>, <strong>display:flex</strong>, <strong>display:inline-grid</strong> or <strong>display:inline-flex</strong>. { learn-more }
|
||||
|
||||
inactive-css-not-grid-item-fix =Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong> to the item’s parent. { learn-more }
|
||||
|
||||
inactive-css-not-grid-container-fix = Try adding <strong>display:grid</strong> or <strong>display:inline-grid</strong>. { learn-more }
|
||||
|
||||
inactive-css-not-flex-item-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong> to the item’s parent. { learn-more }
|
||||
|
||||
inactive-css-not-flex-container-fix = Try adding <strong>display:flex</strong> or <strong>display:inline-flex</strong>. { learn-more }
|
|
@ -721,6 +721,11 @@ HTMLTooltip.prototype = {
|
|||
* is hidden.
|
||||
*/
|
||||
async hide({ fromMouseup = false } = {}) {
|
||||
// Exit if the disable autohide setting is in effect.
|
||||
if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.doc.defaultView.clearTimeout(this.attachEventsTimer);
|
||||
if (!this.isVisible()) {
|
||||
this.emit("hidden");
|
||||
|
@ -826,11 +831,6 @@ HTMLTooltip.prototype = {
|
|||
return;
|
||||
}
|
||||
|
||||
// If the disable autohide setting is in effect, ignore.
|
||||
if (Services.prefs.getBoolPref("ui.popup.disable_autohide", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.hide({ fromMouseup: true });
|
||||
},
|
||||
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
loader.lazyRequireGetter(this, "openDocLink", "devtools/client/shared/link", true);
|
||||
|
||||
class InactiveCssTooltipHelper {
|
||||
constructor() {
|
||||
this.addTab = this.addTab.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fill the tooltip with inactive CSS information.
|
||||
*
|
||||
* @param {String} propertyName
|
||||
* The property name to be displayed in bold.
|
||||
* @param {String} text
|
||||
* The main text, which follows property name.
|
||||
*/
|
||||
async setContent(data, tooltip) {
|
||||
const fragment = this.getTemplate(data, tooltip);
|
||||
const { doc } = tooltip;
|
||||
|
||||
tooltip.panel.innerHTML = "";
|
||||
|
||||
tooltip.panel.addEventListener("click", this.addTab);
|
||||
tooltip.once("hidden", () => {
|
||||
tooltip.panel.removeEventListener("click", this.addTab);
|
||||
});
|
||||
|
||||
// Because Fluent is async we need to manually translate the fragment and
|
||||
// then insert it into the tooltip. This is needed in order for the tooltip
|
||||
// to size to the contents properly and for tests.
|
||||
await doc.l10n.translateFragment(fragment);
|
||||
doc.l10n.pauseObserving();
|
||||
tooltip.panel.appendChild(fragment);
|
||||
doc.l10n.resumeObserving();
|
||||
|
||||
// Size the content.
|
||||
tooltip.setContentSize({width: 275, height: Infinity});
|
||||
}
|
||||
/**
|
||||
* Get the template that the Fluent string will be merged with. This template
|
||||
* looks something like this but there is a variable amount of properties in the
|
||||
* fix section:
|
||||
*
|
||||
* <div class="devtools-tooltip-inactive-css">
|
||||
* <p data-l10n-id="inactive-css-not-grid-or-flex-container"
|
||||
* data-l10n-args="{"property":"align-content"}">
|
||||
* <strong></strong>
|
||||
* </p>
|
||||
* <p data-l10n-id="inactive-css-not-grid-or-flex-container-fix">
|
||||
* <strong></strong>
|
||||
* <strong></strong>
|
||||
* <span data-l10n-name="link" class="link"></span>
|
||||
* </p>
|
||||
* </div>
|
||||
*
|
||||
* @param {Object} data
|
||||
* An object in the following format: {
|
||||
* fixId: "inactive-css-not-grid-item-fix", // Fluent id containing the
|
||||
* // Inactive CSS fix.
|
||||
* msgId: "inactive-css-not-grid-item", // Fluent id containing the
|
||||
* // Inactive CSS message.
|
||||
* numFixProps: 2, // Number of properties in the fix section of the
|
||||
* // tooltip.
|
||||
* property: "color", // Property name
|
||||
* }
|
||||
* @param {HTMLTooltip} tooltip
|
||||
* The tooltip we are targetting.
|
||||
*/
|
||||
getTemplate(data, tooltip) {
|
||||
const XHTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
const { fixId, msgId, numFixProps, property } = data;
|
||||
const { doc } = tooltip;
|
||||
|
||||
this._currentTooltip = tooltip;
|
||||
this._currentUrl = `https://developer.mozilla.org/docs/Web/CSS/${property}`;
|
||||
|
||||
const templateNode = doc.createElementNS(XHTML_NS, "template");
|
||||
|
||||
// eslint-disable-next-line
|
||||
templateNode.innerHTML = `
|
||||
<div class="devtools-tooltip-inactive-css">
|
||||
<p data-l10n-id="${msgId}"
|
||||
data-l10n-args='${JSON.stringify({property})}'>
|
||||
<strong></strong>
|
||||
</p>
|
||||
<p data-l10n-id="${fixId}">
|
||||
${"<strong></strong>".repeat(numFixProps)}
|
||||
<span data-l10n-name="link" class="link"></span>
|
||||
</p>
|
||||
</div>`;
|
||||
|
||||
return doc.importNode(templateNode.content, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the tooltip, open `this._currentUrl` in a new tab and focus it.
|
||||
*
|
||||
* @param {DOMEvent} event
|
||||
* The click event originating from the tooltip.
|
||||
*
|
||||
*/
|
||||
addTab(event) {
|
||||
// The XUL panel swallows click events so handlers can't be added directly
|
||||
// to the link span. As a workaround we listen to all click events in the
|
||||
// panel and if a link span is clicked we proceed.
|
||||
if (event.target.className !== "link") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltip = this._currentTooltip;
|
||||
tooltip.hide();
|
||||
openDocLink(this._currentUrl);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this._currentTooltip = null;
|
||||
this._currentUrl = null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = InactiveCssTooltipHelper;
|
|
@ -8,6 +8,7 @@ DevToolsModules(
|
|||
'EventTooltipHelper.js',
|
||||
'HTMLTooltip.js',
|
||||
'ImageTooltipHelper.js',
|
||||
'inactive-css-tooltip-helper.js',
|
||||
'InlineTooltip.js',
|
||||
'RulePreviewTooltip.js',
|
||||
'SwatchBasedEditorTooltip.js',
|
||||
|
|
|
@ -67,6 +67,10 @@
|
|||
--theme-arrowpanel-separator: hsla(210,4%,10%,.14);
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Tooltip: CSS variables tooltip */
|
||||
|
||||
.devtools-tooltip-css-variable {
|
||||
|
@ -74,6 +78,24 @@
|
|||
padding: 2px;
|
||||
}
|
||||
|
||||
/* Tooltip: Inactive CSS tooltip */
|
||||
|
||||
.devtools-tooltip-inactive-css {
|
||||
color: var(--theme-body-color);
|
||||
padding: 7px 10px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.devtools-tooltip-inactive-css p {
|
||||
margin-block-start: 0;
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.devtools-tooltip-inactive-css .link {
|
||||
color: var(--theme-highlight-blue);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Tooltip: Tiles */
|
||||
|
||||
.devtools-tooltip-tiles {
|
||||
|
|
|
@ -7,11 +7,8 @@
|
|||
"use strict";
|
||||
|
||||
const Services = require("Services");
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
|
||||
const PREF_UNUSED_CSS_ENABLED = "devtools.inspector.inactive.css.enabled";
|
||||
const INSPECTOR_L10N =
|
||||
new LocalizationHelper("devtools/client/locales/inspector.properties");
|
||||
|
||||
class InactivePropertyHelper {
|
||||
/**
|
||||
|
@ -36,11 +33,14 @@ class InactivePropertyHelper {
|
|||
* when:
|
||||
* The rule itself, a JS function used to identify the conditions
|
||||
* indicating whether a property is valid or not.
|
||||
*
|
||||
* error:
|
||||
* A JS function that returns a custom error message explaining why the
|
||||
* property is inactive in this situation. This function takes a single
|
||||
* argument: the property name.
|
||||
* fixId:
|
||||
* A Fluent id containing a suggested solution to the problem that is
|
||||
* causing a property to be inactive.
|
||||
* msgId:
|
||||
* A Fluent id containing an error message explaining why a property is
|
||||
* inactive in this situation.
|
||||
* numFixProps:
|
||||
* The number of properties we suggest in the fixId string.
|
||||
* }
|
||||
*
|
||||
* NOTE: validProperties and invalidProperties are mutually exclusive.
|
||||
|
@ -58,7 +58,9 @@ class InactivePropertyHelper {
|
|||
"flex-wrap",
|
||||
],
|
||||
when: () => !this.flexContainer,
|
||||
error: property => msg("rule.inactive.css.not.flex.container", property),
|
||||
fixId: "inactive-css-not-flex-container-fix",
|
||||
msgId: "inactive-css-not-flex-container",
|
||||
numFixProps: 2,
|
||||
},
|
||||
// Flex item property used on non-flex item.
|
||||
{
|
||||
|
@ -70,7 +72,9 @@ class InactivePropertyHelper {
|
|||
"order",
|
||||
],
|
||||
when: () => !this.flexItem,
|
||||
error: property => msg("rule.inactive.css.not.flex.item", property),
|
||||
fixId: "inactive-css-not-flex-item-fix",
|
||||
msgId: "inactive-css-not-flex-item",
|
||||
numFixProps: 2,
|
||||
},
|
||||
// Grid container property used on non-grid container.
|
||||
{
|
||||
|
@ -85,7 +89,9 @@ class InactivePropertyHelper {
|
|||
"justify-items",
|
||||
],
|
||||
when: () => !this.gridContainer,
|
||||
error: property => msg("rule.inactive.css.not.grid.container", property),
|
||||
fixId: "inactive-css-not-grid-container-fix",
|
||||
msgId: "inactive-css-not-grid-container",
|
||||
numFixProps: 2,
|
||||
},
|
||||
// Grid item property used on non-grid item.
|
||||
{
|
||||
|
@ -100,7 +106,9 @@ class InactivePropertyHelper {
|
|||
"justify-self",
|
||||
],
|
||||
when: () => !this.gridItem,
|
||||
error: property => msg("rule.inactive.css.not.grid.item", property),
|
||||
fixId: "inactive-css-not-grid-item-fix",
|
||||
msgId: "inactive-css-not-grid-item",
|
||||
numFixProps: 2,
|
||||
},
|
||||
// Grid and flex item properties used on non-grid or non-flex item.
|
||||
{
|
||||
|
@ -108,7 +116,9 @@ class InactivePropertyHelper {
|
|||
"align-self",
|
||||
],
|
||||
when: () => !this.gridItem && !this.flexItem,
|
||||
error: property => msg("rule.inactive.css.not.grid.or.flex.item", property),
|
||||
fixId: "inactive-css-not-grid-or-flex-item-fix",
|
||||
msgId: "inactive-css-not-grid-or-flex-item",
|
||||
numFixProps: 4,
|
||||
},
|
||||
// Grid and flex container properties used on non-grid or non-flex container.
|
||||
{
|
||||
|
@ -118,7 +128,9 @@ class InactivePropertyHelper {
|
|||
"justify-content",
|
||||
],
|
||||
when: () => !this.gridContainer && !this.flexContainer,
|
||||
error: property => msg("rule.inactive.css.not.grid.or.flex.container", property),
|
||||
fixId: "inactive-css-not-grid-or-flex-container-fix",
|
||||
msgId: "inactive-css-not-grid-or-flex-container",
|
||||
numFixProps: 2,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
@ -143,19 +155,30 @@ class InactivePropertyHelper {
|
|||
* The CSS property name.
|
||||
*
|
||||
* @return {Object} object
|
||||
* @return {Boolean} object.fixId
|
||||
* A Fluent id containing a suggested solution to the problem that is
|
||||
* causing a property to be inactive.
|
||||
* @return {Boolean} object.msgId
|
||||
* A Fluent id containing an error message explaining why a property
|
||||
* is inactive in this situation.
|
||||
* @return {Boolean} object.numFixProps
|
||||
* The number of properties we suggest in the fixId string.
|
||||
* @return {Boolean} object.property
|
||||
* The inactive property name.
|
||||
* @return {Boolean} object.used
|
||||
* true if the property is used.
|
||||
* @return {Array} object.reasons
|
||||
* A string array listing the reasons a property isn't used.
|
||||
*/
|
||||
isPropertyUsed(el, elStyle, cssRule, property) {
|
||||
if (!this.unusedCssEnabled) {
|
||||
return {used: true};
|
||||
}
|
||||
|
||||
const errors = [];
|
||||
let fixId = "";
|
||||
let msgId = "";
|
||||
let numFixProps = 0;
|
||||
let used = true;
|
||||
|
||||
this.VALIDATORS.forEach(validator => {
|
||||
this.VALIDATORS.some(validator => {
|
||||
// First check if this rule cares about this property.
|
||||
let isRuleConcerned = false;
|
||||
|
||||
|
@ -167,7 +190,7 @@ class InactivePropertyHelper {
|
|||
}
|
||||
|
||||
if (!isRuleConcerned) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
this.select(el, elStyle, cssRule, property);
|
||||
|
@ -175,17 +198,23 @@ class InactivePropertyHelper {
|
|||
// And then run the validator, gathering the error message if the
|
||||
// validator passes.
|
||||
if (validator.when()) {
|
||||
const error = validator.error(property);
|
||||
fixId = validator.fixId;
|
||||
msgId = validator.msgId;
|
||||
numFixProps = validator.numFixProps;
|
||||
used = false;
|
||||
|
||||
if (typeof error === "string") {
|
||||
errors.push(validator.error(property));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
return {
|
||||
used: !errors.length,
|
||||
reasons: errors,
|
||||
fixId,
|
||||
msgId,
|
||||
numFixProps,
|
||||
property,
|
||||
used,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -350,17 +379,4 @@ class InactivePropertyHelper {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function that gets localized strings.
|
||||
*
|
||||
* @param {String} propName
|
||||
* The property name to use. This property name must exist in the
|
||||
* `inspector.properties` file).
|
||||
* @param {*} values
|
||||
* Values to be used as replacement strings.
|
||||
*/
|
||||
function msg(...args) {
|
||||
return INSPECTOR_L10N.getFormatStr(...args);
|
||||
}
|
||||
|
||||
exports.inactivePropertyHelper = new InactivePropertyHelper();
|
||||
|
|
Загрузка…
Ссылка в новой задаче