utils.js (6470B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 loader.lazyRequireGetter( 8 this, 9 "KeyCodes", 10 "resource://devtools/client/shared/keycodes.js", 11 true 12 ); 13 loader.lazyRequireGetter( 14 this, 15 "InspectorCSSParserWrapper", 16 "resource://devtools/shared/css/lexer.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 "parseDeclarations", 22 "resource://devtools/shared/css/parsing-utils.js", 23 true 24 ); 25 26 const HTML_NS = "http://www.w3.org/1999/xhtml"; 27 28 /** 29 * Called when a character is typed in a value editor. This decides 30 * whether to advance or not, first by checking to see if ";" was 31 * typed, and then by lexing the input and seeing whether the ";" 32 * would be a terminator at this point. 33 * 34 * @param {number} keyCode 35 * Key code to be checked. 36 * @param {string} aValue 37 * Current text editor value. 38 * @param {number} insertionPoint 39 * The index of the insertion point. 40 * @return {boolean} True if the focus should advance; false if 41 * the character should be inserted. 42 */ 43 function advanceValidate(keyCode, value, insertionPoint) { 44 // Only ";" has special handling here. 45 if (keyCode !== KeyCodes.DOM_VK_SEMICOLON) { 46 return false; 47 } 48 49 // Insert the character provisionally and see what happens. If we 50 // end up with a ";" symbol token, then the semicolon terminates the 51 // value. Otherwise it's been inserted in some spot where it has a 52 // valid meaning, like a comment or string. 53 value = value.slice(0, insertionPoint) + ";" + value.slice(insertionPoint); 54 const lexer = new InspectorCSSParserWrapper(value); 55 while (true) { 56 const token = lexer.nextToken(); 57 if (token.endOffset > insertionPoint) { 58 if (token.tokenType === "Semicolon") { 59 // The ";" is a terminator. 60 return true; 61 } 62 // The ";" is not a terminator in this context. 63 break; 64 } 65 } 66 return false; 67 } 68 69 /** 70 * Append a text node to an element. 71 * 72 * @param {Element} parent 73 * The parent node. 74 * @param {string} text 75 * The text content for the text node. 76 */ 77 function appendText(parent, text) { 78 parent.appendChild(parent.ownerDocument.createTextNode(text)); 79 } 80 81 /** 82 * Event handler that causes a blur on the target if the input has 83 * multiple CSS properties as the value. 84 */ 85 function blurOnMultipleProperties(cssProperties) { 86 return e => { 87 setTimeout(() => { 88 const props = parseDeclarations(cssProperties.isKnown, e.target.value); 89 if (props.length > 1) { 90 e.target.blur(); 91 } 92 }, 0); 93 }; 94 } 95 96 /** 97 * Create a child element with a set of attributes. 98 * 99 * @param {Element} parent 100 * The parent node. 101 * @param {string} tagName 102 * The tag name. 103 * @param {object} attributes 104 * A set of attributes to set on the node. 105 */ 106 function createChild(parent, tagName, attributes = {}) { 107 const elt = parent.ownerDocument.createElementNS(HTML_NS, tagName); 108 for (const attr in attributes) { 109 if (attributes.hasOwnProperty(attr)) { 110 if (attr === "textContent") { 111 elt.textContent = attributes[attr]; 112 } else if (attr === "child") { 113 elt.appendChild(attributes[attr]); 114 } else { 115 elt.setAttribute(attr, attributes[attr]); 116 } 117 } 118 } 119 parent.appendChild(elt); 120 return elt; 121 } 122 123 /** 124 * Retrieve the content of a longString (via a promise resolving a LongStringActor). 125 * 126 * @param {Promise} longStringActorPromise 127 * promise expected to resolve a LongStringActor instance 128 * @return {Promise} promise resolving with the retrieved string as argument 129 */ 130 async function getLongString(longStringActorPromise) { 131 try { 132 const longStringActor = await longStringActorPromise; 133 const string = await longStringActor.string(); 134 longStringActor.release().catch(console.error); 135 return string; 136 } catch (e) { 137 console.error(e); 138 return undefined; 139 } 140 } 141 142 /** 143 * Returns a selector of the Element Rep from the grip. This is based on the 144 * getElements() function in our devtools-reps component for a ElementNode. 145 * 146 * @param {object} grip 147 * Grip-like object that can be used with Reps. 148 * @return {string} selector of the element node. 149 */ 150 function getSelectorFromGrip(grip) { 151 const { attributes, displayName } = grip.preview; 152 let selector = displayName; 153 154 if (attributes.id) { 155 selector += `#${attributes.id}`; 156 } 157 158 if (attributes.class) { 159 selector += attributes.class 160 .trim() 161 .split(/\s+/) 162 .map(cls => `.${cls}`) 163 .join(""); 164 } 165 166 return selector; 167 } 168 169 /** 170 * Log the provided error to the console and return a rejected Promise for 171 * this error. 172 * 173 * @param {Error} error 174 * The error to log 175 * @return {Promise} A rejected promise 176 */ 177 function promiseWarn(error) { 178 console.error(error); 179 return Promise.reject(error); 180 } 181 182 /** 183 * While waiting for a reps fix in https://github.com/firefox-devtools/reps/issues/92, 184 * translate nodeFront to a grip-like object that can be used with an ElementNode rep. 185 * 186 * @param {NodeFront} nodeFront 187 * The NodeFront for which we want to create a grip-like object. 188 * @returns {object} a grip-like object that can be used with Reps. 189 */ 190 function translateNodeFrontToGrip(nodeFront) { 191 const { attributes } = nodeFront; 192 193 // The main difference between NodeFront and grips is that attributes are treated as 194 // a map in grips and as an array in NodeFronts. 195 const attributesMap = {}; 196 for (const { name, value } of attributes) { 197 attributesMap[name] = value; 198 } 199 200 return { 201 actor: nodeFront.actorID, 202 preview: { 203 attributes: attributesMap, 204 attributesLength: attributes.length, 205 isPseudoElement: nodeFront.isPseudoElement, 206 // All the grid containers are assumed to be in the DOM tree. 207 isConnected: true, 208 displayName: nodeFront.displayName, 209 // nodeName is already lowerCased in Node grips 210 nodeName: nodeFront.nodeName.toLowerCase(), 211 nodeType: nodeFront.nodeType, 212 }, 213 }; 214 } 215 216 exports.advanceValidate = advanceValidate; 217 exports.appendText = appendText; 218 exports.blurOnMultipleProperties = blurOnMultipleProperties; 219 exports.createChild = createChild; 220 exports.getLongString = getLongString; 221 exports.getSelectorFromGrip = getSelectorFromGrip; 222 exports.promiseWarn = promiseWarn; 223 exports.translateNodeFrontToGrip = translateNodeFrontToGrip;