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