1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/lib/xpcom",
  7     "firebug/lib/events",
  8     "firebug/lib/url",
  9     "firebug/lib/css",
 10     "firebug/chrome/window",
 11     "firebug/lib/xml",
 12     "firebug/lib/options",
 13     "firebug/lib/array",
 14     "firebug/editor/editorSelector"
 15 ],
 16 function(Obj, Firebug, Xpcom, Events, Url, Css, Win, Xml, Options, Arr, EditorSelector) {
 17 
 18 // ********************************************************************************************* //
 19 // Constants
 20 
 21 const Cc = Components.classes;
 22 const Ci = Components.interfaces;
 23 
 24 const reSplitCSS = /(url\("?[^"\)]+"?\)?)|(rgba?\([^)]*\)?)|(hsla?\([^)]*\)?)|(#[\dA-Fa-f]+)|(-?\d+(\.\d+)?(%|[a-z]{1,4})?)|"([^"]*)"?|'([^']*)'?|([^,\s\/!\(\)]+)|(!(.*)?)/;
 25 const reURL = /url\("?([^"\)]+)?"?\)/;
 26 const reRepeat = /no-repeat|repeat-x|repeat-y|repeat/;
 27 
 28 // ********************************************************************************************* //
 29 // CSS Module
 30 
 31 Firebug.CSSModule = Obj.extend(Firebug.Module, Firebug.EditorSelector,
 32 {
 33     dispatchName: "cssModule",
 34 
 35     freeEdit: function(styleSheet, value)
 36     {
 37         if (FBTrace.DBG_CSS)
 38             FBTrace.sysout("CSSModule.freeEdit", arguments);
 39 
 40         if (!styleSheet.editStyleSheet)
 41         {
 42             var ownerNode = getStyleSheetOwnerNode(styleSheet);
 43             styleSheet.disabled = true;
 44 
 45             var url = Xpcom.CCSV("@mozilla.org/network/standard-url;1", Ci.nsIURL);
 46             url.spec = styleSheet.href;
 47 
 48             var editStyleSheet = ownerNode.ownerDocument.createElementNS(
 49                 "http://www.w3.org/1999/xhtml",
 50                 "style");
 51 
 52             Firebug.setIgnored(editStyleSheet);
 53 
 54             editStyleSheet.setAttribute("type", "text/css");
 55             editStyleSheet.setAttributeNS(
 56                 "http://www.w3.org/XML/1998/namespace",
 57                 "base",
 58                 url.directory);
 59 
 60             if (ownerNode.hasAttribute("media"))
 61                 editStyleSheet.setAttribute("media", ownerNode.getAttribute("media"));
 62 
 63             // Insert the edited stylesheet directly after the old one to ensure the styles
 64             // cascade properly.
 65             ownerNode.parentNode.insertBefore(editStyleSheet, ownerNode.nextSibling);
 66 
 67             styleSheet.editStyleSheet = editStyleSheet;
 68         }
 69 
 70         styleSheet.editStyleSheet.innerHTML = value;
 71 
 72         if (FBTrace.DBG_CSS)
 73             FBTrace.sysout("css.saveEdit styleSheet.href:" + styleSheet.href +
 74                 " got innerHTML:" + value);
 75 
 76         Events.dispatch(this.fbListeners, "onCSSFreeEdit", [styleSheet, value]);
 77     },
 78 
 79     insertRule: function(styleSheet, cssText, ruleIndex)
 80     {
 81         if (FBTrace.DBG_CSS)
 82             FBTrace.sysout("Insert: " + ruleIndex + " " + cssText);
 83 
 84         var insertIndex = styleSheet.insertRule(cssText, ruleIndex);
 85 
 86         Events.dispatch(this.fbListeners, "onCSSInsertRule", [styleSheet, cssText, ruleIndex]);
 87 
 88         return insertIndex;
 89     },
 90 
 91     deleteRule: function(src, ruleIndex)
 92     {
 93         var inlineStyle = (src instanceof window.Element);
 94         if (FBTrace.DBG_CSS)
 95         {
 96             if (inlineStyle)
 97             {
 98                 FBTrace.sysout("deleteRule: element.style", src);
 99             }
100             else
101             {
102                 FBTrace.sysout("deleteRule: " + ruleIndex + " " + src.cssRules.length,
103                     src.cssRules);
104             }
105         }
106 
107         var rule = (inlineStyle ? src : src.cssRules[ruleIndex]);
108         var afterParams = [src, rule.style.cssText];
109         afterParams.push(inlineStyle ? "" : rule.selectorText);
110 
111         Events.dispatch(this.fbListeners, "onCSSDeleteRule", [src, ruleIndex]);
112 
113         if (src instanceof window.Element)
114             src.removeAttribute("style");
115         else
116             src.deleteRule(ruleIndex);
117 
118         Events.dispatch(this.fbListeners, "onAfterCSSDeleteRule", afterParams);
119     },
120 
121     setProperty: function(rule, propName, propValue, propPriority)
122     {
123         var style = rule.style || rule;
124 
125         // Record the original CSS text for the inline case so we can reconstruct at a later
126         // point for diffing purposes
127         var baseText = style.cssText;
128 
129         var prevValue = style.getPropertyValue(propName);
130         var prevPriority = style.getPropertyPriority(propName);
131 
132         // XXXjoe Gecko bug workaround: Just changing priority doesn't have any effect
133         // unless we remove the property first
134         style.removeProperty(propName);
135 
136         style.setProperty(propName, propValue, propPriority);
137 
138         if (propName)
139         {
140             Events.dispatch(this.fbListeners, "onCSSSetProperty", [style, propName, propValue,
141                 propPriority, prevValue, prevPriority, rule, baseText]);
142         }
143     },
144 
145     removeProperty: function(rule, propName, parent)
146     {
147         var style = rule.style || rule;
148 
149         // Record the original CSS text for the inline case so we can reconstruct at a later
150         // point for diffing purposes
151         var baseText = style.cssText;
152 
153         var prevValue = style.getPropertyValue(propName);
154         var prevPriority = style.getPropertyPriority(propName);
155 
156         style.removeProperty(propName);
157 
158         if (propName)
159             Events.dispatch(this.fbListeners, "onCSSRemoveProperty", [style, propName, prevValue,
160                 prevPriority, rule, baseText]);
161     },
162 
163     /**
164      * Method for atomic property removal, such as through the context menu.
165      */
166     deleteProperty: function(rule, propName, context)
167     {
168         Events.dispatch(this.fbListeners, "onBeginFirebugChange", [rule, context]);
169         Firebug.CSSModule.removeProperty(rule, propName);
170         Events.dispatch(this.fbListeners, "onEndFirebugChange", [rule, context]);
171     },
172 
173     disableProperty: function(disable, rule, propName, parsedValue, map, context)
174     {
175         Events.dispatch(this.fbListeners, "onBeginFirebugChange", [rule, context]);
176 
177         if (disable)
178         {
179             Firebug.CSSModule.removeProperty(rule, propName);
180 
181             map.push({"name": propName, "value": parsedValue.value,
182                 "important": parsedValue.priority});
183         }
184         else
185         {
186             Firebug.CSSModule.setProperty(rule, propName, parsedValue.value, parsedValue.priority);
187 
188             var index = findPropByName(map, propName);
189             map.splice(index, 1);
190         }
191 
192         Events.dispatch(this.fbListeners, "onEndFirebugChange", [rule, context]);
193     },
194 
195     /**
196      * Get a document's temporary stylesheet for storage of user-provided rules.
197      * If it doesn't exist yet, create it.
198      */
199     getDefaultStyleSheet: function(doc)
200     {
201         // Cache the temporary sheet on an expando of the document.
202         var sheet = doc.fbDefaultSheet;
203         if (!sheet)
204         {
205             sheet = Css.appendStylesheet(doc, "chrome://firebug/default-stylesheet.css").sheet;
206             sheet.defaultStylesheet = true;
207             doc.fbDefaultSheet = sheet;
208         }
209         return sheet;
210     },
211 
212     cleanupSheets: function(doc, context)
213     {
214         if (!context)
215             return false;
216 
217         // Due to the manner in which the layout engine handles multiple
218         // references to the same sheet we need to kick it a little bit.
219         // The injecting a simple stylesheet then removing it will force
220         // Firefox to regenerate it's CSS hierarchy.
221         //
222         // WARN: This behavior was determined anecdotally.
223         // See http://code.google.com/p/fbug/issues/detail?id=2440
224 
225         // This causes additional HTTP request for a font (see 4649).
226         /*if (!Xml.isXMLPrettyPrint(context))
227         {
228             var style = Css.createStyleSheet(doc);
229             style.textContent = "#fbIgnoreStyleDO_NOT_USE {}";
230             Css.addStyleSheet(doc, style);
231 
232             if (style.parentNode)
233             {
234                 style.parentNode.removeChild(style);
235             }
236             else
237             {
238                 if (FBTrace.DBG_ERRORS)
239                     FBTrace.sysout("css.cleanupSheets; ERROR no parent style:", style);
240             }
241         }*/
242 
243         var result = true;
244 
245         // https://bugzilla.mozilla.org/show_bug.cgi?id=500365
246         // This voodoo touches each style sheet to force some Firefox internal change
247         // to allow edits.
248         var styleSheets = Css.getAllStyleSheets(context);
249         for(var i = 0; i < styleSheets.length; i++)
250         {
251             try
252             {
253                 var rules = styleSheets[i].cssRules;
254                 if (rules.length > 0)
255                     var touch = rules[0];
256 
257                 //if (FBTrace.DBG_CSS && touch)
258                 //    FBTrace.sysout("css.show() touch "+typeof(touch)+" in "+
259                 //        (styleSheets[i].href?styleSheets[i].href:context.getName()));
260             }
261             catch(e)
262             {
263                 result = false;
264 
265                 if (FBTrace.DBG_ERRORS)
266                     FBTrace.sysout("css.show: sheet.cssRules FAILS for " +
267                         (styleSheets[i] ? styleSheets[i].href : "null sheet") + e, e);
268             }
269         }
270 
271         // Return true only if all stylesheets are fully loaded and there is no
272         // excpetion when accessing them.
273         return result;
274     },
275 
276     cleanupSheetHandler: function(event, context)
277     {
278         var target = event.target;
279         var tagName = (target.tagName || "").toLowerCase();
280 
281         if (tagName == "link")
282             this.cleanupSheets(target.ownerDocument, context);
283     },
284 
285     parseCSSValue: function(value, offset)
286     {
287         var start = 0;
288         var m;
289         while (true)
290         {
291             m = reSplitCSS.exec(value);
292             if (m && m.index+m[0].length < offset)
293             {
294                 value = value.substr(m.index+m[0].length);
295                 start += m.index+m[0].length;
296                 offset -= m.index+m[0].length;
297             }
298             else
299                 break;
300         }
301 
302         if (!m)
303             return;
304 
305         var type;
306         if (m[1])
307             type = "url";
308         else if (m[2] || m[4])
309             type = "rgb";
310         else if (m[3])
311             type = "hsl";
312         else if (m[5])
313             type = "int";
314 
315         var cssValue = {value: m[0], start: start+m.index, end: start+m.index+m[0].length, type: type};
316 
317         if (!type)
318         {
319             if (m[10] && m[10].indexOf("gradient") != -1)
320             {
321                 var arg = value.substr(m[0].length).match(/\((?:(?:[^\(\)]*)|(?:\(.*?\)))+\)/);
322                 if (!arg)
323                   return;
324 
325                 cssValue.value += arg[0];
326                 cssValue.type = "gradient";
327             }
328             else if (Css.isColorKeyword(cssValue.value))
329             {
330                 cssValue.type = "colorKeyword";
331             }
332         }
333 
334         return cssValue;
335     },
336 
337     parseCSSFontFamilyValue: function(value, offset, propName)
338     {
339         var skipped = 0;
340         if (propName === "font")
341         {
342             var rePreFont = new RegExp(
343                 "^.*" + // anything, then
344                 "(" +
345                     "\\d+(\\.\\d+)?([a-z]*|%)|" + // a number (with possible unit)
346                     "(x{1,2}-)?(small|large)|medium|larger|smaller" + // or an named size description
347                 ") "
348             );
349             var m = rePreFont.exec(value);
350             if (!m || offset < m[0].length)
351                 return this.parseCSSValue(value, offset);
352             skipped = m[0].length;
353             value = value.substr(skipped);
354             offset -= skipped;
355         }
356 
357         var matches = /^(.*?)(\s*!.*)?$/.exec(value);
358         var fonts = matches[1].split(",");
359 
360         var totalLength = 0;
361         for (var i = 0; i < fonts.length; ++i)
362         {
363             totalLength += fonts[i].length;
364             if (offset <= totalLength)
365             {
366                 // Give back the value and location of this font, whitespace-trimmed.
367                 var font = fonts[i].replace(/^\s+/, "");
368                 var end = totalLength;
369                 var start = end - font.length;
370                 return {
371                     value: font,
372                     start: start + skipped,
373                     end: end + skipped,
374                     type: "fontFamily"
375                 };
376             }
377 
378             // include ","
379             ++totalLength;
380         }
381 
382         // Parse !important.
383         var ret = this.parseCSSValue(value, offset);
384         if (ret)
385         {
386             ret.start += skipped;
387             ret.end += skipped;
388         }
389         return ret;
390     },
391 
392     parseURLValue: function(value)
393     {
394         var m = reURL.exec(value);
395         return m ? m[1] : "";
396     },
397 
398     parseRepeatValue: function(value)
399     {
400         var m = reRepeat.exec(value);
401         return m ? m[0] : "";
402     },
403 
404     getPropertyInfo: function(computedStyle, propName)
405     {
406         var propInfo = {
407             property: propName,
408             value: computedStyle.getPropertyValue(propName),
409             matchedSelectors: [],
410             matchedRuleCount: 0
411         };
412 
413         return propInfo;
414     },
415 
416     getColorDisplayOptionMenuItems: function()
417     {
418         return [
419             "-",
420             {
421                 label: "computed.option.label.Colors_As_Hex",
422                 tooltiptext: "computed.option.tip.Colors_As_Hex",
423                 type: "radio",
424                 name: "colorDisplay",
425                 id: "colorDisplayHex",
426                 command: function() {
427                     return Options.set("colorDisplay", "hex");
428                 },
429                 checked: Options.get("colorDisplay") == "hex"
430             },
431             {
432                 label: "computed.option.label.Colors_As_RGB",
433                 tooltiptext: "computed.option.tip.Colors_As_RGB",
434                 type: "radio",
435                 name: "colorDisplay",
436                 id: "colorDisplayRGB",
437                 command: function() {
438                     return Options.set("colorDisplay", "rgb");
439                 },
440                 checked: Options.get("colorDisplay") == "rgb"
441             },
442             {
443                 label: "computed.option.label.Colors_As_HSL",
444                 tooltiptext: "computed.option.tip.Colors_As_HSL",
445                 type: "radio",
446                 name: "colorDisplay",
447                 id: "colorDisplayHSL",
448                 command: function() {
449                     return Options.set("colorDisplay", "hsl");
450                 },
451                 checked: Options.get("colorDisplay") == "hsl"
452             }
453         ];
454     },
455 
456     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
457     // Module functions
458 
459     initialize: function()
460     {
461         this.editors = {};
462         this.registerEditor("Live",
463         {
464             startEditing: function(stylesheet, context, panel)
465             {
466                 panel.startLiveEditing(stylesheet, context);
467             },
468             stopEditing: function()
469             {
470                 Firebug.Editor.stopEditing();
471             }
472         });
473 
474         this.registerEditor("Source",
475         {
476             startEditing: function(stylesheet, context, panel)
477             {
478                 panel.startSourceEditing(stylesheet, context);
479             },
480             stopEditing: function()
481             {
482                 Firebug.Editor.stopEditing();
483             }
484         });
485     },
486 
487     watchWindow: function(context, win)
488     {
489         if (!context.cleanupSheetListener)
490             context.cleanupSheetListener = Obj.bind(this.cleanupSheetHandler, this, context);
491 
492         context.addEventListener(win, "DOMAttrModified", context.cleanupSheetListener, false);
493         context.addEventListener(win, "DOMNodeInserted", context.cleanupSheetListener, false);
494     },
495 
496     unwatchWindow: function(context, win)
497     {
498         if (context.cleanupSheetListener)
499         {
500             context.removeEventListener(win, "DOMAttrModified", context.cleanupSheetListener, false);
501             context.removeEventListener(win, "DOMNodeInserted", context.cleanupSheetListener, false);
502         }
503     },
504 
505     loadedContext: function(context)
506     {
507         var self = this;
508         Win.iterateWindows(context.browser.contentWindow, function(subwin)
509         {
510             self.cleanupSheets(subwin.document, context);
511         });
512     },
513 
514     initContext: function(context)
515     {
516         context.dirtyListener = new Firebug.CSSDirtyListener(context);
517         this.addListener(context.dirtyListener);
518     },
519 
520     destroyContext: function(context)
521     {
522         this.removeListener(context.dirtyListener);
523     }
524 });
525 
526 // ********************************************************************************************* //
527 // Helpers
528 
529 function getStyleSheetOwnerNode(sheet)
530 {
531     for (; sheet && !sheet.ownerNode; sheet = sheet.parentStyleSheet);
532 
533     return sheet.ownerNode;
534 }
535 
536 function findPropByName(props, name)
537 {
538     for (var i = 0; i < props.length; ++i)
539     {
540         if (props[i].name == name)
541             return i;
542     }
543 }
544 
545 // ********************************************************************************************* //
546 // Registration
547 
548 Firebug.registerModule(Firebug.CSSModule);
549 
550 return Firebug.CSSModule;
551 
552 // ********************************************************************************************* //
553 });
554