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