1 /* See license.txt for terms of usage */ 2 3 define([ 4 "firebug/firebug", 5 "firebug/lib/domplate", 6 "firebug/lib/locale", 7 "firebug/lib/css", 8 "firebug/lib/string", 9 "firebug/lib/array", 10 ], 11 function(Firebug, Domplate, Locale, Css, Str, Arr) { 12 with (Domplate) { 13 14 // ********************************************************************************************* // 15 // Constants 16 17 const reSelectorChar = /[-_0-9a-zA-Z]/; 18 19 // ********************************************************************************************* // 20 // CSS Selector Editor 21 22 function SelectorEditor() {} 23 24 SelectorEditor.prototype = domplate(Firebug.InlineEditor.prototype, 25 { 26 getAutoCompleteRange: function(value, offset) 27 { 28 // Find the word part of an identifier. 29 var reIdent = /[-_a-zA-Z0-9]*/; 30 var rbefore = Str.reverseString(value.substr(0, offset)); 31 var after = value.substr(offset); 32 var start = offset - reIdent.exec(rbefore)[0].length; 33 var end = offset + reIdent.exec(after)[0].length; 34 35 // Expand it to include '.', '#', ':', or '::'. 36 if (start > 0 && ".#:".indexOf(value.charAt(start-1)) !== -1) 37 { 38 --start; 39 if (start > 0 && value.substr(start-1, 2) === "::") 40 --start; 41 } 42 return {start: start, end: end}; 43 }, 44 45 getAutoCompleteList: function(preExpr, expr, postExpr, range, cycle, context, out) 46 { 47 // Don't support attribute selectors, for now. 48 if (preExpr.lastIndexOf("[") > preExpr.lastIndexOf("]")) 49 return []; 50 51 if (preExpr.lastIndexOf("(") > preExpr.lastIndexOf(")")) 52 { 53 // We are in an parenthesized expression, where we can only complete 54 // for a few particular pseudo-classes that take selector-like arguments. 55 var par = preExpr.lastIndexOf("("), colon = preExpr.lastIndexOf(":", par); 56 if (colon === -1) 57 return; 58 var allowed = ["-moz-any", "not", "-moz-empty-except-children-with-localname"]; 59 var name = preExpr.substring(colon+1, par); 60 if (allowed.indexOf(name) === -1) 61 return []; 62 } 63 64 var includeTagNames = true; 65 var includeIds = true; 66 var includeClasses = true; 67 var includePseudoClasses = true; 68 var includePseudoElements = true; 69 70 if (expr.length > 0) 71 { 72 includeTagNames = includeClasses = includeIds = 73 includePseudoClasses = includePseudoElements = false; 74 if (Str.hasPrefix(expr, "::")) 75 includePseudoElements = true; 76 else if (expr.charAt(0) === ":") 77 includePseudoClasses = true; 78 else if (expr.charAt(0) === "#") 79 includeIds = true; 80 else if (expr.charAt(0) === ".") 81 includeClasses = true; 82 else 83 includeTagNames = true; 84 } 85 if (preExpr.length > 0 && reSelectorChar.test(preExpr.slice(-1))) 86 includeTagNames = false; 87 88 var ret = []; 89 90 if (includeTagNames || includeIds || includeClasses) 91 { 92 // Traverse the DOM to get the used ids/classes/tag names that 93 // are relevant as continuations. 94 // (Tag names could be hard-coded, but finding which ones are 95 // actually used hides annoying things like 'b'/'i' when they 96 // are not used, and works in other contexts than HTML.) 97 // This isn't actually that bad, performance-wise. 98 var doc = context.window.document, els; 99 if (preExpr && " >+~".indexOf(preExpr.slice(-1)) === -1) 100 { 101 try 102 { 103 var preSelector = preExpr.split(",").reverse()[0]; 104 els = doc.querySelectorAll(preSelector); 105 } 106 catch (exc) 107 { 108 if (FBTrace.DBG_CSS) 109 FBTrace.sysout("Invalid previous selector part \"" + preSelector + "\"", exc); 110 } 111 } 112 if (!els) 113 els = doc.getElementsByTagName("*"); 114 els = [].slice.call(els); 115 116 if (includeTagNames) 117 { 118 var tagMap = {}; 119 els.forEach(function(e) 120 { 121 tagMap[e.localName] = 1; 122 }); 123 ret.push.apply(ret, Object.keys(tagMap)); 124 } 125 126 if (includeIds) 127 { 128 var ids = []; 129 els.forEach(function(e) 130 { 131 if (e.id) 132 ids.push(e.id); 133 }); 134 ids = Arr.sortUnique(ids); 135 ret.push.apply(ret, ids.map(function(cl) 136 { 137 return "#" + cl; 138 })); 139 } 140 141 if (includeClasses) 142 { 143 var clCombinationMap = Object.create(null), classes = []; 144 els.forEach(function(e) 145 { 146 var cl = e.className; 147 if (cl && !(cl in clCombinationMap)) 148 { 149 clCombinationMap[cl] = 1; 150 classes.push.apply(classes, e.classList); 151 } 152 }); 153 classes = Arr.sortUnique(classes); 154 ret.push.apply(ret, classes.map(function(cl) 155 { 156 return "." + cl; 157 })); 158 } 159 } 160 161 if (includePseudoClasses) 162 { 163 // Add the pseudo-class-looking :before, :after. 164 ret.push( 165 ":after", 166 ":before" 167 ); 168 169 ret.push.apply(ret, SelectorEditor.stripCompletedParens(Css.pseudoClasses, postExpr)); 170 } 171 172 if (includePseudoElements) 173 { 174 ret.push.apply(ret, Css.pseudoElements); 175 } 176 177 // Don't suggest things that are already included (by way of totally- 178 // incorrect-but-probably-good-enough logic). 179 var rev = Str.reverseString(preExpr); 180 var partInd = rev.search(/[, >+~]/); 181 var lastPart = (partInd === -1 ? rev : rev.substr(0, partInd)); 182 lastPart = Str.reverseString(lastPart); 183 if (lastPart !== "") 184 { 185 ret = ret.filter(function(str) 186 { 187 var ind = lastPart.indexOf(str); 188 if (ind === -1) 189 return true; 190 var before = ind-1, after = ind+str.length; 191 var re = reSelectorChar; 192 if (before >= 0 && re.test(str.charAt(0)) && re.test(lastPart.charAt(before))) 193 return true; 194 if (after < lastPart.length && re.test(lastPart.charAt(after))) 195 return true; 196 return false; 197 }); 198 } 199 200 // Don't suggest internal Firebug things. 201 var reInternal = /^[.#]firebug[A-Z]/; 202 ret = ret.filter(function(str) 203 { 204 return !reInternal.test(str); 205 }); 206 207 if (ret.indexOf(":hover") !== -1) 208 out.suggestion = ":hover"; 209 210 return ret.sort(); 211 }, 212 213 getAutoCompletePropSeparator: function(range, expr, prefixOf) 214 { 215 // For e.g. 'd|span', expand to a descendant selector; otherwise assume 216 // that this is part of the same selector part. 217 return (reSelectorChar.test(prefixOf.charAt(0)) ? " " : ""); 218 }, 219 220 autoCompleteAdjustSelection: function(value, offset) 221 { 222 if (offset >= 2 && value.substr(offset-2, 2) === "()") 223 return offset-1; 224 return offset; 225 } 226 }); 227 228 229 // Transform completions so that they don't add additional parentheses when 230 // ones already exist. 231 SelectorEditor.stripCompletedParens = function(list, postExpr) 232 { 233 var c = postExpr.charAt(0), rem = 0; 234 if (c === "(") 235 rem = 2; 236 else if (c === ")") 237 rem = 1; 238 else 239 return list; 240 return list.map(function(cl) 241 { 242 return (cl.slice(-2) === "()" ? cl.slice(0, -rem) : cl); 243 }); 244 }; 245 246 // ********************************************************************************************* // 247 // Registration 248 249 return SelectorEditor; 250 251 // ********************************************************************************************* // 252 }}); 253