1/* 2 * Copyright (C) 2007 Apple Inc. All rights reserved. 3 * Copyright (C) 2009 Joseph Pecoraro 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 15 * its contributors may be used to endorse or promote products derived 16 * from this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 */ 29 30WebInspector.StylesSidebarPane = function() 31{ 32 WebInspector.SidebarPane.call(this, WebInspector.UIString("Styles")); 33} 34 35WebInspector.StylesSidebarPane.prototype = { 36 update: function(node, editedSection, forceUpdate) 37 { 38 var refresh = false; 39 40 if (forceUpdate) 41 delete this.node; 42 43 if (!forceUpdate && (!node || node === this.node)) 44 refresh = true; 45 46 if (node && node.nodeType === Node.TEXT_NODE && node.parentNode) 47 node = node.parentNode; 48 49 if (node && node.nodeType !== Node.ELEMENT_NODE) 50 node = null; 51 52 if (node) 53 this.node = node; 54 else 55 node = this.node; 56 57 var body = this.bodyElement; 58 if (!refresh || !node) { 59 body.removeChildren(); 60 this.sections = []; 61 } 62 63 if (!node) 64 return; 65 66 var self = this; 67 var callback = function(styles) { 68 if (!styles) 69 return; 70 var nodeWrapper = WebInspector.wrapNodeWithStyles(node, styles); 71 self._update(refresh, body, nodeWrapper, editedSection, forceUpdate); 72 }; 73 InspectorController.getStyles(node, !Preferences.showUserAgentStyles, callback); 74 }, 75 76 _update: function(refresh, body, node, editedSection, forceUpdate) 77 { 78 if (!refresh) { 79 body.removeChildren(); 80 this.sections = []; 81 } 82 83 var styleRules = []; 84 85 if (refresh) { 86 for (var i = 0; i < this.sections.length; ++i) { 87 var section = this.sections[i]; 88 if (section._blank) 89 continue; 90 if (section.computedStyle) 91 section.styleRule.style = node.ownerDocument.defaultView.getComputedStyle(node); 92 var styleRule = { section: section, style: section.styleRule.style, computedStyle: section.computedStyle, rule: section.rule }; 93 styleRules.push(styleRule); 94 } 95 } else { 96 var computedStyle = node.ownerDocument.defaultView.getComputedStyle(node); 97 styleRules.push({ computedStyle: true, selectorText: WebInspector.UIString("Computed Style"), style: computedStyle, editable: false }); 98 99 var nodeName = node.nodeName.toLowerCase(); 100 for (var i = 0; i < node.attributes.length; ++i) { 101 var attr = node.attributes[i]; 102 if (attr.style) { 103 var attrStyle = { style: attr.style, editable: false }; 104 attrStyle.subtitle = WebInspector.UIString("element’s “%s” attribute", attr.name); 105 attrStyle.selectorText = nodeName + "[" + attr.name; 106 if (attr.value.length) 107 attrStyle.selectorText += "=" + attr.value; 108 attrStyle.selectorText += "]"; 109 styleRules.push(attrStyle); 110 } 111 } 112 113 // Always Show element's Style Attributes 114 if (node.nodeType === Node.ELEMENT_NODE) { 115 var inlineStyle = { selectorText: WebInspector.UIString("Style Attribute"), style: node.style, isAttribute: true }; 116 inlineStyle.subtitle = WebInspector.UIString("element’s “%s” attribute", "style"); 117 styleRules.push(inlineStyle); 118 } 119 120 var matchedStyleRules = node.ownerDocument.defaultView.getMatchedCSSRules(node, "", !Preferences.showUserAgentStyles); 121 if (matchedStyleRules) { 122 // Add rules in reverse order to match the cascade order. 123 for (var i = (matchedStyleRules.length - 1); i >= 0; --i) { 124 var rule = matchedStyleRules[i]; 125 styleRules.push({ style: rule.style, selectorText: rule.selectorText, parentStyleSheet: rule.parentStyleSheet, rule: rule }); 126 } 127 } 128 } 129 130 function deleteDisabledProperty(style, name) 131 { 132 if (!style || !name) 133 return; 134 if (style.__disabledPropertyValues) 135 delete style.__disabledPropertyValues[name]; 136 if (style.__disabledPropertyPriorities) 137 delete style.__disabledPropertyPriorities[name]; 138 if (style.__disabledProperties) 139 delete style.__disabledProperties[name]; 140 } 141 142 var usedProperties = {}; 143 var disabledComputedProperties = {}; 144 var priorityUsed = false; 145 146 // Walk the style rules and make a list of all used and overloaded properties. 147 for (var i = 0; i < styleRules.length; ++i) { 148 var styleRule = styleRules[i]; 149 if (styleRule.computedStyle) 150 continue; 151 if (styleRule.section && styleRule.section.noAffect) 152 continue; 153 154 styleRule.usedProperties = {}; 155 156 var style = styleRule.style; 157 for (var j = 0; j < style.length; ++j) { 158 var name = style[j]; 159 160 if (!priorityUsed && style.getPropertyPriority(name).length) 161 priorityUsed = true; 162 163 // If the property name is already used by another rule then this rule's 164 // property is overloaded, so don't add it to the rule's usedProperties. 165 if (!(name in usedProperties)) 166 styleRule.usedProperties[name] = true; 167 168 if (name === "font") { 169 // The font property is not reported as a shorthand. Report finding the individual 170 // properties so they are visible in computed style. 171 // FIXME: remove this when http://bugs.webkit.org/show_bug.cgi?id=15598 is fixed. 172 styleRule.usedProperties["font-family"] = true; 173 styleRule.usedProperties["font-size"] = true; 174 styleRule.usedProperties["font-style"] = true; 175 styleRule.usedProperties["font-variant"] = true; 176 styleRule.usedProperties["font-weight"] = true; 177 styleRule.usedProperties["line-height"] = true; 178 } 179 180 // Delete any disabled properties, since the property does exist. 181 // This prevents it from showing twice. 182 deleteDisabledProperty(style, name); 183 deleteDisabledProperty(style, style.getPropertyShorthand(name)); 184 } 185 186 // Add all the properties found in this style to the used properties list. 187 // Do this here so only future rules are affect by properties used in this rule. 188 for (var name in styleRules[i].usedProperties) 189 usedProperties[name] = true; 190 191 // Remember all disabled properties so they show up in computed style. 192 if (style.__disabledProperties) 193 for (var name in style.__disabledProperties) 194 disabledComputedProperties[name] = true; 195 } 196 197 if (priorityUsed) { 198 // Walk the properties again and account for !important. 199 var foundPriorityProperties = []; 200 201 // Walk in reverse to match the order !important overrides. 202 for (var i = (styleRules.length - 1); i >= 0; --i) { 203 if (styleRules[i].computedStyle) 204 continue; 205 206 var style = styleRules[i].style; 207 var uniqueProperties = style.uniqueStyleProperties; 208 for (var j = 0; j < uniqueProperties.length; ++j) { 209 var name = uniqueProperties[j]; 210 if (style.getPropertyPriority(name).length) { 211 if (!(name in foundPriorityProperties)) 212 styleRules[i].usedProperties[name] = true; 213 else 214 delete styleRules[i].usedProperties[name]; 215 foundPriorityProperties[name] = true; 216 } else if (name in foundPriorityProperties) 217 delete styleRules[i].usedProperties[name]; 218 } 219 } 220 } 221 222 if (refresh) { 223 // Walk the style rules and update the sections with new overloaded and used properties. 224 for (var i = 0; i < styleRules.length; ++i) { 225 var styleRule = styleRules[i]; 226 var section = styleRule.section; 227 if (styleRule.computedStyle) 228 section.disabledComputedProperties = disabledComputedProperties; 229 section._usedProperties = (styleRule.usedProperties || usedProperties); 230 section.update((section === editedSection) || styleRule.computedStyle); 231 } 232 } else { 233 // Make a property section for each style rule. 234 for (var i = 0; i < styleRules.length; ++i) { 235 var styleRule = styleRules[i]; 236 var subtitle = styleRule.subtitle; 237 delete styleRule.subtitle; 238 239 var computedStyle = styleRule.computedStyle; 240 delete styleRule.computedStyle; 241 242 var ruleUsedProperties = styleRule.usedProperties; 243 delete styleRule.usedProperties; 244 245 var editable = styleRule.editable; 246 delete styleRule.editable; 247 248 var isAttribute = styleRule.isAttribute; 249 delete styleRule.isAttribute; 250 251 // Default editable to true if it was omitted. 252 if (typeof editable === "undefined") 253 editable = true; 254 255 var section = new WebInspector.StylePropertiesSection(styleRule, subtitle, computedStyle, (ruleUsedProperties || usedProperties), editable); 256 if (computedStyle) 257 section.disabledComputedProperties = disabledComputedProperties; 258 section.pane = this; 259 260 if (Preferences.styleRulesExpandedState && section.identifier in Preferences.styleRulesExpandedState) 261 section.expanded = Preferences.styleRulesExpandedState[section.identifier]; 262 else if (computedStyle) 263 section.collapse(true); 264 else if (isAttribute && styleRule.style.length === 0) 265 section.collapse(true); 266 else 267 section.expand(true); 268 269 body.appendChild(section.element); 270 this.sections.push(section); 271 } 272 273 this.addBlankSection(); 274 } 275 }, 276 277 addBlankSection: function() 278 { 279 var blankSection = new WebInspector.BlankStylePropertiesSection(); 280 blankSection.pane = this; 281 282 this.bodyElement.insertBefore(blankSection.element, this.bodyElement.firstChild.nextSibling.nextSibling); // 0 is computed, 1 is element.style 283 var computed = this.sections.shift(); 284 var elementStyle = this.sections.shift(); 285 this.sections.unshift(blankSection); 286 this.sections.unshift(elementStyle); 287 this.sections.unshift(computed); 288 }, 289 290 appropriateSelectorForNode: function() 291 { 292 var node = this.node; 293 if (!node) 294 return; 295 296 if (node.id) 297 return "#" + node.id; 298 299 if (node.className) 300 return "." + node.className.replace(/\s+/, "."); 301 302 var nodeName = node.nodeName.toLowerCase(); 303 if (nodeName === "input" && node.type) 304 return nodeName + "[type=\"" + node.type + "\"]"; 305 306 return nodeName; 307 } 308} 309 310WebInspector.StylesSidebarPane.prototype.__proto__ = WebInspector.SidebarPane.prototype; 311 312WebInspector.StylePropertiesSection = function(styleRule, subtitle, computedStyle, usedProperties, editable) 313{ 314 WebInspector.PropertiesSection.call(this, styleRule.selectorText); 315 this.titleElement.addEventListener("click", function(e) { e.stopPropagation(); }, false); 316 this.titleElement.addEventListener("dblclick", this._dblclickSelector.bind(this), false); 317 this.element.addEventListener("dblclick", this._dblclickEmptySpace.bind(this), false); 318 319 this.styleRule = styleRule; 320 this.rule = this.styleRule.rule; 321 this.computedStyle = computedStyle; 322 this.editable = (editable && !computedStyle); 323 324 // Prevent editing the user agent and user rules. 325 var isUserAgent = this.styleRule.isUserAgent; 326 var isUser = this.styleRule.isUser; 327 328 if (isUserAgent || isUser) 329 this.editable = false; 330 331 this._usedProperties = usedProperties; 332 333 if (computedStyle) { 334 this.element.addStyleClass("computed-style"); 335 336 if (Preferences.showInheritedComputedStyleProperties) 337 this.element.addStyleClass("show-inherited"); 338 339 var showInheritedLabel = document.createElement("label"); 340 var showInheritedInput = document.createElement("input"); 341 showInheritedInput.type = "checkbox"; 342 showInheritedInput.checked = Preferences.showInheritedComputedStyleProperties; 343 344 var computedStyleSection = this; 345 var showInheritedToggleFunction = function(event) { 346 Preferences.showInheritedComputedStyleProperties = showInheritedInput.checked; 347 if (Preferences.showInheritedComputedStyleProperties) 348 computedStyleSection.element.addStyleClass("show-inherited"); 349 else 350 computedStyleSection.element.removeStyleClass("show-inherited"); 351 event.stopPropagation(); 352 }; 353 354 showInheritedLabel.addEventListener("click", showInheritedToggleFunction, false); 355 356 showInheritedLabel.appendChild(showInheritedInput); 357 showInheritedLabel.appendChild(document.createTextNode(WebInspector.UIString("Show inherited"))); 358 this.subtitleElement.appendChild(showInheritedLabel); 359 } else { 360 if (!subtitle) { 361 if (this.styleRule.parentStyleSheet && this.styleRule.parentStyleSheet.href) { 362 var url = this.styleRule.parentStyleSheet.href; 363 subtitle = WebInspector.linkifyURL(url, WebInspector.displayNameForURL(url)); 364 this.subtitleElement.addStyleClass("file"); 365 } else if (isUserAgent) 366 subtitle = WebInspector.UIString("user agent stylesheet"); 367 else if (isUser) 368 subtitle = WebInspector.UIString("user stylesheet"); 369 else if (this.styleRule.parentStyleSheet === WebInspector.panels.elements.stylesheet) 370 subtitle = WebInspector.UIString("via inspector"); 371 else 372 subtitle = WebInspector.UIString("inline stylesheet"); 373 } 374 375 this.subtitle = subtitle; 376 } 377 378 this.identifier = styleRule.selectorText; 379 if (this.subtitle) 380 this.identifier += ":" + this.subtitleElement.textContent; 381} 382 383WebInspector.StylePropertiesSection.prototype = { 384 get usedProperties() 385 { 386 return this._usedProperties || {}; 387 }, 388 389 set usedProperties(x) 390 { 391 this._usedProperties = x; 392 this.update(); 393 }, 394 395 expand: function(dontRememberState) 396 { 397 if (this._blank) 398 return; 399 400 WebInspector.PropertiesSection.prototype.expand.call(this); 401 if (dontRememberState) 402 return; 403 404 if (!Preferences.styleRulesExpandedState) 405 Preferences.styleRulesExpandedState = {}; 406 Preferences.styleRulesExpandedState[this.identifier] = true; 407 }, 408 409 collapse: function(dontRememberState) 410 { 411 WebInspector.PropertiesSection.prototype.collapse.call(this); 412 if (dontRememberState) 413 return; 414 415 if (!Preferences.styleRulesExpandedState) 416 Preferences.styleRulesExpandedState = {}; 417 Preferences.styleRulesExpandedState[this.identifier] = false; 418 }, 419 420 isPropertyInherited: function(property) 421 { 422 if (!this.computedStyle || !this._usedProperties || this.noAffect) 423 return false; 424 // These properties should always show for Computed Style. 425 var alwaysShowComputedProperties = { "display": true, "height": true, "width": true }; 426 return !(property in this.usedProperties) && !(property in alwaysShowComputedProperties) && !(property in this.disabledComputedProperties); 427 }, 428 429 isPropertyOverloaded: function(property, shorthand) 430 { 431 if (this.computedStyle || !this._usedProperties || this.noAffect) 432 return false; 433 434 var used = (property in this.usedProperties); 435 if (used || !shorthand) 436 return !used; 437 438 // Find out if any of the individual longhand properties of the shorthand 439 // are used, if none are then the shorthand is overloaded too. 440 var longhandProperties = this.styleRule.style.getLonghandProperties(property); 441 for (var j = 0; j < longhandProperties.length; ++j) { 442 var individualProperty = longhandProperties[j]; 443 if (individualProperty in this.usedProperties) 444 return false; 445 } 446 447 return true; 448 }, 449 450 isInspectorStylesheet: function() 451 { 452 return (this.styleRule.parentStyleSheet === WebInspector.panels.elements.stylesheet); 453 }, 454 455 update: function(full) 456 { 457 if (full || this.computedStyle) { 458 this.propertiesTreeOutline.removeChildren(); 459 this.populated = false; 460 } else { 461 var child = this.propertiesTreeOutline.children[0]; 462 while (child) { 463 child.overloaded = this.isPropertyOverloaded(child.name, child.shorthand); 464 child = child.traverseNextTreeElement(false, null, true); 465 } 466 } 467 468 if (this._afterUpdate) { 469 this._afterUpdate(this); 470 delete this._afterUpdate; 471 } 472 }, 473 474 onpopulate: function() 475 { 476 var style = this.styleRule.style; 477 478 var foundShorthands = {}; 479 var uniqueProperties = style.uniqueStyleProperties; 480 var disabledProperties = style.__disabledPropertyValues || {}; 481 482 for (var name in disabledProperties) 483 uniqueProperties.push(name); 484 485 uniqueProperties.sort(); 486 487 for (var i = 0; i < uniqueProperties.length; ++i) { 488 var name = uniqueProperties[i]; 489 var disabled = name in disabledProperties; 490 if (!disabled && this.disabledComputedProperties && !(name in this.usedProperties) && name in this.disabledComputedProperties) 491 disabled = true; 492 493 var shorthand = !disabled ? style.getPropertyShorthand(name) : null; 494 495 if (shorthand && shorthand in foundShorthands) 496 continue; 497 498 if (shorthand) { 499 foundShorthands[shorthand] = true; 500 name = shorthand; 501 } 502 503 var isShorthand = (shorthand ? true : false); 504 var inherited = this.isPropertyInherited(name); 505 var overloaded = this.isPropertyOverloaded(name, isShorthand); 506 507 var item = new WebInspector.StylePropertyTreeElement(this.styleRule, style, name, isShorthand, inherited, overloaded, disabled); 508 this.propertiesTreeOutline.appendChild(item); 509 } 510 }, 511 512 findTreeElementWithName: function(name) 513 { 514 var treeElement = this.propertiesTreeOutline.children[0]; 515 while (treeElement) { 516 if (treeElement.name === name) 517 return treeElement; 518 treeElement = treeElement.traverseNextTreeElement(true, null, true); 519 } 520 return null; 521 }, 522 523 addNewBlankProperty: function() 524 { 525 var item = new WebInspector.StylePropertyTreeElement(this.styleRule, this.styleRule.style, "", false, false, false, false); 526 this.propertiesTreeOutline.appendChild(item); 527 item.listItemElement.textContent = ""; 528 item._newProperty = true; 529 return item; 530 }, 531 532 _dblclickEmptySpace: function(event) 533 { 534 this.expand(); 535 this.addNewBlankProperty().startEditing(); 536 }, 537 538 _dblclickSelector: function(event) 539 { 540 if (!this.editable) 541 return; 542 543 if (!this.rule && this.propertiesTreeOutline.children.length === 0) { 544 this.expand(); 545 this.addNewBlankProperty().startEditing(); 546 return; 547 } 548 549 if (!this.rule) 550 return; 551 552 this.startEditingSelector(); 553 event.stopPropagation(); 554 }, 555 556 startEditingSelector: function() 557 { 558 var element = this.titleElement; 559 if (WebInspector.isBeingEdited(element)) 560 return; 561 562 var context = this.styleRule.selectorText; 563 WebInspector.startEditing(this.titleElement, this.editingSelectorCommitted.bind(this), this.editingSelectorCancelled.bind(this), context); 564 window.getSelection().setBaseAndExtent(element, 0, element, 1); 565 }, 566 567 editingSelectorCommitted: function(element, newContent, oldContent, context, moveDirection) 568 { 569 function moveToNextIfNeeded() { 570 if (!moveDirection || moveDirection !== "forward") 571 return; 572 573 this.expand(); 574 if (this.propertiesTreeOutline.children.length === 0) 575 this.addNewBlankProperty().startEditing(); 576 else { 577 var item = this.propertiesTreeOutline.children[0] 578 item.startEditing(item.valueElement); 579 } 580 } 581 582 if (newContent === oldContent) 583 return moveToNextIfNeeded.call(this); 584 585 var self = this; 586 var callback = function(result) { 587 if (!result) { 588 // Invalid Syntax for a Selector 589 self.editingSelectorCancelled(element, context); 590 moveToNextIfNeeded.call(self); 591 return; 592 } 593 594 var newRulePayload = result[0]; 595 var doesAffectSelectedNode = result[1]; 596 if (!doesAffectSelectedNode) { 597 self.noAffect = true; 598 self.element.addStyleClass("no-affect"); 599 } else { 600 delete self.noAffect; 601 self.element.removeStyleClass("no-affect"); 602 } 603 604 var newRule = WebInspector.CSSStyleDeclaration.parseRule(newRulePayload); 605 self.rule = newRule; 606 self.styleRule = { section: self, style: newRule.style, selectorText: newRule.selectorText, parentStyleSheet: newRule.parentStyleSheet, rule: newRule }; 607 var oldIdentifier = this.identifier; 608 self.identifier = newRule.selectorText + ":" + self.subtitleElement.textContent; 609 self.pane.update(null, true); 610 WebInspector.panels.elements.renameSelector(oldIdentifier, this.identifier, oldContent, newContent); 611 moveToNextIfNeeded.call(self); 612 }; 613 614 InspectorController.applyStyleRuleText(this.rule._id, newContent, this.pane.node, callback); 615 }, 616 617 editingSelectorCancelled: function(element, context) 618 { 619 element.textContent = context; 620 }, 621 622 _doesSelectorAffectSelectedNode: function(selector) 623 { 624 var selectedNode = this.pane.node; 625 var nodes = selectedNode.ownerDocument.querySelectorAll(selector); 626 for (var i = 0; i < nodes.length; ++i) { 627 if (nodes[i] === selectedNode) 628 return true; 629 } 630 631 return false; 632 } 633} 634 635WebInspector.StylePropertiesSection.prototype.__proto__ = WebInspector.PropertiesSection.prototype; 636 637WebInspector.BlankStylePropertiesSection = function() 638{ 639 WebInspector.PropertiesSection.call(this, WebInspector.UIString("Double-Click to Add"), null); 640 641 this._blank = true; 642 this._dblclickListener = this._dblclick.bind(this); 643 this.element.addStyleClass("blank-section"); 644 this.titleElement.addStyleClass("blank-title"); 645 this.titleElement.addEventListener("click", function(e) { e.stopPropagation(); }, false); 646 this.titleElement.addEventListener("dblclick", this._dblclickListener, false); 647} 648 649WebInspector.BlankStylePropertiesSection.prototype = { 650 _dblclick: function(event) 651 { 652 this.startEditing(); 653 }, 654 655 startEditing: function() 656 { 657 var element = this.titleElement; 658 if (WebInspector.isBeingEdited(element)) 659 return; 660 661 this.titleElement.textContent = this.pane.appropriateSelectorForNode(); 662 this.titleElement.removeStyleClass("blank-title"); 663 WebInspector.startEditing(this.titleElement, this.editingCommitted.bind(this), this.editingCancelled.bind(this), ""); 664 window.getSelection().setBaseAndExtent(element, 0, element, 1); 665 }, 666 667 editingCancelled: function() 668 { 669 this.titleElement.textContent = WebInspector.UIString("Double-Click to Add"); 670 this.titleElement.addStyleClass("blank-title"); 671 }, 672 673 editingCommitted: function(element, newContent, oldContent, context) 674 { 675 var self = this; 676 var callback = function(styleRule) { 677 if (!styleRule) { 678 // Invalid Syntax for a Selector 679 self.editingCancelled(); 680 return; 681 } 682 self.makeNormal(WebInspector.CSSStyleDeclaration.parseRule(styleRule)); 683 684 if (!self._doesSelectorAffectSelectedNode(newContent)) { 685 self.noAffect = true; 686 self.element.addStyleClass("no-affect"); 687 } 688 689 self.subtitleElement.textContent = WebInspector.UIString("via inspector"); 690 self.expand(); 691 692 self.pane.addBlankSection(); 693 self.addNewBlankProperty().startEditing(); 694 }; 695 InspectorController.addStyleSelector(newContent, callback); 696 }, 697 698 makeNormal: function(styleRule) 699 { 700 this.titleElement.removeEventListener("dblclick", this._dblclickListener, false); 701 this.titleElement.addEventListener("dblclick", this._dblclickSelector.bind(this), false); 702 this.element.addEventListener("dblclick", this._dblclickEmptySpace.bind(this), false); 703 this.element.removeStyleClass("blank-section"); 704 delete this._blank; 705 delete this._dblclick; 706 delete this.startEditing; 707 delete this.editingCancelled; 708 delete this.editingCommitted; 709 delete this._dblclickListener; 710 delete this.makeNormal; 711 this.styleRule = styleRule; 712 this.rule = styleRule.rule; 713 this.computedStyle = false; 714 this.editable = true; 715 this.identifier = styleRule.selectorText + ":inspector"; 716 // leftovers are: this.noAffect if applicable 717 } 718} 719 720WebInspector.BlankStylePropertiesSection.prototype.__proto__ = WebInspector.StylePropertiesSection.prototype; 721 722WebInspector.StylePropertyTreeElement = function(styleRule, style, name, shorthand, inherited, overloaded, disabled) 723{ 724 this._styleRule = styleRule; 725 this.style = style; 726 this.name = name; 727 this.shorthand = shorthand; 728 this._inherited = inherited; 729 this._overloaded = overloaded; 730 this._disabled = disabled; 731 732 // Pass an empty title, the title gets made later in onattach. 733 TreeElement.call(this, "", null, shorthand); 734} 735 736WebInspector.StylePropertyTreeElement.prototype = { 737 get inherited() 738 { 739 return this._inherited; 740 }, 741 742 set inherited(x) 743 { 744 if (x === this._inherited) 745 return; 746 this._inherited = x; 747 this.updateState(); 748 }, 749 750 get overloaded() 751 { 752 return this._overloaded; 753 }, 754 755 set overloaded(x) 756 { 757 if (x === this._overloaded) 758 return; 759 this._overloaded = x; 760 this.updateState(); 761 }, 762 763 get disabled() 764 { 765 return this._disabled; 766 }, 767 768 set disabled(x) 769 { 770 if (x === this._disabled) 771 return; 772 this._disabled = x; 773 this.updateState(); 774 }, 775 776 get priority() 777 { 778 if (this.disabled && this.style.__disabledPropertyPriorities && this.name in this.style.__disabledPropertyPriorities) 779 return this.style.__disabledPropertyPriorities[this.name]; 780 return (this.shorthand ? this.style.getShorthandPriority(this.name) : this.style.getPropertyPriority(this.name)); 781 }, 782 783 get value() 784 { 785 if (this.disabled && this.style.__disabledPropertyValues && this.name in this.style.__disabledPropertyValues) 786 return this.style.__disabledPropertyValues[this.name]; 787 return (this.shorthand ? this.style.getShorthandValue(this.name) : this.style.getPropertyValue(this.name)); 788 }, 789 790 onattach: function() 791 { 792 this.updateTitle(); 793 }, 794 795 updateTitle: function() 796 { 797 // "Nicknames" for some common values that are easier to read. 798 var valueNicknames = { 799 "rgb(0, 0, 0)": "black", 800 "#000": "black", 801 "#000000": "black", 802 "rgb(255, 255, 255)": "white", 803 "#fff": "white", 804 "#ffffff": "white", 805 "#FFF": "white", 806 "#FFFFFF": "white", 807 "rgba(0, 0, 0, 0)": "transparent", 808 "rgb(255, 0, 0)": "red", 809 "rgb(0, 255, 0)": "lime", 810 "rgb(0, 0, 255)": "blue", 811 "rgb(255, 255, 0)": "yellow", 812 "rgb(255, 0, 255)": "magenta", 813 "rgb(0, 255, 255)": "cyan" 814 }; 815 816 var priority = this.priority; 817 var value = this.value; 818 var htmlValue = value; 819 820 if (priority && !priority.length) 821 delete priority; 822 if (priority) 823 priority = "!" + priority; 824 825 if (value) { 826 var urls = value.match(/url\([^)]+\)/); 827 if (urls) { 828 for (var i = 0; i < urls.length; ++i) { 829 var url = urls[i].substring(4, urls[i].length - 1); 830 htmlValue = htmlValue.replace(urls[i], "url(" + WebInspector.linkifyURL(url) + ")"); 831 } 832 } else { 833 if (value in valueNicknames) 834 htmlValue = valueNicknames[value]; 835 htmlValue = htmlValue.escapeHTML(); 836 } 837 } else 838 htmlValue = value = ""; 839 840 this.updateState(); 841 842 var enabledCheckboxElement = document.createElement("input"); 843 enabledCheckboxElement.className = "enabled-button"; 844 enabledCheckboxElement.type = "checkbox"; 845 enabledCheckboxElement.checked = !this.disabled; 846 enabledCheckboxElement.addEventListener("change", this.toggleEnabled.bind(this), false); 847 848 var nameElement = document.createElement("span"); 849 nameElement.className = "name"; 850 nameElement.textContent = this.name; 851 this.nameElement = nameElement; 852 853 var valueElement = document.createElement("span"); 854 valueElement.className = "value"; 855 valueElement.innerHTML = htmlValue; 856 this.valueElement = valueElement; 857 858 if (priority) { 859 var priorityElement = document.createElement("span"); 860 priorityElement.className = "priority"; 861 priorityElement.textContent = priority; 862 } 863 864 this.listItemElement.removeChildren(); 865 866 // Append the checkbox for root elements of an editable section. 867 if (this.treeOutline.section && this.treeOutline.section.editable && this.parent.root) 868 this.listItemElement.appendChild(enabledCheckboxElement); 869 this.listItemElement.appendChild(nameElement); 870 this.listItemElement.appendChild(document.createTextNode(": ")); 871 this.listItemElement.appendChild(valueElement); 872 873 if (priorityElement) { 874 this.listItemElement.appendChild(document.createTextNode(" ")); 875 this.listItemElement.appendChild(priorityElement); 876 } 877 878 this.listItemElement.appendChild(document.createTextNode(";")); 879 880 if (value) { 881 // FIXME: this only covers W3C and CSS 16 valid color names 882 var colors = value.match(/((rgb|hsl)a?\([^)]+\))|(#[0-9a-fA-F]{6})|(#[0-9a-fA-F]{3})|aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|purple|red|silver|teal|white|yellow|transparent/g); 883 var swatch; 884 if (colors) { 885 var colorsLength = colors.length; 886 for (var i = 0; i < colorsLength; ++i) { 887 var swatchElement = document.createElement("span"); 888 swatchElement.className = "swatch"; 889 swatchElement.style.setProperty("background-color", colors[i]); 890 this.listItemElement.appendChild(swatchElement); 891 swatch = swatchElement; 892 } 893 } 894 895 // Rotate through Color Representations by Clicking on the Swatch 896 // Simple: rgb -> hsl -> nickname? -> shorthex? -> hex -> ... 897 // Advanced: rgba -> hsla -> nickname? -> ... 898 if (colors && colors.length === 1) { 899 var color = new WebInspector.Color(htmlValue); 900 swatch.addEventListener("click", changeColorDisplay, false); 901 swatch.addEventListener("dblclick", function(event) { 902 event.stopPropagation(); 903 }, false); 904 905 var mode = color.mode; 906 var valueElement = this.valueElement; 907 function changeColorDisplay(event) { 908 909 function changeTo(newMode, content) { 910 mode = newMode; 911 valueElement.textContent = content; 912 } 913 914 switch (mode) { 915 case "rgb": 916 changeTo("hsl", color.toHsl()); 917 break; 918 919 case "shorthex": 920 changeTo("hex", color.toHex()); 921 break; 922 923 case "hex": 924 changeTo("rgb", color.toRgb()); 925 break; 926 927 case "nickname": 928 if (color.simple) { 929 if (color.hasShortHex()) 930 changeTo("shorthex", color.toShortHex()); 931 else 932 changeTo("hex", color.toHex()); 933 } else 934 changeTo("rgba", color.toRgba()); 935 break; 936 937 case "hsl": 938 if (color.nickname) 939 changeTo("nickname", color.toNickname()); 940 else if (color.hasShortHex()) 941 changeTo("shorthex", color.toShortHex()); 942 else 943 changeTo("hex", color.toHex()); 944 break; 945 946 case "rgba": 947 changeTo("hsla", color.toHsla()); 948 break; 949 950 case "hsla": 951 if (color.nickname) 952 changeTo("nickname", color.toNickname()); 953 else 954 changeTo("rgba", color.toRgba()); 955 break; 956 } 957 } 958 } 959 } 960 961 this.tooltip = this.name + ": " + (valueNicknames[value] || value) + (priority ? " " + priority : ""); 962 }, 963 964 updateAll: function(updateAllRules) 965 { 966 if (updateAllRules && this.treeOutline.section && this.treeOutline.section.pane) 967 this.treeOutline.section.pane.update(null, this.treeOutline.section); 968 else if (this.treeOutline.section) 969 this.treeOutline.section.update(true); 970 else 971 this.updateTitle(); // FIXME: this will not show new properties. But we don't hit his case yet. 972 }, 973 974 toggleEnabled: function(event) 975 { 976 var disabled = !event.target.checked; 977 978 var self = this; 979 var callback = function(newPayload) { 980 if (!newPayload) 981 return; 982 983 self.style = WebInspector.CSSStyleDeclaration.parseStyle(newPayload); 984 self._styleRule.style = self.style; 985 986 // Set the disabled property here, since the code above replies on it not changing 987 // until after the value and priority are retrieved. 988 self.disabled = disabled; 989 990 if (self.treeOutline.section && self.treeOutline.section.pane) 991 self.treeOutline.section.pane.dispatchEventToListeners("style property toggled"); 992 993 self.updateAll(true); 994 }; 995 InspectorController.toggleStyleEnabled(this.style._id, this.name, disabled, callback); 996 }, 997 998 updateState: function() 999 { 1000 if (!this.listItemElement) 1001 return; 1002 1003 if (this.style.isPropertyImplicit(this.name) || this.value === "initial") 1004 this.listItemElement.addStyleClass("implicit"); 1005 else 1006 this.listItemElement.removeStyleClass("implicit"); 1007 1008 if (this.inherited) 1009 this.listItemElement.addStyleClass("inherited"); 1010 else 1011 this.listItemElement.removeStyleClass("inherited"); 1012 1013 if (this.overloaded) 1014 this.listItemElement.addStyleClass("overloaded"); 1015 else 1016 this.listItemElement.removeStyleClass("overloaded"); 1017 1018 if (this.disabled) 1019 this.listItemElement.addStyleClass("disabled"); 1020 else 1021 this.listItemElement.removeStyleClass("disabled"); 1022 }, 1023 1024 onpopulate: function() 1025 { 1026 // Only populate once and if this property is a shorthand. 1027 if (this.children.length || !this.shorthand) 1028 return; 1029 1030 var longhandProperties = this.style.getLonghandProperties(this.name); 1031 for (var i = 0; i < longhandProperties.length; ++i) { 1032 var name = longhandProperties[i]; 1033 1034 if (this.treeOutline.section) { 1035 var inherited = this.treeOutline.section.isPropertyInherited(name); 1036 var overloaded = this.treeOutline.section.isPropertyOverloaded(name); 1037 } 1038 1039 var item = new WebInspector.StylePropertyTreeElement(this._styleRule, this.style, name, false, inherited, overloaded); 1040 this.appendChild(item); 1041 } 1042 }, 1043 1044 ondblclick: function(element, event) 1045 { 1046 this.startEditing(event.target); 1047 event.stopPropagation(); 1048 }, 1049 1050 startEditing: function(selectElement) 1051 { 1052 // FIXME: we don't allow editing of longhand properties under a shorthand right now. 1053 if (this.parent.shorthand) 1054 return; 1055 1056 if (WebInspector.isBeingEdited(this.listItemElement) || (this.treeOutline.section && !this.treeOutline.section.editable)) 1057 return; 1058 1059 var context = { expanded: this.expanded, hasChildren: this.hasChildren }; 1060 1061 // Lie about our children to prevent expanding on double click and to collapse shorthands. 1062 this.hasChildren = false; 1063 1064 if (!selectElement) 1065 selectElement = this.listItemElement; 1066 1067 this.listItemElement.handleKeyEvent = this.editingKeyDown.bind(this); 1068 1069 WebInspector.startEditing(this.listItemElement, this.editingCommitted.bind(this), this.editingCancelled.bind(this), context); 1070 window.getSelection().setBaseAndExtent(selectElement, 0, selectElement, 1); 1071 }, 1072 1073 editingKeyDown: function(event) 1074 { 1075 var arrowKeyPressed = (event.keyIdentifier === "Up" || event.keyIdentifier === "Down"); 1076 var pageKeyPressed = (event.keyIdentifier === "PageUp" || event.keyIdentifier === "PageDown"); 1077 if (!arrowKeyPressed && !pageKeyPressed) 1078 return; 1079 1080 var selection = window.getSelection(); 1081 if (!selection.rangeCount) 1082 return; 1083 1084 var selectionRange = selection.getRangeAt(0); 1085 if (selectionRange.commonAncestorContainer !== this.listItemElement && !selectionRange.commonAncestorContainer.isDescendant(this.listItemElement)) 1086 return; 1087 1088 const styleValueDelimeters = " \t\n\"':;,/()"; 1089 var wordRange = selectionRange.startContainer.rangeOfWord(selectionRange.startOffset, styleValueDelimeters, this.listItemElement); 1090 var wordString = wordRange.toString(); 1091 var replacementString = wordString; 1092 1093 var matches = /(.*?)(-?\d+(?:\.\d+)?)(.*)/.exec(wordString); 1094 if (matches && matches.length) { 1095 var prefix = matches[1]; 1096 var number = parseFloat(matches[2]); 1097 var suffix = matches[3]; 1098 1099 // If the number is near zero or the number is one and the direction will take it near zero. 1100 var numberNearZero = (number < 1 && number > -1); 1101 if (number === 1 && event.keyIdentifier === "Down") 1102 numberNearZero = true; 1103 else if (number === -1 && event.keyIdentifier === "Up") 1104 numberNearZero = true; 1105 1106 if (numberNearZero && event.altKey && arrowKeyPressed) { 1107 if (event.keyIdentifier === "Down") 1108 number = Math.ceil(number - 1); 1109 else 1110 number = Math.floor(number + 1); 1111 } else { 1112 // Jump by 10 when shift is down or jump by 0.1 when near zero or Alt/Option is down. 1113 // Also jump by 10 for page up and down, or by 100 if shift is held with a page key. 1114 var changeAmount = 1; 1115 if (event.shiftKey && pageKeyPressed) 1116 changeAmount = 100; 1117 else if (event.shiftKey || pageKeyPressed) 1118 changeAmount = 10; 1119 else if (event.altKey || numberNearZero) 1120 changeAmount = 0.1; 1121 1122 if (event.keyIdentifier === "Down" || event.keyIdentifier === "PageDown") 1123 changeAmount *= -1; 1124 1125 // Make the new number and constrain it to a precision of 6, this matches numbers the engine returns. 1126 // Use the Number constructor to forget the fixed precision, so 1.100000 will print as 1.1. 1127 number = Number((number + changeAmount).toFixed(6)); 1128 } 1129 1130 replacementString = prefix + number + suffix; 1131 } else { 1132 // FIXME: this should cycle through known keywords for the current property name. 1133 return; 1134 } 1135 1136 var replacementTextNode = document.createTextNode(replacementString); 1137 1138 wordRange.deleteContents(); 1139 wordRange.insertNode(replacementTextNode); 1140 1141 var finalSelectionRange = document.createRange(); 1142 finalSelectionRange.setStart(replacementTextNode, 0); 1143 finalSelectionRange.setEnd(replacementTextNode, replacementString.length); 1144 1145 selection.removeAllRanges(); 1146 selection.addRange(finalSelectionRange); 1147 1148 event.preventDefault(); 1149 event.handled = true; 1150 1151 if (!this.originalCSSText) { 1152 // Remember the rule's original CSS text, so it can be restored 1153 // if the editing is canceled and before each apply. 1154 this.originalCSSText = this.style.styleTextWithShorthands(); 1155 } else { 1156 // Restore the original CSS text before applying user changes. This is needed to prevent 1157 // new properties from sticking around if the user adds one, then removes it. 1158 InspectorController.setStyleText(this.style, this.originalCSSText); 1159 } 1160 1161 this.applyStyleText(this.listItemElement.textContent); 1162 }, 1163 1164 editingEnded: function(context) 1165 { 1166 this.hasChildren = context.hasChildren; 1167 if (context.expanded) 1168 this.expand(); 1169 delete this.listItemElement.handleKeyEvent; 1170 delete this.originalCSSText; 1171 }, 1172 1173 editingCancelled: function(element, context) 1174 { 1175 if (this._newProperty) 1176 this.treeOutline.removeChild(this); 1177 else if (this.originalCSSText) { 1178 InspectorController.setStyleText(this.style, this.originalCSSText); 1179 1180 if (this.treeOutline.section && this.treeOutline.section.pane) 1181 this.treeOutline.section.pane.dispatchEventToListeners("style edited"); 1182 1183 this.updateAll(); 1184 } else 1185 this.updateTitle(); 1186 1187 this.editingEnded(context); 1188 }, 1189 1190 editingCommitted: function(element, userInput, previousContent, context, moveDirection) 1191 { 1192 this.editingEnded(context); 1193 1194 // Determine where to move to before making changes 1195 var newProperty, moveToPropertyName, moveToSelector; 1196 var moveTo = (moveDirection === "forward" ? this.nextSibling : this.previousSibling); 1197 if (moveTo) 1198 moveToPropertyName = moveTo.name; 1199 else if (moveDirection === "forward") 1200 newProperty = true; 1201 else if (moveDirection === "backward" && this.treeOutline.section.rule) 1202 moveToSelector = true; 1203 1204 // Make the Changes and trigger the moveToNextCallback after updating 1205 var blankInput = /^\s*$/.test(userInput); 1206 if (userInput !== previousContent || (this._newProperty && blankInput)) { // only if something changed, or adding a new style and it was blank 1207 this.treeOutline.section._afterUpdate = moveToNextCallback.bind(this, this._newProperty, !blankInput); 1208 this.applyStyleText(userInput, true); 1209 } else 1210 moveToNextCallback(this._newProperty, false, this.treeOutline.section, false); 1211 1212 // The Callback to start editing the next property 1213 function moveToNextCallback(alreadyNew, valueChanged, section) { 1214 if (!moveDirection) 1215 return; 1216 1217 // User just tabbed through without changes 1218 if (moveTo && moveTo.parent) { 1219 moveTo.startEditing(moveTo.valueElement); 1220 return; 1221 } 1222 1223 // User has made a change then tabbed, wiping all the original treeElements, 1224 // recalculate the new treeElement for the same property we were going to edit next 1225 if (moveTo && !moveTo.parent) { 1226 var treeElement = section.findTreeElementWithName(moveToPropertyName); 1227 if (treeElement) 1228 treeElement.startEditing(treeElement.valueElement); 1229 return; 1230 } 1231 1232 // Create a new attribute in this section 1233 if (newProperty) { 1234 if (alreadyNew && !valueChanged) 1235 return; 1236 1237 var item = section.addNewBlankProperty(); 1238 item.startEditing(); 1239 return; 1240 } 1241 1242 if (moveToSelector) 1243 section.startEditingSelector(); 1244 } 1245 }, 1246 1247 applyStyleText: function(styleText, updateInterface) 1248 { 1249 var section = this.treeOutline.section; 1250 var elementsPanel = WebInspector.panels.elements; 1251 var styleTextLength = styleText.trimWhitespace().length; 1252 if (!styleTextLength) { 1253 if (updateInterface) { 1254 // The user deleted everything, so remove the tree element and update. 1255 if (!this._newProperty) 1256 delete section._afterUpdate; 1257 if (section && section.pane) 1258 section.pane.update(); 1259 this.parent.removeChild(this); 1260 elementsPanel.removeStyleChange(section.identifier, this.style, this.name); 1261 } 1262 return; 1263 } 1264 1265 var self = this; 1266 var callback = function(result) { 1267 if (!result) { 1268 // The user typed something, but it didn't parse. Just abort and restore 1269 // the original title for this property. If this was a new attribute and 1270 // we couldn't parse, then just remove it. 1271 if (self._newProperty) { 1272 self.parent.removeChild(self); 1273 return; 1274 } 1275 if (updateInterface) 1276 self.updateTitle(); 1277 return; 1278 } 1279 1280 var newPayload = result[0]; 1281 var changedProperties = result[1]; 1282 elementsPanel.removeStyleChange(section.identifier, self.style, self.name); 1283 1284 self.style = WebInspector.CSSStyleDeclaration.parseStyle(newPayload); 1285 for (var i = 0; i < changedProperties.length; ++i) 1286 elementsPanel.addStyleChange(section.identifier, self.style, changedProperties[i]); 1287 self._styleRule.style = self.style; 1288 if (section && section.pane) 1289 section.pane.dispatchEventToListeners("style edited"); 1290 1291 if (updateInterface) 1292 self.updateAll(true); 1293 1294 if (!self.rule) 1295 WebInspector.panels.elements.treeOutline.update(); 1296 }; 1297 InspectorController.applyStyleText(this.style._id, styleText.trimWhitespace(), this.name, callback); 1298 } 1299} 1300 1301WebInspector.StylePropertyTreeElement.prototype.__proto__ = TreeElement.prototype; 1302