SSL output-parser.js
Interaktion und PortierbarkeitJAVA
/* 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/. */
// This regexp matches a URL token. It puts the "url(", any // leading whitespace, and any opening quote into |leader|; the // URL text itself into |body|, and any trailing quote, trailing // whitespace, and the ")" into |trailer|. const URL_REGEX =
/^(?<leader>url\([ \t\r\n\f]*(["']?))(?.*?)(?\2[ \t\r\n\f]*\))$/i;
// Very long text properties should be truncated using CSS to avoid creating // extremely tall propertyvalue containers. 5000 characters is an arbitrary // limit. Assuming an average ruleview can hold 50 characters per line, this // should start truncating properties which would otherwise be 100 lines long. const TRUNCATE_LENGTH_THRESHOLD = 5000; const TRUNCATE_NODE_CLASSNAME = "propertyvalue-long-text";
/** * This module is used to process CSS text declarations and output DOM fragments (to be * appended to panels in DevTools) for CSS values decorated with additional UI and * functionality. * * For example: * - attaching swatches for values instrumented with specialized tools: colors, timing * functions (cubic-bezier), filters, shapes, display values (flex/grid), etc. * - adding previews where possible (images, fonts, CSS transforms). * - converting between color types on Shift+click on their swatches. * * Usage: * const OutputParser = require("devtools/client/shared/output-parser"); * const parser = new OutputParser(document, cssProperties); * parser.parseCssProperty("color", "red"); // Returns document fragment. *
*/ class OutputParser { /** * @param {Document} document * Used to create DOM nodes. * @param {CssProperties} cssProperties * Instance of CssProperties, an object which provides an interface for * working with the database of supported CSS properties and values.
*/
constructor(document, cssProperties) { this.#doc = document; this.#cssProperties = cssProperties;
}
#angleSwatches = new WeakMap();
#colorSwatches = new WeakMap();
#cssProperties;
#doc;
#parsed = [];
#stack = [];
/** * Parse a CSS property value given a property name. * * @param {String} name * CSS Property Name * @param {String} value * CSS Property value * @param {Object} [options] * Options object. For valid options and default values see * #mergeOptions(). * @return {DocumentFragment} * A document fragment containing color swatches etc.
*/
parseCssProperty(name, value, options = {}) {
options = this.#mergeOptions(options);
options.expectCubicBezier = this.#cssProperties.supportsType(
name, "timing-function"
);
options.expectLinearEasing = this.#cssProperties.supportsType(
name, "timing-function"
);
options.expectDisplay = name === "display";
options.expectFilter =
name === "filter" ||
(BACKDROP_FILTER_ENABLED && name === "backdrop-filter");
options.expectShape =
name === "clip-path" ||
name === "shape-outside" ||
name === "offset-path";
options.expectFont = name === "font-family";
options.isVariable = name.startsWith("--");
options.supportsColor = this.#cssProperties.supportsType(name, "color") || this.#cssProperties.supportsType(name, "gradient") || // Parse colors for CSS variables declaration if the declaration value or the computed // value are valid colors.
(options.isVariable &&
(InspectorUtils.isValidCSSColor(value) ||
InspectorUtils.isValidCSSColor(
options.getVariableData?.(name).computedValue
)));
// The filter property is special in that we want to show the // swatch even if the value is invalid, because this way the user // can easily use the editor to fix it. if (options.expectFilter || this.#cssPropertySupportsValue(name, value)) { returnthis.#parse(value, options);
} this.#appendTextNode(value);
returnthis.#toDOM();
}
/** * Read tokens from |tokenStream| and collect all the (non-comment) * text. Return the collected texts and variable data (if any). * Stop when an unmatched closing paren is seen. * If |stopAtComma| is true, then also stop when a top-level * (unparenthesized) comma is seen. * * @param {String} text * The original source text. * @param {CSSLexer} tokenStream * The token stream from which to read. * @param {Object} options * The options object in use; @see #mergeOptions. * @param {Boolean} stopAtComma * If true, stop at a comma. * @return {Object} * An object of the form {tokens, functionData, sawComma, sawVariable, depth}. * |tokens| is a list of the non-comment, non-whitespace tokens * that were seen. The stopping token (paren or comma) will not * be included. * |functionData| is a list of parsed strings and nodes that contain the * data between the matching parenthesis. The stopping token's text will * not be included. * |sawComma| is true if the stop was due to a comma, or false otherwise. * |sawVariable| is true if a variable was seen while parsing the text. * |depth| is the number of unclosed parenthesis remaining when we return.
*/
#parseMatchingParens(text, tokenStream, options, stopAtComma) {
let depth = 1; const functionData = []; const tokens = [];
let sawVariable = false;
while (depth > 0) { const token = tokenStream.nextToken(); if (!token) { break;
} if (token.tokenType === "Comment") { continue;
}
/** * Parse var() use and return a variable node to be added to the output state. * This will read tokens up to and including the ")" that closes the "var(" * invocation. * * @param {CSSToken} initialToken * The "var(" token that was already seen. * @param {String} text * The original input text. * @param {CSSLexer} tokenStream * The token stream from which to read. * @param {Object} options * The options object in use; @see #mergeOptions. * @return {Object} * - node: A node for the variable, with the appropriate text and * title. Eg. a span with "var(--var1)" as the textContent * and a title for --var1 like "--var1 = 10" or * "--var1 is not set". * - value: The value for the variable.
*/
#parseVariable(initialToken, text, tokenStream, options) { // Handle the "var(". const varText = text.substring(
initialToken.startOffset,
initialToken.endOffset
); const variableNode = this.#createNode("span", {}, varText);
// Parse the first variable name within the parens of var(). const { tokens, functionData, sawComma, sawVariable } = this.#parseMatchingParens(text, tokenStream, options, true);
const result = sawVariable ? "" : functionData.join("");
// Display options for the first and second argument in the var(). const firstOpts = {}; const secondOpts = {};
let varData;
let varFallbackValue;
let varSubstitutedValue;
let varComputedValue;
// Get the variable value if it is in use. if (tokens && tokens.length === 1) {
varData = options.getVariableData(tokens[0].text); const varValue = typeof varData.value === "string"
? varData.value
: varData.registeredProperty?.initialValue;
const varStartingStyleValue = typeof varData.startingStyle === "string"
? varData.startingStyle
: // If the variable is not set in starting style, then it will default to either: // - a declaration in a "regular" rule // - or if there's no declaration in regular rule, to the registered property initial-value.
varValue;
if (typeof varSubstitutedValue === "string") { // The variable value is valid, store the substituted value in a data attribute to // be reused by the variable tooltip.
firstOpts["data-variable"] = varSubstitutedValue;
firstOpts.class = options.matchedVariableClass;
secondOpts.class = options.unmatchedClass;
// Display computed value when it exists, is different from the substituted value // we computed, and we're not inside a starting-style rule if (
!options.inStartingStyleRule && typeof varComputedValue === "string" &&
varComputedValue !== varSubstitutedValue
) {
firstOpts["data-variable-computed"] = varComputedValue;
}
// Display starting-style value when not in a starting style rule if (
!options.inStartingStyleRule && typeof varData.startingStyle === "string"
) {
firstOpts["data-starting-style-variable"] = varData.startingStyle;
}
if (varData.registeredProperty) { const { initialValue, syntax, inherits } = varData.registeredProperty;
firstOpts["data-registered-property-initial-value"] = initialValue;
firstOpts["data-registered-property-syntax"] = syntax; // createNode does not handle `false`, let's stringify the boolean.
firstOpts["data-registered-property-inherits"] = `${inherits}`;
}
} else { // The variable is not set and does not have an initial value, mark it unmatched.
firstOpts.class = options.unmatchedClass;
// Get the variable name. const varName = text.substring(
tokens[0].startOffset,
tokens[0].endOffset
);
firstOpts["data-variable"] = STYLE_INSPECTOR_L10N.getFormatStr( "rule.variableUnset",
varName
);
}
// If we saw a ",", then append it and show the remainder using // the correct highlighting. if (sawComma) {
variableNode.appendChild(this.#doc.createTextNode(","));
// Parse the text up until the close paren, being sure to // disable the special case for filter. const subOptions = Object.assign({}, options);
subOptions.expectFilter = false; const saveParsed = this.#parsed; const savedStack = this.#stack; this.#parsed = []; this.#stack = []; const rest = this.#doParse(text, subOptions, tokenStream, true); this.#parsed = saveParsed; this.#stack = savedStack;
/** * The workhorse for @see #parse. This parses some CSS text, * stopping at EOF; or optionally when an umatched close paren is * seen. * * @param {String} text * The original input text. * @param {Object} options * The options object in use; @see #mergeOptions. * @param {CSSLexer} tokenStream * The token stream from which to read * @param {Boolean} stopAtCloseParen * If true, stop at an umatched close paren. * @return {DocumentFragment} * A document fragment.
*/ // eslint-disable-next-line complexity
#doParse(text, options, tokenStream, stopAtCloseParen) {
let fontFamilyNameParts = [];
let previousWasBang = false;
this.#stack.push({
lowerCaseFunctionName,
functionName,
isColorTakingFunction, // The position of the function separators ("," or "/") in the `parts` property
separatorIndexes: [], // The parsed parts of the function that will be rendered on screen. // This can hold both simple strings and DOMNodes.
parts: [],
});
if (
isColorTakingFunction ||
ANGLE_TAKING_FUNCTIONS.has(lowerCaseFunctionName)
) { // The function can accept a color or an angle argument, and we know // it isn't special in some other way. So, we let it // through to the ordinary parsing loop so that the value // can be handled in a single place. this.#appendTextNode(
text.substring(token.startOffset, token.endOffset)
);
} elseif (
lowerCaseFunctionName === "var" &&
options.getVariableData
) { const {
node: variableNode,
value,
computedValue,
} = this.#parseVariable(token, text, tokenStream, options);
const variableValue = computedValue ?? value; // InspectorUtils.isValidCSSColor returns true for `light-dark()` function, // but `#isValidColor` returns false. As the latter is used in #appendColor, // we need to check that both functions return true. const colorObj =
value &&
colorOK() &&
InspectorUtils.isValidCSSColor(variableValue)
? new colorUtils.CssColor(variableValue)
: null;
this.#appendColor(computedFunctionText, {
...options,
colorFunction: colorFunctionEntry?.functionName,
valueParts: [
functionName, "(",
...functionData.map(data => data.node || data), ")",
],
});
} else { // If function contains variable, we need to add both strings // and nodes. this.#appendTextNode(functionName + "("); for (const data of functionData) { if (typeof data === "string") { this.#appendTextNode(data);
} elseif (data) { this.#append(data.node);
}
} this.#appendTextNode(")");
}
} else { // If no variable in function, join the text together and add // to DOM accordingly. const functionText =
functionName + "(" +
functionData.join("") + // only append closing parenthesis if the authored text actually had it // In such case, we should probably indicate that there's a "syntax error" // See Bug 1891461.
(depth == 0 ? ")" : "");
if (lowerCaseFunctionName === "url" && options.urlClass) { // url() with quoted strings are not mapped as UnquotedUrl, // instead, we get a "Function" token with "url" function name, // and later, a "QuotedString" token, which contains the actual URL.
let url; for (const argToken of functionArgTokens) { if (argToken.tokenType === "QuotedString") {
url = argToken.value; break;
}
}
case"ParenthesisBlock": this.#stack.push({
isParenthesis: true,
separatorIndexes: [], // The parsed parts of the function that will be rendered on screen. // This can hold both simple strings and DOMNodes.
parts: [],
}); this.#appendTextNode(
text.substring(token.startOffset, token.endOffset)
); break;
// Add separator for the current function if (this.#stack.length) { this.#appendTextNode(token.text); const entry = this.#stack.at(-1);
entry.separatorIndexes.push(entry.parts.length - 1); break;
}
// falls through default: this.#appendTextNode(
text.substring(token.startOffset, token.endOffset)
); break;
}
if (options.expectFont && fontFamilyNameParts.length !== 0) { this.#appendFontFamily(fontFamilyNameParts.join(""), options);
}
// We might never encounter a matching closing parenthesis for a function and still // have a "valid" value (e.g. `background: linear-gradient(90deg, red, blue"`) // In such case, go through the stack and handle each items until we have nothing left. if (this.#stack.length) { while (this.#stack.length !== 0) { this.#onCloseParenthesis(options);
}
}
let result = this.#toDOM();
if (options.expectFilter && !options.filterSwatch) {
result = this.#wrapFilter(text, options, result);
}
return result;
}
#onCloseParenthesis(options) { if (!this.#stack.length) { return;
}
const stackEntry = this.#stack.at(-1); if (
stackEntry.lowerCaseFunctionName === "light-dark" && typeof options.isDarkColorScheme === "boolean" && // light-dark takes exactly two parameters, so if we don't get exactly 1 separator // at this point, that means that the value is valid at parse time, but is invalid // at computed value time. // TODO: We might want to add a class to indicate that this is invalid at computed // value time (See Bug 1910845)
stackEntry.separatorIndexes.length === 1
) { const stackEntryParts = this.#getCurrentStackParts(); const separatorIndex = stackEntry.separatorIndexes[0];
let startIndex;
let endIndex; if (options.isDarkColorScheme) { // If we're using a dark color scheme, we want to mark the first param as // not used.
// The first "part" is `light-dark(`, so we can start after that. // We want to filter out white space character before the first parameter for (startIndex = 1; startIndex < separatorIndex; startIndex++) { const part = stackEntryParts[startIndex]; if (typeof part !== "string" || part.trim() !== "") { break;
}
}
// same for the end of the parameter, we want to filter out whitespaces // after the parameter and before the comma for (
endIndex = separatorIndex - 1;
endIndex >= startIndex;
endIndex--
) { const part = stackEntryParts[endIndex]; if (typeof part !== "string" || part.trim() !== "") { // We found a non-whitespace part, we need to include it, so increment the endIndex
endIndex++; break;
}
}
} else { // If we're not using a dark color scheme, we want to mark the second param as // not used.
// We want to filter out white space character after the comma and before the // second parameter for (
startIndex = separatorIndex + 1;
startIndex < stackEntryParts.length;
startIndex++
) { const part = stackEntryParts[startIndex]; if (typeof part !== "string" || part.trim() !== "") { break;
}
}
// same for the end of the parameter, we want to filter out whitespaces // after the parameter and before the closing parenthesis (which is not yet // included in stackEntryParts) for (
endIndex = stackEntryParts.length - 1;
endIndex > separatorIndex;
endIndex--
) { const part = stackEntryParts[endIndex]; if (typeof part !== "string" || part.trim() !== "") { // We found a non-whitespace part, we need to include it, so increment the endIndex
endIndex++; break;
}
}
}
const parts = stackEntryParts.slice(startIndex, endIndex);
// If the item we need to mark is already an element (e.g. a parsed color), // just add a class to it. if (parts.length === 1 && Element.isInstance(parts[0])) {
parts[0].classList.add(options.unmatchedClass);
} else { // Otherwise, we need to wrap our parts into a specific element so we can // style them const node = this.#createNode("span", { class: options.unmatchedClass,
});
node.append(...parts);
stackEntryParts.splice(startIndex, parts.length, node);
}
}
// Our job is done here, pop last stack entry const { parts } = this.#stack.pop(); // Put all the parts in the "new" last stack, or the main parsed array if there // is no more entry in the stack this.#getCurrentStackParts().push(...parts);
}
/** * Parse a string. * * @param {String} text * Text to parse. * @param {Object} [options] * Options object. For valid options and default values see * #mergeOptions(). * @return {DocumentFragment} * A document fragment.
*/
#parse(text, options = {}) {
text = text.trim(); this.#parsed.length = 0; this.#stack.length = 0;
const tokenStream = new InspectorCSSParserWrapper(text); returnthis.#doParse(text, options, tokenStream, false);
}
/** * Returns true if it's a "display: [inline-]flex" token. * * @param {String} text * The parsed text. * @param {Object} token * The parsed token. * @param {Object} options * The options given to #parse.
*/
#isDisplayFlex(text, token, options) { return (
options.expectDisplay &&
(token.text === "flex" || token.text === "inline-flex")
);
}
/** * Returns true if it's a "display: [inline-]grid" token. * * @param {String} text * The parsed text. * @param {Object} token * The parsed token. * @param {Object} options * The options given to #parse.
*/
#isDisplayGrid(text, token, options) { return (
options.expectDisplay &&
(token.text === "grid" || token.text === "inline-grid")
);
}
/** * Append a cubic-bezier timing function value to the output * * @param {String} bezier * The cubic-bezier timing function * @param {Object} options * Options object. For valid options and default values see * #mergeOptions()
*/
#appendCubicBezier(bezier, options) { const container = this.#createNode("span", { "data-bezier": bezier,
});
/** * Append a Flexbox|Grid highlighter toggle icon next to the value in a * "display: [inline-]flex" or "display: [inline-]grid" declaration. * * @param {String} text * The text value to append * @param {String} toggleButtonClassName * The class name for the toggle button. * If not passed/empty, the toggle button won't be created.
*/
#appendDisplayWithHighlighterToggle(text, toggleButtonClassName) { const container = this.#createNode("span", {});
const value = this.#createNode("span", {}, text);
container.append(value); this.#append(container);
}
/** * Append a CSS shapes highlighter toggle next to the value, and parse the value * into spans, each containing a point that can be hovered over. * * @param {String} shape * The shape text value to append * @param {Object} options * Options object. For valid options and default values see * #mergeOptions()
*/
#appendShape(shape, options) { const shapeTypes = [
{
prefix: "polygon(",
coordParser: this.#addPolygonPointNodes.bind(this),
},
{
prefix: "circle(",
coordParser: this.#addCirclePointNodes.bind(this),
},
{
prefix: "ellipse(",
coordParser: this.#addEllipsePointNodes.bind(this),
},
{
prefix: "inset(",
coordParser: this.#addInsetPointNodes.bind(this),
},
];
/** * Parse the given polygon coordinates and create a span for each coordinate pair, * adding it to the given container node. * * @param {String} coords * The string of coordinate pairs. * @param {Node} container * The node to which spans containing points are added. * @returns {Node} The container to which spans have been added.
*/ // eslint-disable-next-line complexity
#addPolygonPointNodes(coords, container) { const tokenStream = new InspectorCSSParserWrapper(coords);
let token = tokenStream.nextToken();
let coord = "";
let i = 0;
let depth = 0;
let isXCoord = true;
let fillRule = false;
let coordNode = this.#createNode("span", { class: "inspector-shape-point", "data-point": `${i}`,
});
while (token) { if (token.tokenType === "Comma") { // Comma separating coordinate pairs; add coordNode to container and reset vars if (!isXCoord) { // Y coord not added to coordNode yet const node = this.#createNode( "span",
{ class: "inspector-shape-point", "data-point": `${i}`, "data-pair": isXCoord ? "x" : "y",
},
coord
);
coordNode.appendChild(node);
coord = "";
isXCoord = !isXCoord;
}
// Add coords if any are left over. if (coord) { if (point === "rx" || point === "ry") { const node = this.#createNode( "span",
{ class: "inspector-shape-point", "data-point": point,
},
coord
);
container.appendChild(node);
} else { const node = this.#createNode( "span",
{ class: "inspector-shape-point", "data-point": "center", "data-pair": point === "cx" ? "x" : "y",
},
coord
);
centerNode.appendChild(node);
}
}
if (centerNode.textContent) {
container.appendChild(centerNode);
} return container;
}
/** * Parse the given inset coordinates and populate the given container appropriately. * * @param {String} coords * The inset definition. * @param {Node} container * The node to which the definition is added. * @returns {Node} The container to which the definition has been added.
*/ // eslint-disable-next-line complexity
#addInsetPointNodes(coords, container) { const insetPoints = ["top", "right", "bottom", "left"]; const tokenStream = new InspectorCSSParserWrapper(coords);
let token = tokenStream.nextToken();
let depth = 0;
let coord = "";
let i = 0;
let round = false; // nodes is an array containing all the coordinate spans. otherText is an array of // arrays, each containing the text that should be inserted into container before // the node with the same index. i.e. all elements of otherText[i] is inserted // into container before nodes[i]. const nodes = []; const otherText = [[]];
while (token) { if (round) { // Everything that comes after "round" should just be plain text
otherText[i].push(coords.substring(token.startOffset, token.endOffset));
} elseif (token.tokenType === "ParenthesisBlock") {
depth++;
coord += coords.substring(token.startOffset, token.endOffset);
} elseif (token.tokenType === "CloseParenthesis") {
depth--;
coord += coords.substring(token.startOffset, token.endOffset);
} elseif (token.tokenType === "WhiteSpace" && coord === "") { // Whitespace at beginning of coord; add to container
otherText[i].push(coords.substring(token.startOffset, token.endOffset));
} elseif (token.tokenType === "WhiteSpace" && depth === 0) { // Whitespace signifying end of coord; create node and push to nodes const node = this.#createNode( "span",
{ class: "inspector-shape-point",
},
coord
);
nodes.push(node);
i++;
coord = "";
otherText[i] = [coords.substring(token.startOffset, token.endOffset)];
depth = 0;
} elseif (
token.tokenType === "Number" ||
token.tokenType === "Dimension" ||
token.tokenType === "Percentage" ||
token.tokenType === "Function"
) { if (coord && depth === 0) { // Inset coords don't require whitespace between each coord. const node = this.#createNode( "span",
{ class: "inspector-shape-point",
},
coord
);
nodes.push(node);
i++;
coord = "";
otherText[i] = [];
}
coord += coords.substring(token.startOffset, token.endOffset); if (token.tokenType === "Function") {
depth++;
}
} elseif (token.tokenType === "Ident" && token.text === "round") { if (coord && depth === 0) { // Whitespace is not necessary before "round"; create a new node for the coord const node = this.#createNode( "span",
{ class: "inspector-shape-point",
},
coord
);
nodes.push(node);
i++;
coord = "";
otherText[i] = [];
}
round = true;
otherText[i].push(coords.substring(token.startOffset, token.endOffset));
} else {
coord += coords.substring(token.startOffset, token.endOffset);
}
token = tokenStream.nextToken();
}
// Take care of any leftover text if (coord) { if (round) {
otherText[i].push(coord);
} else { const node = this.#createNode( "span",
{ class: "inspector-shape-point",
},
coord
);
nodes.push(node);
}
}
// insetPoints contains the 4 different possible inset points in the order they are // defined. By taking the modulo of the index in insetPoints with the number of nodes, // we can get which node represents each point (e.g. if there is only 1 node, it // represents all 4 points). The exception is "left" when there are 3 nodes. In that // case, it is nodes[1] that represents the left point rather than nodes[0]. for (let j = 0; j < 4; j++) { const point = insetPoints[j]; const nodeIndex =
point === "left" && nodes.length === 3 ? 1 : j % nodes.length;
nodes[nodeIndex].classList.add(point);
}
nodes.forEach((node, j) => { for (const text of otherText[j]) {
appendText(container, text);
}
container.appendChild(node);
});
// Add text that comes after the last node, if any exists if (otherText[nodes.length]) { for (const text of otherText[nodes.length]) {
appendText(container, text);
}
}
return container;
}
/** * Append a angle value to the output * * @param {String} angle * angle to append * @param {Object} options * Options object. For valid options and default values see * #mergeOptions()
*/
#appendAngle(angle, options) { const angleObj = new angleUtils.CssAngle(angle); const container = this.#createNode("span", { "data-angle": angle,
});
// Add click listener to stop event propagation when shift key is pressed // in order to prevent the value input to be focused. // Bug 711942 will add a tooltip to edit angle values and we should // be able to move this listener to Tooltip.js when it'll be implemented.
swatch.addEventListener("click", function (event) { if (event.shiftKey) {
event.stopPropagation();
}
});
container.appendChild(swatch);
}
/** * Check if a CSS property supports a specific value. * * @param {String} name * CSS Property name to check * @param {String} value * CSS Property value to check
*/
#cssPropertySupportsValue(name, value) { // Checking pair as a CSS declaration string to account for "!important" in value. const declaration = `${name}:${value}`; returnthis.#doc.defaultView.CSS.supports(declaration);
}
/** * Tests if a given colorObject output by CssColor is valid for parsing. * Valid means it's really a color, not any of the CssColor SPECIAL_VALUES * except transparent
*/
#isValidColor(colorObj) { return (
colorObj.valid &&
(!colorObj.specialValue || colorObj.specialValue === "transparent")
);
}
/** * Append a color to the output. * * @param {String} color * Color to append * @param {Object} [options] * @param {CSSColor} options.colorObj: A css color for the passed color. Will be computed * if not passed. * @param {DOMNode} options.variableContainer: A DOM Node that is the result of parsing * a CSS variable * @param {String} options.colorFunction: The color function that is used to produce this color * @param {*} For all the other valid options and default values see #mergeOptions().
*/
#appendColor(color, options = {}) { const colorObj = options.colorObj || new colorUtils.CssColor(color);
if (options.colorSwatchClass) {
let attributes = { class: options.colorSwatchClass,
style: "background-color:" + color,
};
// Color swatches next to values trigger the color editor everywhere aside from // the Computed panel where values are read-only. if (!options.colorSwatchReadOnly) {
attributes = { ...attributes, tabindex: "0", role: "button" };
}
// The swatch is a <span> instead of a <button> intentionally. See Bug 1597125. // It is made keyboard accessible via `tabindex` and has keydown handlers // attached for pressing SPACE and RETURN in SwatchBasedEditorTooltip.js const swatch = this.#createNode("span", attributes); this.#colorSwatches.set(swatch, colorObj); if (options.colorFunction) {
swatch.dataset.colorFunction = options.colorFunction;
}
swatch.addEventListener("mousedown", this.#onColorSwatchMouseDown);
container.appendChild(swatch);
container.classList.add("color-swatch-container");
}
let colorUnit = options.defaultColorUnit; if (!options.useDefaultColorUnit) { // If we're not being asked to convert the color to the default color type // specified by the user, then force the CssColor instance to be set to the type // of the current color. // Not having a type means that the default color type will be automatically used.
colorUnit = colorUtils.classifyColor(color);
}
color = colorObj.toString(colorUnit);
container.dataset.color = color;
// Next we create the markup to show the value of the property. if (options.variableContainer) { // If we are creating a color swatch for a CSS variable we simply reuse // the markup created for the variableContainer. if (options.colorClass) {
options.variableContainer.classList.add(options.colorClass);
}
container.appendChild(options.variableContainer);
} else { // Otherwise we create a new element with the `color` as textContent. const value = this.#createNode("span", { class: options.colorClass,
}); if (options.valueParts) {
value.append(...options.valueParts);
} else {
value.append(this.#doc.createTextNode(color));
}
/** * Wrap some existing nodes in a filter editor. * * @param {String} filters * The full text of the "filter" property. * @param {object} options * The options object passed to parseCssProperty(). * @param {object} nodes * Nodes created by #toDOM(). * * @returns {object} * A new node that supplies a filter swatch and that wraps |nodes|.
*/
#wrapFilter(filters, options, nodes) { const container = this.#createNode("span", { "data-filters": filters,
});
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 ist noch experimentell.