Source code
Revision control
Copy as Markdown
Other Tools
/* 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
"use strict";
loader.lazyRequireGetter(
this,
"KeyCodes",
"resource://devtools/client/shared/keycodes.js",
true
);
loader.lazyRequireGetter(
this,
"InspectorCSSParserWrapper",
"resource://devtools/shared/css/lexer.js",
true
);
loader.lazyRequireGetter(
this,
"parseDeclarations",
"resource://devtools/shared/css/parsing-utils.js",
true
);
/**
* Called when a character is typed in a value editor. This decides
* whether to advance or not, first by checking to see if ";" was
* typed, and then by lexing the input and seeing whether the ";"
* would be a terminator at this point.
*
* @param {number} keyCode
* Key code to be checked.
* @param {string} aValue
* Current text editor value.
* @param {number} insertionPoint
* The index of the insertion point.
* @return {Boolean} True if the focus should advance; false if
* the character should be inserted.
*/
function advanceValidate(keyCode, value, insertionPoint) {
// Only ";" has special handling here.
if (keyCode !== KeyCodes.DOM_VK_SEMICOLON) {
return false;
}
// Insert the character provisionally and see what happens. If we
// end up with a ";" symbol token, then the semicolon terminates the
// value. Otherwise it's been inserted in some spot where it has a
// valid meaning, like a comment or string.
value = value.slice(0, insertionPoint) + ";" + value.slice(insertionPoint);
const lexer = new InspectorCSSParserWrapper(value);
while (true) {
const token = lexer.nextToken();
if (token.endOffset > insertionPoint) {
if (token.tokenType === "Semicolon") {
// The ";" is a terminator.
return true;
}
// The ";" is not a terminator in this context.
break;
}
}
return false;
}
/**
* Append a text node to an element.
*
* @param {Element} parent
* The parent node.
* @param {string} text
* The text content for the text node.
*/
function appendText(parent, text) {
parent.appendChild(parent.ownerDocument.createTextNode(text));
}
/**
* Event handler that causes a blur on the target if the input has
* multiple CSS properties as the value.
*/
function blurOnMultipleProperties(cssProperties) {
return e => {
setTimeout(() => {
const props = parseDeclarations(cssProperties.isKnown, e.target.value);
if (props.length > 1) {
e.target.blur();
}
}, 0);
};
}
/**
* Create a child element with a set of attributes.
*
* @param {Element} parent
* The parent node.
* @param {string} tagName
* The tag name.
* @param {object} attributes
* A set of attributes to set on the node.
*/
function createChild(parent, tagName, attributes = {}) {
const elt = parent.ownerDocument.createElementNS(HTML_NS, tagName);
for (const attr in attributes) {
if (attributes.hasOwnProperty(attr)) {
if (attr === "textContent") {
elt.textContent = attributes[attr];
} else if (attr === "child") {
elt.appendChild(attributes[attr]);
} else {
elt.setAttribute(attr, attributes[attr]);
}
}
}
parent.appendChild(elt);
return elt;
}
/**
* Retrieve the content of a longString (via a promise resolving a LongStringActor).
*
* @param {Promise} longStringActorPromise
* promise expected to resolve a LongStringActor instance
* @return {Promise} promise resolving with the retrieved string as argument
*/
async function getLongString(longStringActorPromise) {
try {
const longStringActor = await longStringActorPromise;
const string = await longStringActor.string();
longStringActor.release().catch(console.error);
return string;
} catch (e) {
console.error(e);
return undefined;
}
}
/**
* Returns a selector of the Element Rep from the grip. This is based on the
* getElements() function in our devtools-reps component for a ElementNode.
*
* @param {Object} grip
* Grip-like object that can be used with Reps.
* @return {String} selector of the element node.
*/
function getSelectorFromGrip(grip) {
const {
attributes,
nodeName,
isAfterPseudoElement,
isBeforePseudoElement,
isMarkerPseudoElement,
} = grip.preview;
if (isAfterPseudoElement) {
return "::after";
} else if (isBeforePseudoElement) {
return "::before";
} else if (isMarkerPseudoElement) {
return "::marker";
}
let selector = nodeName;
if (attributes.id) {
selector += `#${attributes.id}`;
}
if (attributes.class) {
selector += attributes.class
.trim()
.split(/\s+/)
.map(cls => `.${cls}`)
.join("");
}
return selector;
}
/**
* Log the provided error to the console and return a rejected Promise for
* this error.
*
* @param {Error} error
* The error to log
* @return {Promise} A rejected promise
*/
function promiseWarn(error) {
console.error(error);
return Promise.reject(error);
}
/**
* translate nodeFront to a grip-like object that can be used with an ElementNode rep.
*
* @params {NodeFront} nodeFront
* The NodeFront for which we want to create a grip-like object.
* @returns {Object} a grip-like object that can be used with Reps.
*/
function translateNodeFrontToGrip(nodeFront) {
const { attributes } = nodeFront;
// The main difference between NodeFront and grips is that attributes are treated as
// a map in grips and as an array in NodeFronts.
const attributesMap = {};
for (const { name, value } of attributes) {
attributesMap[name] = value;
}
return {
actor: nodeFront.actorID,
preview: {
attributes: attributesMap,
attributesLength: attributes.length,
isAfterPseudoElement: nodeFront.isAfterPseudoElement,
isBeforePseudoElement: nodeFront.isBeforePseudoElement,
isMarkerPseudoElement: nodeFront.isMarkerPseudoElement,
// All the grid containers are assumed to be in the DOM tree.
isConnected: true,
// nodeName is already lowerCased in Node grips
nodeName: nodeFront.nodeName.toLowerCase(),
nodeType: nodeFront.nodeType,
},
};
}
exports.advanceValidate = advanceValidate;
exports.appendText = appendText;
exports.blurOnMultipleProperties = blurOnMultipleProperties;
exports.createChild = createChild;
exports.getLongString = getLongString;
exports.getSelectorFromGrip = getSelectorFromGrip;
exports.promiseWarn = promiseWarn;
exports.translateNodeFrontToGrip = translateNodeFrontToGrip;