1 /* 2 * Copyright (C) 2010 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 WebInspector.CSSStyleModel = function() 32 { 33 new WebInspector.CSSStyleModelResourceBinding(this); 34 } 35 36 WebInspector.CSSStyleModel.parseRuleArrayPayload = function(ruleArray) 37 { 38 var result = []; 39 for (var i = 0; i < ruleArray.length; ++i) 40 result.push(WebInspector.CSSRule.parsePayload(ruleArray[i])); 41 return result; 42 } 43 44 WebInspector.CSSStyleModel.Events = { 45 StyleSheetChanged: 0 46 } 47 48 WebInspector.CSSStyleModel.prototype = { 49 getStylesAsync: function(nodeId, userCallback) 50 { 51 function callback(userCallback, error, payload) 52 { 53 if (error) { 54 if (userCallback) 55 userCallback(null); 56 return; 57 } 58 59 var result = {}; 60 if ("inlineStyle" in payload) 61 result.inlineStyle = WebInspector.CSSStyleDeclaration.parsePayload(payload.inlineStyle); 62 63 result.computedStyle = WebInspector.CSSStyleDeclaration.parsePayload(payload.computedStyle); 64 result.matchedCSSRules = WebInspector.CSSStyleModel.parseRuleArrayPayload(payload.matchedCSSRules); 65 66 result.styleAttributes = {}; 67 var payloadStyleAttributes = payload.styleAttributes; 68 for (var i = 0; i < payloadStyleAttributes.length; ++i) { 69 var name = payloadStyleAttributes[i].name; 70 result.styleAttributes[name] = WebInspector.CSSStyleDeclaration.parsePayload(payloadStyleAttributes[i].style); 71 } 72 73 result.pseudoElements = []; 74 for (var i = 0; i < payload.pseudoElements.length; ++i) { 75 var entryPayload = payload.pseudoElements[i]; 76 result.pseudoElements.push({ pseudoId: entryPayload.pseudoId, rules: WebInspector.CSSStyleModel.parseRuleArrayPayload(entryPayload.rules) }); 77 } 78 79 result.inherited = []; 80 for (var i = 0; i < payload.inherited.length; ++i) { 81 var entryPayload = payload.inherited[i]; 82 var entry = {}; 83 if ("inlineStyle" in entryPayload) 84 entry.inlineStyle = WebInspector.CSSStyleDeclaration.parsePayload(entryPayload.inlineStyle); 85 if ("matchedCSSRules" in entryPayload) 86 entry.matchedCSSRules = WebInspector.CSSStyleModel.parseRuleArrayPayload(entryPayload.matchedCSSRules); 87 result.inherited.push(entry); 88 } 89 90 if (userCallback) 91 userCallback(result); 92 } 93 94 CSSAgent.getStylesForNode(nodeId, callback.bind(null, userCallback)); 95 }, 96 97 getComputedStyleAsync: function(nodeId, userCallback) 98 { 99 function callback(userCallback, error, stylePayload) 100 { 101 if (error) 102 userCallback(null); 103 else 104 userCallback(WebInspector.CSSStyleDeclaration.parsePayload(stylePayload)); 105 } 106 107 CSSAgent.getComputedStyleForNode(nodeId, callback.bind(null, userCallback)); 108 }, 109 110 getInlineStyleAsync: function(nodeId, userCallback) 111 { 112 function callback(userCallback, error, stylePayload) 113 { 114 if (error) 115 userCallback(null); 116 else 117 userCallback(WebInspector.CSSStyleDeclaration.parsePayload(stylePayload)); 118 } 119 120 CSSAgent.getInlineStyleForNode(nodeId, callback.bind(null, userCallback)); 121 }, 122 123 setRuleSelector: function(ruleId, nodeId, newSelector, successCallback, failureCallback) 124 { 125 function checkAffectsCallback(nodeId, successCallback, rulePayload, selectedNodeIds) 126 { 127 if (!selectedNodeIds) 128 return; 129 var doesAffectSelectedNode = (selectedNodeIds.indexOf(nodeId) >= 0); 130 var rule = WebInspector.CSSRule.parsePayload(rulePayload); 131 successCallback(rule, doesAffectSelectedNode); 132 this._fireStyleSheetChanged(rule.id.styleSheetId, true); 133 } 134 135 function callback(nodeId, successCallback, failureCallback, error, newSelector, rulePayload) 136 { 137 // FIXME: looks like rulePayload is always null. 138 if (error) 139 failureCallback(); 140 else { 141 var documentElementId = this._documentElementId(nodeId); 142 if (documentElementId) 143 WebInspector.domAgent.querySelectorAll(documentElementId, newSelector, checkAffectsCallback.bind(this, nodeId, successCallback, rulePayload)); 144 else 145 failureCallback(); 146 } 147 } 148 149 CSSAgent.setRuleSelector(ruleId, newSelector, callback.bind(this, nodeId, successCallback, failureCallback, newSelector)); 150 }, 151 152 addRule: function(nodeId, selector, successCallback, failureCallback) 153 { 154 function checkAffectsCallback(nodeId, successCallback, rulePayload, selectedNodeIds) 155 { 156 if (!selectedNodeIds) 157 return; 158 var doesAffectSelectedNode = (selectedNodeIds.indexOf(nodeId) >= 0); 159 var rule = WebInspector.CSSRule.parsePayload(rulePayload); 160 successCallback(rule, doesAffectSelectedNode); 161 this._fireStyleSheetChanged(rule.id.styleSheetId, true); 162 } 163 164 function callback(successCallback, failureCallback, selector, error, rulePayload) 165 { 166 if (error) { 167 // Invalid syntax for a selector 168 failureCallback(); 169 } else { 170 var documentElementId = this._documentElementId(nodeId); 171 if (documentElementId) 172 WebInspector.domAgent.querySelectorAll(documentElementId, selector, checkAffectsCallback.bind(this, nodeId, successCallback, rulePayload)); 173 else 174 failureCallback(); 175 } 176 } 177 178 CSSAgent.addRule(nodeId, selector, callback.bind(this, successCallback, failureCallback, selector)); 179 }, 180 181 _documentElementId: function(nodeId) 182 { 183 var node = WebInspector.domAgent.nodeForId(nodeId); 184 if (!node) 185 return null; 186 return node.ownerDocumentElement().id; 187 }, 188 189 _fireStyleSheetChanged: function(styleSheetId, majorChange, callback) 190 { 191 callback = callback || function() {}; 192 193 if (!majorChange || !styleSheetId || !this.hasEventListeners(WebInspector.CSSStyleModel.Events.StyleSheetChanged)) { 194 callback(); 195 return; 196 } 197 198 function mycallback(error, content) 199 { 200 if (!error) 201 this.dispatchEventToListeners(WebInspector.CSSStyleModel.Events.StyleSheetChanged, { styleSheetId: styleSheetId, content: content, majorChange: majorChange }); 202 callback(); 203 } 204 205 CSSAgent.getStyleSheetText(styleSheetId, mycallback.bind(this)); 206 }, 207 208 setStyleSheetText: function(styleSheetId, newText, majorChange, userCallback) 209 { 210 function callback(error) 211 { 212 if (!error) 213 this._fireStyleSheetChanged(styleSheetId, majorChange, userCallback ? userCallback.bind(this, error) : null); 214 } 215 CSSAgent.setStyleSheetText(styleSheetId, newText, callback.bind(this)); 216 } 217 } 218 219 WebInspector.CSSStyleModel.prototype.__proto__ = WebInspector.Object.prototype; 220 221 WebInspector.CSSStyleDeclaration = function(payload) 222 { 223 this.id = payload.styleId; 224 this.width = payload.width; 225 this.height = payload.height; 226 this.range = payload.range; 227 this._shorthandValues = WebInspector.CSSStyleDeclaration.buildShorthandValueMap(payload.shorthandEntries); 228 this._livePropertyMap = {}; // LIVE properties (source-based or style-based) : { name -> CSSProperty } 229 this._allProperties = []; // ALL properties: [ CSSProperty ] 230 this._longhandProperties = {}; // shorthandName -> [ CSSProperty ] 231 this.__disabledProperties = {}; // DISABLED properties: { index -> CSSProperty } 232 var payloadPropertyCount = payload.cssProperties.length; 233 234 var propertyIndex = 0; 235 for (var i = 0; i < payloadPropertyCount; ++i) { 236 var property = new WebInspector.CSSProperty.parsePayload(this, i, payload.cssProperties[i]); 237 this._allProperties.push(property); 238 if (property.disabled) 239 this.__disabledProperties[i] = property; 240 if (!property.active && !property.styleBased) 241 continue; 242 var name = property.name; 243 this[propertyIndex] = name; 244 this._livePropertyMap[name] = property; 245 246 // Index longhand properties. 247 if (property.shorthand) { // only for parsed 248 var longhands = this._longhandProperties[property.shorthand]; 249 if (!longhands) { 250 longhands = []; 251 this._longhandProperties[property.shorthand] = longhands; 252 } 253 longhands.push(property); 254 } 255 ++propertyIndex; 256 } 257 this.length = propertyIndex; 258 if ("cssText" in payload) 259 this.cssText = payload.cssText; 260 } 261 262 WebInspector.CSSStyleDeclaration.buildShorthandValueMap = function(shorthandEntries) 263 { 264 var result = {}; 265 for (var i = 0; i < shorthandEntries.length; ++i) 266 result[shorthandEntries[i].name] = shorthandEntries[i].value; 267 return result; 268 } 269 270 WebInspector.CSSStyleDeclaration.parsePayload = function(payload) 271 { 272 return new WebInspector.CSSStyleDeclaration(payload); 273 } 274 275 WebInspector.CSSStyleDeclaration.prototype = { 276 get allProperties() 277 { 278 return this._allProperties; 279 }, 280 281 getLiveProperty: function(name) 282 { 283 return this._livePropertyMap[name]; 284 }, 285 286 getPropertyValue: function(name) 287 { 288 var property = this._livePropertyMap[name]; 289 return property ? property.value : ""; 290 }, 291 292 getPropertyPriority: function(name) 293 { 294 var property = this._livePropertyMap[name]; 295 return property ? property.priority : ""; 296 }, 297 298 getPropertyShorthand: function(name) 299 { 300 var property = this._livePropertyMap[name]; 301 return property ? property.shorthand : ""; 302 }, 303 304 isPropertyImplicit: function(name) 305 { 306 var property = this._livePropertyMap[name]; 307 return property ? property.implicit : ""; 308 }, 309 310 styleTextWithShorthands: function() 311 { 312 var cssText = ""; 313 var foundProperties = {}; 314 for (var i = 0; i < this.length; ++i) { 315 var individualProperty = this[i]; 316 var shorthandProperty = this.getPropertyShorthand(individualProperty); 317 var propertyName = (shorthandProperty || individualProperty); 318 319 if (propertyName in foundProperties) 320 continue; 321 322 if (shorthandProperty) { 323 var value = this.getShorthandValue(shorthandProperty); 324 var priority = this.getShorthandPriority(shorthandProperty); 325 } else { 326 var value = this.getPropertyValue(individualProperty); 327 var priority = this.getPropertyPriority(individualProperty); 328 } 329 330 foundProperties[propertyName] = true; 331 332 cssText += propertyName + ": " + value; 333 if (priority) 334 cssText += " !" + priority; 335 cssText += "; "; 336 } 337 338 return cssText; 339 }, 340 341 getLonghandProperties: function(name) 342 { 343 return this._longhandProperties[name] || []; 344 }, 345 346 getShorthandValue: function(shorthandProperty) 347 { 348 var property = this.getLiveProperty(shorthandProperty); 349 return property ? property.value : this._shorthandValues[shorthandProperty]; 350 }, 351 352 getShorthandPriority: function(shorthandProperty) 353 { 354 var priority = this.getPropertyPriority(shorthandProperty); 355 if (priority) 356 return priority; 357 358 var longhands = this._longhandProperties[shorthandProperty]; 359 return longhands ? this.getPropertyPriority(longhands[0]) : null; 360 }, 361 362 propertyAt: function(index) 363 { 364 return (index < this.allProperties.length) ? this.allProperties[index] : null; 365 }, 366 367 pastLastSourcePropertyIndex: function() 368 { 369 for (var i = this.allProperties.length - 1; i >= 0; --i) { 370 var property = this.allProperties[i]; 371 if (property.active || property.disabled) 372 return i + 1; 373 } 374 return 0; 375 }, 376 377 newBlankProperty: function() 378 { 379 return new WebInspector.CSSProperty(this, this.pastLastSourcePropertyIndex(), "", "", "", "active", true, false, false, ""); 380 }, 381 382 insertPropertyAt: function(index, name, value, userCallback) 383 { 384 function callback(userCallback, error, payload) 385 { 386 if (!userCallback) 387 return; 388 389 if (error) { 390 console.error(JSON.stringify(error)); 391 userCallback(null); 392 } else { 393 userCallback(WebInspector.CSSStyleDeclaration.parsePayload(payload)); 394 WebInspector.cssModel._fireStyleSheetChanged(this.id.styleSheetId, true); 395 } 396 } 397 398 CSSAgent.setPropertyText(this.id, index, name + ": " + value + ";", false, callback.bind(null, userCallback)); 399 }, 400 401 appendProperty: function(name, value, userCallback) 402 { 403 this.insertPropertyAt(this.allProperties.length, name, value, userCallback); 404 } 405 } 406 407 WebInspector.CSSRule = function(payload) 408 { 409 this.id = payload.ruleId; 410 this.selectorText = payload.selectorText; 411 this.sourceLine = payload.sourceLine; 412 this.sourceURL = payload.sourceURL; 413 this.origin = payload.origin; 414 this.style = WebInspector.CSSStyleDeclaration.parsePayload(payload.style); 415 this.style.parentRule = this; 416 this.selectorRange = payload.selectorRange; 417 } 418 419 WebInspector.CSSRule.parsePayload = function(payload) 420 { 421 return new WebInspector.CSSRule(payload); 422 } 423 424 WebInspector.CSSRule.prototype = { 425 get isUserAgent() 426 { 427 return this.origin === "user-agent"; 428 }, 429 430 get isUser() 431 { 432 return this.origin === "user"; 433 }, 434 435 get isViaInspector() 436 { 437 return this.origin === "inspector"; 438 }, 439 440 get isRegular() 441 { 442 return this.origin === ""; 443 } 444 } 445 446 WebInspector.CSSProperty = function(ownerStyle, index, name, value, priority, status, parsedOk, implicit, shorthand, text) 447 { 448 this.ownerStyle = ownerStyle; 449 this.index = index; 450 this.name = name; 451 this.value = value; 452 this.priority = priority; 453 this.status = status; 454 this.parsedOk = parsedOk; 455 this.implicit = implicit; 456 this.shorthand = shorthand; 457 this.text = text; 458 } 459 460 WebInspector.CSSProperty.parsePayload = function(ownerStyle, index, payload) 461 { 462 // The following default field values are used in the payload: 463 // priority: "" 464 // parsedOk: true 465 // implicit: false 466 // status: "style" 467 // shorthandName: "" 468 var result = new WebInspector.CSSProperty( 469 ownerStyle, index, payload.name, payload.value, payload.priority || "", payload.status || "style", ("parsedOk" in payload) ? payload.parsedOk : true, !!payload.implicit, payload.shorthandName || "", payload.text); 470 return result; 471 } 472 473 WebInspector.CSSProperty.prototype = { 474 get propertyText() 475 { 476 if (this.text !== undefined) 477 return this.text; 478 479 if (this.name === "") 480 return ""; 481 return this.name + ": " + this.value + (this.priority ? " !" + this.priority : "") + ";"; 482 }, 483 484 get isLive() 485 { 486 return this.active || this.styleBased; 487 }, 488 489 get active() 490 { 491 return this.status === "active"; 492 }, 493 494 get styleBased() 495 { 496 return this.status === "style"; 497 }, 498 499 get inactive() 500 { 501 return this.status === "inactive"; 502 }, 503 504 get disabled() 505 { 506 return this.status === "disabled"; 507 }, 508 509 // Replaces "propertyName: propertyValue [!important];" in the stylesheet by an arbitrary propertyText. 510 setText: function(propertyText, majorChange, userCallback) 511 { 512 function enabledCallback(style) 513 { 514 if (style) 515 WebInspector.cssModel._fireStyleSheetChanged(style.id.styleSheetId, majorChange); 516 if (userCallback) 517 userCallback(style); 518 } 519 520 function callback(error, stylePayload) 521 { 522 if (!error) { 523 this.text = propertyText; 524 var style = WebInspector.CSSStyleDeclaration.parsePayload(stylePayload); 525 var newProperty = style.allProperties[this.index]; 526 527 if (newProperty && this.disabled && !propertyText.match(/^\s*$/)) { 528 newProperty.setDisabled(false, enabledCallback); 529 return; 530 } 531 532 WebInspector.cssModel._fireStyleSheetChanged(style.id.styleSheetId, majorChange, userCallback.bind(this, style)); 533 } else { 534 console.error(JSON.stringify(error)); 535 if (userCallback) 536 userCallback(null); 537 } 538 } 539 540 if (!this.ownerStyle) 541 throw "No ownerStyle for property"; 542 543 // An index past all the properties adds a new property to the style. 544 CSSAgent.setPropertyText(this.ownerStyle.id, this.index, propertyText, this.index < this.ownerStyle.pastLastSourcePropertyIndex(), callback.bind(this)); 545 }, 546 547 setValue: function(newValue, userCallback) 548 { 549 var text = this.name + ": " + newValue + (this.priority ? " !" + this.priority : "") + ";" 550 this.setText(text, userCallback); 551 }, 552 553 setDisabled: function(disabled, userCallback) 554 { 555 if (!this.ownerStyle && userCallback) 556 userCallback(null); 557 if (disabled === this.disabled && userCallback) 558 userCallback(this.ownerStyle); 559 560 function callback(error, stylePayload) 561 { 562 if (!userCallback) 563 return; 564 if (error) 565 userCallback(null); 566 else { 567 var style = WebInspector.CSSStyleDeclaration.parsePayload(stylePayload); 568 userCallback(style); 569 WebInspector.cssModel._fireStyleSheetChanged(this.ownerStyle.id.styleSheetId, false); 570 } 571 } 572 573 CSSAgent.toggleProperty(this.ownerStyle.id, this.index, disabled, callback.bind(this)); 574 } 575 } 576 577 WebInspector.CSSStyleSheet = function(payload) 578 { 579 this.id = payload.styleSheetId; 580 this.rules = []; 581 this.styles = {}; 582 for (var i = 0; i < payload.rules.length; ++i) { 583 var rule = WebInspector.CSSRule.parsePayload(payload.rules[i]); 584 this.rules.push(rule); 585 if (rule.style) 586 this.styles[rule.style.id] = rule.style; 587 } 588 if ("text" in payload) 589 this._text = payload.text; 590 } 591 592 WebInspector.CSSStyleSheet.createForId = function(styleSheetId, userCallback) 593 { 594 function callback(error, styleSheetPayload) 595 { 596 if (error) 597 userCallback(null); 598 else 599 userCallback(new WebInspector.CSSStyleSheet(styleSheetPayload)); 600 } 601 CSSAgent.getStyleSheet(styleSheetId, callback.bind(this)); 602 } 603 604 WebInspector.CSSStyleSheet.prototype = { 605 getText: function() 606 { 607 return this._text; 608 }, 609 610 setText: function(newText, majorChange, userCallback) 611 { 612 function callback(error) 613 { 614 if (userCallback) 615 userCallback(error); 616 if (!error) 617 WebInspector.cssModel._fireStyleSheetChanged(this.id, majorChange); 618 } 619 620 CSSAgent.setStyleSheetText(this.id, newText, callback.bind(this)); 621 } 622 } 623 624 WebInspector.CSSStyleModelResourceBinding = function(cssModel) 625 { 626 this._cssModel = cssModel; 627 this._urlToStyleSheetId = {}; 628 this._styleSheetIdToURL = {}; 629 this._cssModel.addEventListener(WebInspector.CSSStyleModel.Events.StyleSheetChanged, this._styleSheetChanged, this); 630 WebInspector.resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.FrameNavigated, this._frameNavigated, this); 631 WebInspector.Resource.registerDomainModelBinding(WebInspector.Resource.Type.Stylesheet, this); 632 } 633 634 WebInspector.CSSStyleModelResourceBinding.prototype = { 635 setContent: function(resource, content, majorChange, userCallback) 636 { 637 if (this._urlToStyleSheetId[resource.url]) { 638 this._innerSetContent(resource.url, content, majorChange, userCallback); 639 return; 640 } 641 this._loadStyleSheetHeaders(this._innerSetContent.bind(this, resource.url, content, majorChange, userCallback)); 642 }, 643 644 _frameNavigated: function(event) 645 { 646 var frameId = event.data; 647 if (!frameId) { 648 // Main frame navigation - clear history. 649 this._urlToStyleSheetId = {}; 650 this._styleSheetIdToURL = {}; 651 } 652 }, 653 654 _innerSetContent: function(url, content, majorChange, userCallback, error) 655 { 656 if (error) { 657 userCallback(error); 658 return; 659 } 660 661 var styleSheetId = this._urlToStyleSheetId[url]; 662 if (!styleSheetId) { 663 if (userCallback) 664 userCallback("No stylesheet found: " + url); 665 return; 666 } 667 this._cssModel.setStyleSheetText(styleSheetId, content, majorChange, userCallback); 668 }, 669 670 _loadStyleSheetHeaders: function(callback) 671 { 672 function didGetAllStyleSheets(error, infos) 673 { 674 if (error) { 675 callback(error); 676 return; 677 } 678 679 for (var i = 0; i < infos.length; ++i) { 680 var info = infos[i]; 681 this._urlToStyleSheetId[info.sourceURL] = info.styleSheetId; 682 this._styleSheetIdToURL[info.styleSheetId] = info.sourceURL; 683 } 684 callback(); 685 } 686 CSSAgent.getAllStyleSheets(didGetAllStyleSheets.bind(this)); 687 }, 688 689 _styleSheetChanged: function(event) 690 { 691 var styleSheetId = event.data.styleSheetId; 692 function setContent() 693 { 694 var url = this._styleSheetIdToURL[styleSheetId]; 695 if (!url) 696 return; 697 698 var resource = WebInspector.resourceForURL(url); 699 if (!resource) 700 return; 701 702 var majorChange = event.data.majorChange; 703 if (majorChange) 704 resource.addRevision(event.data.content); 705 } 706 707 if (!this._styleSheetIdToURL[styleSheetId]) { 708 this._loadStyleSheetHeaders(setContent.bind(this)); 709 return; 710 } 711 setContent.call(this); 712 } 713 } 714 715 WebInspector.CSSStyleModelResourceBinding.prototype.__proto__ = WebInspector.ResourceDomainModelBinding.prototype; 716