1 /* See license.txt for terms of usage */
  2 
  3 define([
  4     "firebug/lib/object",
  5     "firebug/firebug",
  6     "firebug/chrome/firefox",
  7     "firebug/lib/domplate",
  8     "firebug/chrome/reps",
  9     "firebug/lib/xpcom",
 10     "firebug/lib/locale",
 11     "firebug/lib/events",
 12     "firebug/lib/url",
 13     "firebug/lib/array",
 14     "firebug/js/sourceLink",
 15     "firebug/lib/dom",
 16     "firebug/lib/css",
 17     "firebug/lib/xpath",
 18     "firebug/lib/fonts",
 19     "firebug/lib/options",
 20     "firebug/css/cssModule",
 21     "firebug/css/cssPanel",
 22     "firebug/chrome/menu"
 23 ],
 24 function(Obj, Firebug, Firefox, Domplate, FirebugReps, Xpcom, Locale, Events, Url, Arr,
 25     SourceLink, Dom, Css, Xpath, Fonts, Options, CSSModule, CSSStyleSheetPanel, Menu) {
 26 
 27 with (Domplate) {
 28 
 29 // ********************************************************************************************* //
 30 // Constants
 31 
 32 const Cc = Components.classes;
 33 const Ci = Components.interfaces;
 34 const nsIDOMCSSStyleRule = Ci.nsIDOMCSSStyleRule;
 35 
 36 // before firefox 6 getCSSStyleRules accepted only one argument
 37 const DOMUTILS_SUPPORTS_PSEUDOELEMENTS = Dom.domUtils.getCSSStyleRules.length > 1;
 38 
 39 // See: http://mxr.mozilla.org/mozilla1.9.2/source/content/events/public/nsIEventStateManager.h#153
 40 const STATE_ACTIVE  = 0x01;
 41 const STATE_FOCUS   = 0x02;
 42 const STATE_HOVER   = 0x04;
 43 
 44 // ********************************************************************************************* //
 45 // CSS Elemenet Panel (HTML side panel)
 46 
 47 function CSSStylePanel() {}
 48 
 49 CSSStylePanel.prototype = Obj.extend(CSSStyleSheetPanel.prototype,
 50 {
 51     template: domplate(
 52     {
 53         cascadedTag:
 54             DIV({"class": "a11yCSSView", role: "presentation"},
 55                 DIV({"class": "cssNonInherited", role: "list",
 56                         "aria-label": Locale.$STR("aria.labels.style rules") },
 57                     FOR("rule", "$rules",
 58                         TAG("$ruleTag", {rule: "$rule"})
 59                     )
 60                 ),
 61                 DIV({role: "list", "aria-label": Locale.$STR("aria.labels.inherited style rules")},
 62                     FOR("section", "$inherited",
 63                         H1({"class": "cssInheritHeader groupHeader focusRow", role: "listitem" },
 64                             SPAN({"class": "cssInheritLabel"}, "$inheritLabel"),
 65                             TAG(FirebugReps.Element.shortTag, {object: "$section.element"})
 66                         ),
 67                         DIV({role: "group"},
 68                             FOR("rule", "$section.rules",
 69                                 TAG("$ruleTag", {rule: "$rule"})
 70                             )
 71                         )
 72                     )
 73                  )
 74             ),
 75 
 76         ruleTag:
 77             DIV({"class": "cssElementRuleContainer"},
 78                 TAG(Firebug.CSSStyleRuleTag.tag, {rule: "$rule"}),
 79                 TAG(FirebugReps.SourceLink.tag, {object: "$rule.sourceLink"})
 80             ),
 81 
 82         newRuleTag:
 83             DIV({"class": "cssElementRuleContainer"},
 84                 DIV({"class": "cssRule insertBefore", style: "display: none"}, "")
 85             ),
 86 
 87         CSSFontPropValueTag:
 88             SPAN({"class": "cssFontPropValue"},
 89                 FOR("part", "$propValueParts",
 90                     SPAN({"class": "$part.type|getClass", _repObject: "$part.font"}, "$part.value"),
 91                     SPAN({"class": "cssFontPropSeparator"}, "$part|getSeparator")
 92                 )
 93             ),
 94 
 95         getSeparator: function(part)
 96         {
 97             if (part.lastFont || part.type == "important")
 98                 return "";
 99 
100             if (part.type == "otherProps")
101                 return " ";
102 
103             return ",";
104         },
105 
106         getClass: function(type)
107         {
108             switch (type)
109             {
110                 case "used":
111                     return "cssPropValueUsed";
112 
113                 case "unused":
114                     return "cssPropValueUnused";
115 
116                 default:
117                     return "";
118             }
119         }
120     }),
121 
122     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
123     // All calls to this method must call cleanupSheets first
124 
125     updateCascadeView: function(element)
126     {
127         Events.dispatch(this.fbListeners, "onBeforeCSSRulesAdded", [this]);
128 
129         var result, warning, inheritLabel;
130         var rules = [], sections = [], usedProps = {};
131 
132         this.getInheritedRules(element, sections, usedProps);
133         this.getElementRules(element, rules, usedProps);
134 
135         if (rules.length || sections.length)
136         {
137             // This removes overridden properties.
138             if (Firebug.onlyShowAppliedStyles)
139                 this.removeOverriddenProps(rules, sections);
140 
141             // This removes user agent rules
142             if (!Firebug.showUserAgentCSS)
143                 this.removeSystemRules(rules, sections);
144         }
145 
146         // Reset the selection, so that clicking that starts before the view
147         // update still result in proper mouseup events (issue 5500).
148         this.document.defaultView.getSelection().removeAllRanges();
149 
150         if (rules.length || sections.length)
151         {
152             inheritLabel = Locale.$STR("InheritedFrom");
153             result = this.template.cascadedTag.replace({rules: rules, inherited: sections,
154                 inheritLabel: inheritLabel}, this.panelNode);
155 
156             var props = result.getElementsByClassName("cssProp");
157 
158             for (var i = 0; i < props.length; i++)
159             {
160                 var prop = props[i];
161                 var propName = prop.getElementsByClassName("cssPropName").item(0).textContent;
162                 if (propName == "font-family" || propName == "font")
163                 {
164                     var propValueElem = prop.getElementsByClassName("cssPropValue").item(0);
165                     var propValue = propValueElem.textContent;
166                     var fontPropValueParts = getFontPropValueParts(element, propValue, propName);
167 
168                     // xxxsz: Web fonts not being loaded at display time
169                     // won't be marked as used. See issue 5420.
170                     this.template.CSSFontPropValueTag.replace({propValueParts: fontPropValueParts},
171                         propValueElem);
172                 }
173             }
174 
175             Events.dispatch(this.fbListeners, "onCSSRulesAdded", [this, result]);
176         }
177         else
178         {
179             warning = FirebugReps.Warning.tag.replace({object: ""}, this.panelNode);
180             result = FirebugReps.Description.render(Locale.$STR("css.EmptyElementCSS"),
181                 warning, Obj.bind(this.editElementStyle, this));
182 
183             Events.dispatch([Firebug.A11yModel], "onCSSRulesAdded", [this, result]);
184         }
185 
186         // Avoid a flickering "disable" icon by forcing a reflow (issue 5500).
187         this.panelNode.offsetHeight;
188     },
189 
190     getStylesheetURL: function(rule, getBaseUri)
191     {
192         // If parentStyleSheet.href is null, then per the CSS standard this is an inline style.
193         if (rule && rule.parentStyleSheet && rule.parentStyleSheet.href)
194             return rule.parentStyleSheet.href;
195         else if (getBaseUri)
196             return this.selection.ownerDocument.baseURI;
197         else
198             return this.selection.ownerDocument.location.href;
199     },
200 
201     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
202 
203     // All calls to this method must call cleanupSheets first
204     getInheritedRules: function(element, sections, usedProps)
205     {
206         var parent = element.parentNode;
207         if (parent && parent.nodeType == Node.ELEMENT_NODE)
208         {
209             this.getInheritedRules(parent, sections, usedProps);
210 
211             var rules = [];
212             this.getElementRules(parent, rules, usedProps, true);
213 
214             if (rules.length)
215                 sections.unshift({element: parent, rules: rules});
216         }
217     },
218 
219     // All calls to this method must call cleanupSheets first
220     getElementRules: function(element, rules, usedProps, inheritMode)
221     {
222         var pseudoElements = [""];
223         var inspectedRules, displayedRules = {};
224 
225         // Firefox 6+ allows inspecting of pseudo-elements (see issue 537)
226         if (DOMUTILS_SUPPORTS_PSEUDOELEMENTS && !inheritMode)
227             pseudoElements = Arr.extendArray(pseudoElements,
228                 [":first-letter", ":first-line", ":before", ":after"]);
229 
230         for (var p in pseudoElements)
231         {
232             try
233             {
234                 inspectedRules = Dom.domUtils.getCSSStyleRules(element, pseudoElements[p]);
235             }
236             catch (exc)
237             {
238                 continue;
239             }
240 
241             if (!inspectedRules)
242                 continue;
243 
244             for (var i = 0; i < inspectedRules.Count(); ++i)
245             {
246                 var rule = Xpcom.QI(inspectedRules.GetElementAt(i), nsIDOMCSSStyleRule);
247                 var isSystemSheet = Url.isSystemStyleSheet(rule.parentStyleSheet);
248 
249                 var props = this.getRuleProperties(this.context, rule, inheritMode);
250                 if (inheritMode && !props.length)
251                     continue;
252 
253                 var isPseudoElementSheet = (pseudoElements[p] != "");
254                 var sourceLink = this.getSourceLink(null, rule);
255 
256                 if (!isPseudoElementSheet)
257                     this.markOverriddenProps(element, props, usedProps, inheritMode);
258 
259                 rules.unshift({
260                     rule: rule,
261                     selector: rule.selectorText.replace(/ :/g, " *:"), // (issue 3683)
262                     sourceLink: sourceLink,
263                     props: props, inherited: inheritMode,
264                     isSystemSheet: isSystemSheet,
265                     isPseudoElementSheet: isPseudoElementSheet,
266                     isSelectorEditable: true
267                 });
268             }
269         }
270 
271         if (element.style)
272             this.getStyleProperties(element, rules, usedProps, inheritMode);
273 
274         if (FBTrace.DBG_CSS)
275             FBTrace.sysout("getElementRules " + rules.length + " rules for " +
276                 Xpath.getElementXPath(element), rules);
277     },
278 
279     markOverriddenProps: function(element, props, usedProps, inheritMode)
280     {
281         // Element can contain an invalid name (see issue 5303)
282         try
283         {
284             var dummyElement = element.ownerDocument.createElementNS(
285                 element.namespaceURI, element.tagName);
286         }
287         catch (err)
288         {
289             if (FBTrace.DBG_ERRORS)
290                 FBTrace.sysout("css.markOverriddenProps:", err);
291             return;
292         }
293 
294         for (var i=0; i<props.length; i++)
295         {
296             var prop = props[i];
297 
298             // Helper array for all shorthand properties for the current property.
299             prop.computed = {};
300 
301             // Get all shorthand propertis.
302             var dummyStyle = dummyElement.style;
303 
304             // xxxHonza: Not sure why this happens.
305             if (!dummyStyle)
306             {
307                 if (FBTrace.DBG_ERRORS)
308                     FBTrace.sysout("css.markOverridenProps; ERROR dummyStyle is NULL");
309                 return;
310             }
311 
312             dummyStyle.cssText = "";
313             dummyStyle.setProperty(prop.name, prop.value, prop.important);
314 
315             var length = dummyStyle.length;
316             for (var k=0; k<length; k++)
317             {
318                 var name = dummyStyle.item(k);
319 
320                 prop.computed[name] = {
321                     overridden: false
322                 };
323 
324                 if (usedProps.hasOwnProperty(name))
325                 {
326                     var deadProps = usedProps[name];
327 
328                     // all previous occurrences of this property
329                     for (var j=0; j<deadProps.length; j++)
330                     {
331                         var deadProp = deadProps[j];
332 
333                         // xxxHonza: fix for issue 3009, cross out even inherited properties
334                         //if (deadProp.wasInherited)
335                         //    continue;
336 
337                         if (!deadProp.disabled && deadProp.important && !prop.important)
338                         {
339                             // new occurrence overridden
340                             prop.overridden = true;
341 
342                             // Remember what exact shorthand property has been overridden.
343                             // This should help when we want to cross out only specific
344                             // part of the property value.
345                             if (prop.computed.hasOwnProperty(name))
346                                 prop.computed[name].overridden = true;
347                         }
348                         else if (!prop.disabled)
349                         {
350                             // previous occurrences overridden
351                             deadProp.overridden = true;
352 
353                             if (deadProp.computed.hasOwnProperty(name))
354                                 deadProp.computed[name].overridden = true;
355                         }
356                     }
357                 }
358                 else
359                 {
360                     usedProps[name] = [];
361                 }
362 
363                 // all occurrences of a property seen so far, by name
364                 usedProps[name].push(prop);
365             }
366 
367             prop.wasInherited = inheritMode ? true : false;
368         }
369     },
370 
371     removeOverriddenProps: function(rules, sections)
372     {
373         function removeProps(rules)
374         {
375             var i=0;
376             while (i<rules.length)
377             {
378                 var props = rules[i].props;
379 
380                 var j=0;
381                 while (j<props.length)
382                 {
383                     if (props[j].overridden)
384                         props.splice(j, 1);
385                     else
386                         ++j;
387                 }
388 
389                 if (props.length == 0)
390                     rules.splice(i, 1);
391                 else
392                     ++i;
393             }
394         }
395 
396         removeProps(rules);
397 
398         var i=0;
399         while (i < sections.length)
400         {
401             var section = sections[i];
402             removeProps(section.rules);
403 
404             if (section.rules.length == 0)
405                 sections.splice(i, 1);
406             else
407                 ++i;
408         }
409     },
410 
411     removeSystemRules: function(rules, sections)
412     {
413         function removeSystem(rules)
414         {
415             var i=0;
416             while (i<rules.length)
417             {
418                 if (rules[i].isSystemSheet)
419                     rules.splice(i, 1);
420                 else
421                     ++i;
422             }
423         }
424 
425         removeSystem(rules);
426 
427         var i=0;
428         while (i<sections.length)
429         {
430             var section = sections[i];
431             removeSystem(section.rules);
432 
433             if (section.rules.length == 0)
434                 sections.splice(i, 1);
435             else
436                 ++i;
437         }
438     },
439 
440     getStyleProperties: function(element, rules, usedProps, inheritMode)
441     {
442         var props = this.parseCSSProps(element.style, inheritMode);
443         this.addDisabledProperties(this.context, element, inheritMode, props);
444 
445         this.sortProperties(props);
446 
447         this.markOverriddenProps(element, props, usedProps, inheritMode);
448 
449         if (props.length)
450         {
451             rules.unshift({rule: element, selector: "element.style",
452                 props: props, inherited: inheritMode});
453         }
454     },
455 
456     inspectDeclaration: function(rule)
457     {
458         Firebug.chrome.select(rule, "stylesheet");
459     },
460 
461     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
462     // extends Panel
463 
464     name: "css",
465     parentPanel: "html",
466     order: 0,
467 
468     initialize: function()
469     {
470         this.onStateChange = Obj.bindFixed(this.contentStateCheck, this);
471         this.onHoverChange = Obj.bindFixed(this.contentStateCheck, this, STATE_HOVER);
472         this.onActiveChange = Obj.bindFixed(this.contentStateCheck, this, STATE_ACTIVE);
473 
474         CSSStyleSheetPanel.prototype.initialize.apply(this, arguments);
475     },
476 
477     show: function(state)
478     {
479         if (this.selection)
480             this.refresh();
481     },
482 
483     watchWindow: function(context, win)
484     {
485         if (Dom.domUtils)
486         {
487             // Normally these would not be required, but in order to update after the state is set
488             // using the options menu we need to monitor these global events as well
489             context.addEventListener(win, "mouseover", this.onHoverChange, false);
490             context.addEventListener(win, "mousedown", this.onActiveChange, false);
491         }
492     },
493 
494     unwatchWindow: function(context, win)
495     {
496         context.removeEventListener(win, "mouseover", this.onHoverChange, false);
497         context.removeEventListener(win, "mousedown", this.onActiveChange, false);
498 
499         var doc = win.document;
500         if (Dom.isAncestor(this.stateChangeEl, doc))
501             this.removeStateChangeHandlers();
502     },
503 
504     supportsObject: function(object, type)
505     {
506         return object instanceof window.Element ? 1 : 0;
507     },
508 
509     updateView: function(element)
510     {
511         var result = CSSModule.cleanupSheets(element.ownerDocument, Firebug.currentContext);
512 
513         // If cleanupSheets returns false there was an exception thrown when accessing
514         // a styleshet (probably since it isn't fully loaded yet). So, delay the panel
515         // update and try it again a bit later (issue 5654).
516         if (!result)
517         {
518             this.context.setTimeout(Obj.bindFixed(this.updateView, this, element), 200);
519             return;
520         }
521 
522         // All stylesheets should be ready now, update the view.
523         this.updateCascadeView(element);
524 
525         if (Dom.domUtils)
526         {
527             this.contentState = safeGetContentState(element);
528             this.addStateChangeHandlers(element);
529         }
530     },
531 
532     updateSelection: function(element)
533     {
534         if (!(element instanceof window.Element)) // html supports SourceLink
535             return;
536 
537         var sothinkInstalled = !!Firefox.getElementById("swfcatcherKey_sidebar");
538         if (sothinkInstalled)
539         {
540             var div = FirebugReps.Warning.tag.replace({object: "SothinkWarning"}, this.panelNode);
541             div.textContent = Locale.$STR("SothinkWarning");
542             return;
543         }
544 
545         this.updateView(element);
546     },
547 
548     updateOption: function(name, value)
549     {
550         var options = new Set();
551         options.add("onlyShowAppliedStyles");
552         options.add("showUserAgentCSS");
553         options.add("expandShorthandProps");
554         options.add("colorDisplay");
555         options.add("showMozillaSpecificStyles");
556 
557         if (options.has(name))
558             this.refresh();
559     },
560 
561     getOptionsMenuItems: function()
562     {
563         var items = [
564             Menu.optionMenu("Only_Show_Applied_Styles", "onlyShowAppliedStyles",
565                 "style.option.tip.Only_Show_Applied_Styles"),
566             Menu.optionMenu("Show_User_Agent_CSS", "showUserAgentCSS",
567                 "style.option.tip.Show_User_Agent_CSS"),
568             Menu.optionMenu("Expand_Shorthand_Properties", "expandShorthandProps",
569                 "css.option.tip.Expand_Shorthand_Properties")
570         ];
571 
572         items = Arr.extendArray(items, CSSModule.getColorDisplayOptionMenuItems());
573 
574         if (Dom.domUtils && this.selection)
575         {
576             var self = this;
577 
578             items.push(
579                 "-",
580                 {
581                     label: "style.option.label.hover",
582                     type: "checkbox",
583                     checked: self.hasPseudoClassLock(":hover"),
584                     tooltiptext: "style.option.tip.hover",
585                     command: function()
586                     {
587                         self.togglePseudoClassLock(":hover");
588                     }
589                 },
590                 {
591                     label: "style.option.label.active",
592                     type: "checkbox",
593                     checked: self.hasPseudoClassLock(":active"),
594                     tooltiptext: "style.option.tip.active",
595                     command: function()
596                     {
597                         self.togglePseudoClassLock(":active");
598                     }
599                 }
600             );
601 
602             if (Dom.domUtils.hasPseudoClassLock)
603             {
604                 items.push(
605                     {
606                         label: "style.option.label.focus",
607                         type: "checkbox",
608                         checked: self.hasPseudoClassLock(":focus"),
609                         tooltiptext: "style.option.tip.focus",
610                         command: function()
611                         {
612                             self.togglePseudoClassLock(":focus");
613                         }
614                     }
615                 );
616             }
617         }
618 
619         return items;
620     },
621 
622     getContextMenuItems: function(style, target)
623     {
624         var items = CSSStyleSheetPanel.prototype.getContextMenuItems.apply(this, [style, target]);
625         var insertIndex = 0;
626 
627         for (var i = 0; i < items.length; ++i)
628         {
629             if (items[i].id == "fbNewCSSRule")
630             {
631                 items.splice(i, 1);
632                 insertIndex = i;
633                 break;
634             }
635         }
636 
637         items.splice(insertIndex, 0, {
638             label: "EditStyle",
639             tooltiptext: "style.tip.Edit_Style",
640             command: Obj.bindFixed(this.editElementStyle, this)
641         },
642         {
643             label: "AddRule",
644             tooltiptext: "css.tip.AddRule",
645             command: Obj.bindFixed(this.addRelatedRule, this)
646         });
647 
648         if (style instanceof Ci.nsIDOMFontFace && style.rule)
649         {
650             items.push(
651                 "-",
652                 {
653                     label: "css.label.Inspect_Declaration",
654                     tooltiptext: "css.tip.Inspect_Declaration",
655                     id: "fbInspectDeclaration",
656                     command: Obj.bindFixed(this.inspectDeclaration, this, style.rule)
657                 }
658             );
659         }
660 
661         return items;
662     },
663 
664     showInfoTip: function(infoTip, target, x, y, rangeParent, rangeOffset)
665     {
666         var prop = Dom.getAncestorByClass(target, "cssProp");
667         if (prop)
668             var propNameNode = prop.getElementsByClassName("cssPropName").item(0);
669 
670         if (propNameNode && (propNameNode.textContent.toLowerCase() == "font" ||
671             propNameNode.textContent.toLowerCase() == "font-family"))
672         {
673             var prevSibling = target.previousElementSibling;
674             while (prevSibling)
675             {
676                 rangeOffset += prevSibling.textContent.length;
677                 prevSibling = prevSibling.previousElementSibling;
678             }
679         }
680 
681         return CSSStyleSheetPanel.prototype.showInfoTip.call(
682             this, infoTip, target, x, y, rangeParent, rangeOffset);
683     },
684 
685     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
686     // Extends stylesheet (CSS Panel)
687 
688     deleteRuleDeclaration: function(cssSelector)
689     {
690         var repObject = Firebug.getRepObject(cssSelector);
691 
692         if (repObject instanceof window.Element)
693             CSSModule.deleteRule(repObject);
694         else
695             CSSStyleSheetPanel.prototype.deleteRuleDeclaration(cssSelector);
696 
697         this.refresh();
698     },
699 
700     // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
701 
702     hasPseudoClassLock: function(pseudoClass)
703     {
704         if (Dom.domUtils.hasPseudoClassLock)
705         {
706             return Dom.domUtils.hasPseudoClassLock(this.selection, pseudoClass);
707         }
708         else
709         {
710             // Fallback in case the new pseudo-class lock API isn't available
711             var state = safeGetContentState(this.selection);
712             switch(pseudoClass)
713             {
714                 case ":active":
715                     return state & STATE_ACTIVE;
716 
717                 case ":hover":
718                     return state & STATE_HOVER;
719             }
720         }
721     },
722 
723     togglePseudoClassLock: function(pseudoClass)
724     {
725         if (FBTrace.DBG_CSS)
726             FBTrace.sysout("css.togglePseudoClassLock; pseudo-class: " + pseudoClass);
727 
728         if (Dom.domUtils.hasPseudoClassLock)
729         {
730             if (Dom.domUtils.hasPseudoClassLock(this.selection, pseudoClass))
731                 Dom.domUtils.removePseudoClassLock(this.selection, pseudoClass);
732             else
733                 Dom.domUtils.addPseudoClassLock(this.selection, pseudoClass);
734         }
735         else
736         {
737             // Fallback in case the new pseudo-class lock API isn't available
738             var currentState = safeGetContentState(this.selection);
739             var remove = false;
740             switch(pseudoClass)
741             {
742                 case ":active":
743                     state = STATE_ACTIVE;
744                     break;
745 
746                 case ":hover":
747                     state = STATE_HOVER;
748                     break;
749             }
750             remove = currentState & state;
751 
752             Dom.domUtils.setContentState(remove ? this.selection.ownerDocument.documentElement :
753                 this.selection, state);
754         }
755 
756         this.refresh();
757     },
758 
759     clearPseudoClassLocks: function()
760     {
761         if (Dom.domUtils.clearPseudoClassLocks)
762             Dom.domUtils.clearPseudoClassLocks(this.selection);
763     },
764 
765     addStateChangeHandlers: function(el)
766     {
767         this.removeStateChangeHandlers();
768 
769         Events.addEventListener(el, "focus", this.onStateChange, true);
770         Events.addEventListener(el, "blur", this.onStateChange, true);
771         Events.addEventListener(el, "mouseup", this.onStateChange, false);
772         Events.addEventListener(el, "mousedown", this.onStateChange, false);
773         Events.addEventListener(el, "mouseover", this.onStateChange, false);
774         Events.addEventListener(el, "mouseout", this.onStateChange, false);
775 
776         this.stateChangeEl = el;
777     },
778 
779     removeStateChangeHandlers: function()
780     {
781         var sel = this.stateChangeEl;
782         if (sel)
783         {
784             Events.removeEventListener(sel, "focus", this.onStateChange, true);
785             Events.removeEventListener(sel, "blur", this.onStateChange, true);
786             Events.removeEventListener(sel, "mouseup", this.onStateChange, false);
787             Events.removeEventListener(sel, "mousedown", this.onStateChange, false);
788             Events.removeEventListener(sel, "mouseover", this.onStateChange, false);
789             Events.removeEventListener(sel, "mouseout", this.onStateChange, false);
790         }
791 
792         this.stateChangeEl = null;
793     },
794 
795     contentStateCheck: function(state)
796     {
797         if (!state || this.contentState & state)
798         {
799             var timeoutRunner = Obj.bindFixed(function()
800             {
801                 var newState = safeGetContentState(this.selection);
802                 if (newState != this.contentState)
803                 {
804                     this.context.invalidatePanels(this.name);
805                 }
806             }, this);
807 
808             // Delay exec until after the event has processed and the state has been updated
809             setTimeout(timeoutRunner, 0);
810       }
811     }
812 });
813 
814 // ********************************************************************************************* //
815 // Helpers
816 
817 function safeGetContentState(selection)
818 {
819     try
820     {
821         if (selection && selection.ownerDocument)
822             return Dom.domUtils.getContentState(selection);
823     }
824     catch (e)
825     {
826         if (FBTrace.DBG_ERRORS && FBTrace.DBG_CSS)
827             FBTrace.sysout("css.safeGetContentState; EXCEPTION "+e, e);
828     }
829 }
830 
831 function getFontPropValueParts(element, value, propName)
832 {
833     const genericFontFamilies =
834     {
835         "serif": 1,
836         "sans-serif": 1,
837         "cursive": 1,
838         "fantasy": 1,
839         "monospace": 1,
840     };
841 
842     var parts = [], origValue = value;
843 
844     // (Mirroring CSSModule.parseCSSFontFamilyValue)
845     if (propName === "font")
846     {
847         var rePreFont = new RegExp(
848             "^.*" + // anything, then
849             "(" +
850                 "\\d+(\\.\\d+)?([a-z]*|%)|" + // a number (with possible unit)
851                 "(x{1,2}-)?(small|large)|medium|larger|smaller" + // or an named size description
852             ") "
853         );
854         var matches = rePreFont.exec(value);
855         if (!matches)
856         {
857             // Non-simple font value, like "inherit", "status-bar" or
858             // "calc(12px) Arial" - just return the whole text.
859             return [{type: "otherProps", value: value, lastFont: true}];
860         }
861         var preProps = matches[0].slice(0, -1);
862         parts.push({type: "otherProps", value: preProps});
863         value = value.substr(matches[0].length);
864     }
865 
866     var matches = /^(.*?)( !important)?$/.exec(value);
867     var fonts = matches[1].split(",");
868 
869     // What we want to know is what the specified "font-family" property means
870     // for the selected element's text, not what the element actually uses (that
871     // depends on font styles of its descendants). Thus, we just check the direct
872     // child text nodes of the element.
873     // Do not create a temporary element for testing to avoid problems like in
874     // issue 5905 and 6048
875     var usedFonts = [];
876     var child = element.firstChild;
877     do
878     {
879         if (!child)
880             break;
881 
882         if (child.nodeType == Node.TEXT_NODE)
883             usedFonts = Arr.extendArray(usedFonts, Fonts.getFonts(child));
884     }
885     while (child = child.nextSibling);
886 
887     var genericFontUsed = false;
888     for (var i = 0; i < fonts.length; ++i)
889     {
890         var font = fonts[i].replace(/^["'](.*)["']$/, "$1").toLowerCase();
891         var isGeneric = genericFontFamilies.hasOwnProperty(font);
892         var isUsedFont = false;
893 
894         for (var j = 0; j < usedFonts.length; ++j)
895         {
896             var usedFont = usedFonts[j].CSSFamilyName.toLowerCase();
897             if (font == usedFont || (isGeneric && !genericFontUsed))
898             {
899                 parts.push({type: "used", value: fonts[i], font: usedFonts[j]});
900                 usedFonts.splice(j, 1);
901 
902                 isUsedFont = true;
903                 if (isGeneric)
904                     genericFontUsed = true;
905                 break;
906             }
907         }
908 
909         if (!isUsedFont)
910             parts.push({type: "unused", value: fonts[i]});
911     }
912 
913     // xxxsz: Domplate doesn't allow to check for the last element in an array yet,
914     // so use this as hack
915     parts[parts.length-1].lastFont = true;
916 
917     if (matches[2])
918         parts.push({type: "important", value: " !important"});
919 
920     return parts;
921 }
922 
923 // ********************************************************************************************* //
924 // Registration
925 
926 Firebug.registerPanel(CSSStylePanel);
927 
928 return CSSStylePanel;
929 
930 // ********************************************************************************************* //
931 }});
932