rule.js (28395B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 style: { ELEMENT_STYLE, PRES_HINTS }, 9 } = require("resource://devtools/shared/constants.js"); 10 const CssLogic = require("resource://devtools/shared/inspector/css-logic.js"); 11 const TextProperty = require("resource://devtools/client/inspector/rules/models/text-property.js"); 12 13 loader.lazyRequireGetter( 14 this, 15 "promiseWarn", 16 "resource://devtools/client/inspector/shared/utils.js", 17 true 18 ); 19 loader.lazyRequireGetter( 20 this, 21 "parseNamedDeclarations", 22 "resource://devtools/shared/css/parsing-utils.js", 23 true 24 ); 25 26 const STYLE_INSPECTOR_PROPERTIES = 27 "devtools/shared/locales/styleinspector.properties"; 28 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 29 const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES); 30 31 /** 32 * Rule is responsible for the following: 33 * Manages a single style declaration or rule. 34 * Applies changes to the properties in a rule. 35 * Maintains a list of TextProperty objects. 36 */ 37 class Rule { 38 /** 39 * @param {ElementStyle} elementStyle 40 * The ElementStyle to which this rule belongs. 41 * @param {object} options 42 * The information used to construct this rule. Properties include: 43 * rule: A StyleRuleActor 44 * inherited: An element this rule was inherited from. If omitted, 45 * the rule applies directly to the current element. 46 * isSystem: Is this a user agent style? 47 * isUnmatched: True if the rule does not match the current selected 48 * element, otherwise, false. 49 */ 50 constructor(elementStyle, options) { 51 this.elementStyle = elementStyle; 52 this.domRule = options.rule; 53 this.compatibilityIssues = null; 54 55 this.matchedSelectorIndexes = options.matchedSelectorIndexes || []; 56 this.isSystem = options.isSystem; 57 this.isUnmatched = options.isUnmatched || false; 58 this.darkColorScheme = options.darkColorScheme; 59 this.inherited = options.inherited || null; 60 this.pseudoElement = options.pseudoElement || ""; 61 this.keyframes = options.keyframes || null; 62 this.userAdded = options.rule.userAdded; 63 64 this.cssProperties = this.elementStyle.ruleView.cssProperties; 65 this.inspector = this.elementStyle.ruleView.inspector; 66 this.store = this.elementStyle.ruleView.store; 67 68 // Populate the text properties with the style's current authoredText 69 // value, and add in any disabled properties from the store. 70 this.textProps = this._getTextProperties(); 71 this.textProps = this.textProps.concat(this._getDisabledProperties()); 72 73 this.getUniqueSelector = this.getUniqueSelector.bind(this); 74 this.onStyleRuleFrontUpdated = this.onStyleRuleFrontUpdated.bind(this); 75 76 this.domRule.on("rule-updated", this.onStyleRuleFrontUpdated); 77 } 78 79 destroy() { 80 if (this._unsubscribeSourceMap) { 81 this._unsubscribeSourceMap(); 82 } 83 84 this.domRule.off("rule-updated", this.onStyleRuleFrontUpdated); 85 this.compatibilityIssues = null; 86 this.destroyed = true; 87 } 88 89 get declarations() { 90 return this.textProps; 91 } 92 93 get selector() { 94 return { 95 getUniqueSelector: this.getUniqueSelector, 96 matchedSelectorIndexes: this.matchedSelectorIndexes, 97 selectors: this.domRule.selectors, 98 selectorsSpecificity: this.domRule.selectorsSpecificity, 99 selectorWarnings: this.domRule.selectors, 100 selectorText: this.keyframes ? this.domRule.keyText : this.selectorText, 101 }; 102 } 103 104 get sourceMapURLService() { 105 return this.inspector.toolbox.sourceMapURLService; 106 } 107 108 get title() { 109 let title = CssLogic.shortSource(this.sheet); 110 if (this.domRule.type !== ELEMENT_STYLE && this.ruleLine > 0) { 111 title += ":" + this.ruleLine; 112 } 113 114 return title; 115 } 116 117 get inheritedSectionLabel() { 118 if (this._inheritedSectionLabel) { 119 return this._inheritedSectionLabel; 120 } 121 this._inheritedSectionLabel = ""; 122 if (this.inherited) { 123 let eltText = this.inherited.displayName; 124 if (this.inherited.id) { 125 eltText += "#" + this.inherited.id; 126 } 127 if (CssLogic.ELEMENT_BACKED_PSEUDO_ELEMENTS.has(this.pseudoElement)) { 128 eltText += this.pseudoElement; 129 } 130 this._inheritedSectionLabel = STYLE_INSPECTOR_L10N.getFormatStr( 131 "rule.inheritedFrom", 132 eltText 133 ); 134 } 135 return this._inheritedSectionLabel; 136 } 137 138 get keyframesName() { 139 if (this._keyframesName) { 140 return this._keyframesName; 141 } 142 this._keyframesName = ""; 143 if (this.keyframes) { 144 this._keyframesName = STYLE_INSPECTOR_L10N.getFormatStr( 145 "rule.keyframe", 146 this.keyframes.name 147 ); 148 } 149 return this._keyframesName; 150 } 151 152 get keyframesRule() { 153 if (!this.keyframes) { 154 return null; 155 } 156 157 return { 158 id: this.keyframes.actorID, 159 keyframesName: this.keyframesName, 160 }; 161 } 162 163 get selectorText() { 164 if (Array.isArray(this.domRule.selectors)) { 165 return this.domRule.selectors.join(", "); 166 } 167 168 if (this.domRule.type === PRES_HINTS) { 169 return CssLogic.l10n("rule.sourceElementAttributesStyle"); 170 } 171 172 return CssLogic.l10n("rule.sourceElement"); 173 } 174 175 /** 176 * The rule's stylesheet. 177 */ 178 get sheet() { 179 return this.domRule ? this.domRule.parentStyleSheet : null; 180 } 181 182 /** 183 * The rule's line within a stylesheet 184 */ 185 get ruleLine() { 186 return this.domRule ? this.domRule.line : -1; 187 } 188 189 /** 190 * The rule's column within a stylesheet 191 */ 192 get ruleColumn() { 193 return this.domRule ? this.domRule.column : null; 194 } 195 196 /** 197 * Get the declaration block issues from the compatibility actor 198 * 199 * @returns A promise that resolves with an array of objects in following form: 200 * { 201 * // Type of compatibility issue 202 * type: <string>, 203 * // The CSS declaration that has compatibility issues 204 * property: <string>, 205 * // Alias to the given CSS property 206 * alias: <Array>, 207 * // Link to MDN documentation for the particular CSS rule 208 * url: <string>, 209 * deprecated: <boolean>, 210 * experimental: <boolean>, 211 * // An array of all the browsers that don't support the given CSS rule 212 * unsupportedBrowsers: <Array>, 213 * } 214 */ 215 async getCompatibilityIssues() { 216 if (!this.compatibilityIssues) { 217 this.compatibilityIssues = 218 this.inspector.commands.inspectorCommand.getCSSDeclarationBlockIssues( 219 this.domRule.declarations 220 ); 221 } 222 223 return this.compatibilityIssues; 224 } 225 226 /** 227 * Returns the TextProperty with the given id or undefined if it cannot be found. 228 * 229 * @param {string | null} id 230 * A TextProperty id. 231 * @return {TextProperty|undefined} with the given id in the current Rule or undefined 232 * if it cannot be found. 233 */ 234 getDeclaration(id) { 235 return id ? this.textProps.find(textProp => textProp.id === id) : undefined; 236 } 237 238 /** 239 * Returns an unique selector for the CSS rule. 240 */ 241 async getUniqueSelector() { 242 let selector = ""; 243 244 if (this.domRule.selectors) { 245 // This is a style rule with a selector. 246 selector = this.domRule.selectors.join(", "); 247 } else if (this.inherited) { 248 // This is an inline style from an inherited rule. Need to resolve the unique 249 // selector from the node which rule this is inherited from. 250 selector = await this.inherited.getUniqueSelector(); 251 } else { 252 // This is an inline style from the current node. 253 selector = await this.inspector.selection.nodeFront.getUniqueSelector(); 254 } 255 256 return selector; 257 } 258 259 /** 260 * Returns true if the rule matches the creation options 261 * specified. 262 * 263 * @param {object} options 264 * Creation options. See the Rule constructor for documentation. 265 */ 266 matches(options) { 267 return this.domRule === options.rule; 268 } 269 270 /** 271 * Create a new TextProperty to include in the rule. 272 * 273 * @param {string} name 274 * The text property name (such as "background" or "border-top"). 275 * @param {string} value 276 * The property's value (not including priority). 277 * @param {string} priority 278 * The property's priority (either "important" or an empty string). 279 * @param {boolean} enabled 280 * True if the property should be enabled. 281 * @param {TextProperty} siblingProp 282 * Optional, property next to which the new property will be added. 283 */ 284 createProperty(name, value, priority, enabled, siblingProp) { 285 const prop = new TextProperty({ 286 rule: this, 287 name, 288 value, 289 priority, 290 enabled, 291 }); 292 293 let ind; 294 if (siblingProp) { 295 ind = this.textProps.indexOf(siblingProp) + 1; 296 this.textProps.splice(ind, 0, prop); 297 } else { 298 ind = this.textProps.length; 299 this.textProps.push(prop); 300 } 301 302 this.applyProperties(modifications => { 303 modifications.createProperty(ind, name, value, priority, enabled); 304 305 this.store.userProperties.setProperty(this.domRule, name, value); 306 307 // Now that the rule has been updated, the server might have given us data 308 // that changes the state of the property. Update it now. 309 prop.updateEditor(); 310 }); 311 312 return prop; 313 } 314 315 /** 316 * Helper function for applyProperties that is called when the actor 317 * does not support as-authored styles. Store disabled properties 318 * in the element style's store. 319 */ 320 _applyPropertiesNoAuthored(modifications) { 321 this.elementStyle.onRuleUpdated(); 322 323 const disabledProps = []; 324 325 for (const prop of this.textProps) { 326 if (prop.invisible) { 327 continue; 328 } 329 if (!prop.enabled) { 330 disabledProps.push({ 331 name: prop.name, 332 value: prop.value, 333 priority: prop.priority, 334 }); 335 continue; 336 } 337 if (prop.value.trim() === "") { 338 continue; 339 } 340 341 modifications.setProperty(-1, prop.name, prop.value, prop.priority); 342 343 prop.updateComputed(); 344 } 345 346 // Store disabled properties in the disabled store. 347 const disabled = this.elementStyle.store.disabled; 348 if (disabledProps.length) { 349 disabled.set(this.domRule, disabledProps); 350 } else { 351 disabled.delete(this.domRule); 352 } 353 354 return modifications.apply().then(() => { 355 const cssProps = {}; 356 // Note that even though StyleRuleActors normally provide parsed 357 // declarations already, _applyPropertiesNoAuthored is only used when 358 // connected to older backend that do not provide them. So parse here. 359 for (const cssProp of parseNamedDeclarations( 360 this.cssProperties.isKnown, 361 this.domRule.authoredText 362 )) { 363 cssProps[cssProp.name] = cssProp; 364 } 365 366 for (const textProp of this.textProps) { 367 if (!textProp.enabled) { 368 continue; 369 } 370 let cssProp = cssProps[textProp.name]; 371 372 if (!cssProp) { 373 cssProp = { 374 name: textProp.name, 375 value: "", 376 priority: "", 377 }; 378 } 379 380 textProp.priority = cssProp.priority; 381 } 382 }); 383 } 384 385 /** 386 * A helper for applyProperties that applies properties in the "as 387 * authored" case; that is, when the StyleRuleActor supports 388 * setRuleText. 389 */ 390 _applyPropertiesAuthored(modifications) { 391 return modifications.apply().then(() => { 392 // The rewriting may have required some other property values to 393 // change, e.g., to insert some needed terminators. Update the 394 // relevant properties here. 395 for (const index in modifications.changedDeclarations) { 396 const newValue = modifications.changedDeclarations[index]; 397 this.textProps[index].updateValue(newValue); 398 } 399 // Recompute and redisplay the computed properties. 400 for (const prop of this.textProps) { 401 if (!prop.invisible && prop.enabled) { 402 prop.updateComputed(); 403 prop.updateEditor(); 404 } 405 } 406 }); 407 } 408 409 /** 410 * Reapply all the properties in this rule, and update their 411 * computed styles. Will re-mark overridden properties. Sets the 412 * |_applyingModifications| property to a promise which will resolve 413 * when the edit has completed. 414 * 415 * @param {Function} modifier a function that takes a RuleModificationList 416 * (or RuleRewriter) as an argument and that modifies it 417 * to apply the desired edit 418 * @return {Promise} a promise which will resolve when the edit 419 * is complete 420 */ 421 applyProperties(modifier) { 422 // If there is already a pending modification, we have to wait 423 // until it settles before applying the next modification. 424 const resultPromise = Promise.resolve(this._applyingModifications) 425 .then(() => { 426 const modifications = this.domRule.startModifyingProperties( 427 this.inspector.panelWin, 428 this.cssProperties 429 ); 430 modifier(modifications); 431 if (this.domRule.canSetRuleText) { 432 return this._applyPropertiesAuthored(modifications); 433 } 434 return this._applyPropertiesNoAuthored(modifications); 435 }) 436 .then(() => { 437 this.elementStyle.onRuleUpdated(); 438 439 if (resultPromise === this._applyingModifications) { 440 this._applyingModifications = null; 441 this.elementStyle._changed(); 442 } 443 }) 444 .catch(promiseWarn); 445 446 this._applyingModifications = resultPromise; 447 return resultPromise; 448 } 449 450 /** 451 * Renames a property. 452 * 453 * @param {TextProperty} property 454 * The property to rename. 455 * @param {string} name 456 * The new property name (such as "background" or "border-top"). 457 * @return {Promise} 458 */ 459 setPropertyName(property, name) { 460 if (name === property.name) { 461 return Promise.resolve(); 462 } 463 464 const oldName = property.name; 465 property.name = name; 466 const index = this.textProps.indexOf(property); 467 return this.applyProperties(modifications => { 468 modifications.renameProperty(index, oldName, name); 469 }); 470 } 471 472 /** 473 * Sets the value and priority of a property, then reapply all properties. 474 * 475 * @param {TextProperty} property 476 * The property to manipulate. 477 * @param {string} value 478 * The property's value (not including priority). 479 * @param {string} priority 480 * The property's priority (either "important" or an empty string). 481 * @return {Promise} 482 */ 483 setPropertyValue(property, value, priority) { 484 if (value === property.value && priority === property.priority) { 485 return Promise.resolve(); 486 } 487 488 property.value = value; 489 property.priority = priority; 490 491 const index = this.textProps.indexOf(property); 492 return this.applyProperties(modifications => { 493 modifications.setProperty(index, property.name, value, priority); 494 }); 495 } 496 497 /** 498 * Just sets the value and priority of a property, in order to preview its 499 * effect on the content document. 500 * 501 * @param {TextProperty} property 502 * The property which value will be previewed 503 * @param {string} value 504 * The value to be used for the preview 505 * @param {string} priority 506 * The property's priority (either "important" or an empty string). 507 * @return {Promise} 508 */ 509 previewPropertyValue(property, value, priority) { 510 this.elementStyle.ruleView.emitForTests("start-preview-property-value"); 511 const modifications = this.domRule.startModifyingProperties( 512 this.inspector.panelWin, 513 this.cssProperties 514 ); 515 modifications.setProperty( 516 this.textProps.indexOf(property), 517 property.name, 518 value, 519 priority 520 ); 521 return modifications.apply().then(() => { 522 // Ensure dispatching a ruleview-changed event 523 // also for previews 524 this.elementStyle._changed(); 525 }); 526 } 527 528 /** 529 * Disables or enables given TextProperty. 530 * 531 * @param {TextProperty} property 532 * The property to enable/disable 533 * @param {boolean} value 534 */ 535 setPropertyEnabled(property, value) { 536 if (property.enabled === !!value) { 537 return; 538 } 539 property.enabled = !!value; 540 const index = this.textProps.indexOf(property); 541 this.applyProperties(modifications => { 542 modifications.setPropertyEnabled(index, property.name, property.enabled); 543 }); 544 } 545 546 /** 547 * Remove a given TextProperty from the rule and update the rule 548 * accordingly. 549 * 550 * @param {TextProperty} property 551 * The property to be removed 552 */ 553 removeProperty(property) { 554 const index = this.textProps.indexOf(property); 555 this.textProps.splice(index, 1); 556 // Need to re-apply properties in case removing this TextProperty 557 // exposes another one. 558 this.applyProperties(modifications => { 559 modifications.removeProperty(index, property.name); 560 }); 561 } 562 563 /** 564 * Event handler for "rule-updated" event fired by StyleRuleActor. 565 * 566 * @param {StyleRuleFront} front 567 */ 568 onStyleRuleFrontUpdated(front) { 569 // Overwritting this reference is not required, but it's here to avoid confusion. 570 // Whenever an actor is passed over the protocol, either as a return value or as 571 // payload on an event, the `form` of its corresponding front will be automatically 572 // updated. No action required. 573 // Even if this `domRule` reference here is not explicitly updated, lookups of 574 // `this.domRule.declarations` will point to the latest state of declarations set 575 // on the actor. Everything on `StyleRuleForm.form` will point to the latest state. 576 this.domRule = front; 577 } 578 579 /** 580 * Get the list of TextProperties from the style. Needs 581 * to parse the style's authoredText. 582 */ 583 _getTextProperties() { 584 const textProps = []; 585 const store = this.elementStyle.store; 586 587 for (const prop of this.domRule.declarations) { 588 const name = prop.name; 589 // In an inherited rule, we only show inherited properties. 590 // However, we must keep all properties in order for rule 591 // rewriting to work properly. So, compute the "invisible" 592 // property here. 593 const inherits = prop.isCustomProperty 594 ? prop.inherits 595 : this.cssProperties.isInherited(name); 596 const invisible = this.inherited && !inherits; 597 598 const value = store.userProperties.getProperty( 599 this.domRule, 600 name, 601 prop.value 602 ); 603 604 const textProp = new TextProperty({ 605 rule: this, 606 name, 607 value, 608 priority: prop.priority, 609 enabled: !("commentOffsets" in prop), 610 invisible, 611 }); 612 textProps.push(textProp); 613 } 614 615 return textProps; 616 } 617 618 /** 619 * Return the list of disabled properties from the store for this rule. 620 */ 621 _getDisabledProperties() { 622 const store = this.elementStyle.store; 623 624 // Include properties from the disabled property store, if any. 625 const disabledProps = store.disabled.get(this.domRule); 626 if (!disabledProps) { 627 return []; 628 } 629 630 const textProps = []; 631 632 for (const prop of disabledProps) { 633 const value = store.userProperties.getProperty( 634 this.domRule, 635 prop.name, 636 prop.value 637 ); 638 const textProp = new TextProperty({ 639 rule: this, 640 name: prop.name, 641 value, 642 priority: prop.priority, 643 }); 644 textProp.enabled = false; 645 textProps.push(textProp); 646 } 647 648 return textProps; 649 } 650 651 /** 652 * Reread the current state of the rules and rebuild text 653 * properties as needed. 654 */ 655 refresh(options) { 656 this.matchedSelectorIndexes = options.matchedSelectorIndexes || []; 657 const colorSchemeChanged = this.darkColorScheme !== options.darkColorScheme; 658 this.darkColorScheme = options.darkColorScheme; 659 660 const newTextProps = this._getTextProperties(); 661 662 // The element style rule behaves differently on refresh. We basically need to update 663 // it to reflect the new text properties exactly. The order might have changed, some 664 // properties might have been removed, etc. And we don't need to mark anything as 665 // disabled here. The element style rule should always reflect the content of the 666 // style attribute. 667 if (this.domRule.type === ELEMENT_STYLE) { 668 this.textProps = newTextProps; 669 670 if (this.editor) { 671 this.editor.populate(true); 672 } 673 674 return; 675 } 676 677 // Update current properties for each property present on the style. 678 // This will mark any touched properties with _visited so we 679 // can detect properties that weren't touched (because they were 680 // removed from the style). 681 // Also keep track of properties that didn't exist in the current set 682 // of properties. 683 const brandNewProps = []; 684 for (const newProp of newTextProps) { 685 if (!this._updateTextProperty(newProp)) { 686 brandNewProps.push(newProp); 687 } 688 } 689 690 // Refresh editors and disabled state for all the properties that 691 // were updated. 692 for (const prop of this.textProps) { 693 // Properties that weren't touched during the update 694 // process must no longer exist on the node. Mark them disabled. 695 if (!prop._visited) { 696 prop.enabled = false; 697 prop.updateEditor(); 698 } else { 699 delete prop._visited; 700 } 701 702 // Valid properties that aren't disabled might need to get updated in some condition 703 if ( 704 prop.enabled && 705 prop.isValid() && 706 // Update if it's using light-dark and the color scheme changed 707 colorSchemeChanged && 708 prop.value.includes("light-dark") 709 ) { 710 prop.updateEditor(); 711 } 712 } 713 714 // Add brand new properties. 715 this.textProps = this.textProps.concat(brandNewProps); 716 717 // Refresh the editor if one already exists. 718 if (this.editor) { 719 this.editor.populate(); 720 } 721 } 722 723 /** 724 * Update the current TextProperties that match a given property 725 * from the authoredText. Will choose one existing TextProperty to update 726 * with the new property's value, and will disable all others. 727 * 728 * When choosing the best match to reuse, properties will be chosen 729 * by assigning a rank and choosing the highest-ranked property: 730 * Name, value, and priority match, enabled. (6) 731 * Name, value, and priority match, disabled. (5) 732 * Name and value match, enabled. (4) 733 * Name and value match, disabled. (3) 734 * Name matches, enabled. (2) 735 * Name matches, disabled. (1) 736 * 737 * If no existing properties match the property, nothing happens. 738 * 739 * @param {TextProperty} newProp 740 * The current version of the property, as parsed from the 741 * authoredText in Rule._getTextProperties(). 742 * @return {boolean} true if a property was updated, false if no properties 743 * were updated. 744 */ 745 _updateTextProperty(newProp) { 746 const match = { rank: 0, prop: null }; 747 748 for (const prop of this.textProps) { 749 if (prop.name !== newProp.name) { 750 continue; 751 } 752 753 // Mark this property visited. 754 prop._visited = true; 755 756 // Start at rank 1 for matching name. 757 let rank = 1; 758 759 // Value and Priority matches add 2 to the rank. 760 // Being enabled adds 1. This ranks better matches higher, 761 // with priority breaking ties. 762 if (prop.value === newProp.value) { 763 rank += 2; 764 if (prop.priority === newProp.priority) { 765 rank += 2; 766 } 767 } 768 769 if (prop.enabled) { 770 rank += 1; 771 } 772 773 if (rank > match.rank) { 774 if (match.prop) { 775 // We outrank a previous match, disable it. 776 match.prop.enabled = false; 777 match.prop.updateEditor(); 778 } 779 match.rank = rank; 780 match.prop = prop; 781 } else if (rank) { 782 // A previous match outranks us, disable ourself. 783 prop.enabled = false; 784 prop.updateEditor(); 785 } 786 } 787 788 // If we found a match, update its value with the new text property 789 // value. 790 if (match.prop) { 791 match.prop.set(newProp); 792 return true; 793 } 794 795 return false; 796 } 797 798 /** 799 * Jump between editable properties in the UI. If the focus direction is 800 * forward, begin editing the next property name if available or focus the 801 * new property editor otherwise. If the focus direction is backward, 802 * begin editing the previous property value or focus the selector editor if 803 * this is the first element in the property list. 804 * 805 * @param {TextProperty} textProperty 806 * The text property that will be left to focus on a sibling. 807 * @param {number} direction 808 * The move focus direction number. 809 */ 810 editClosestTextProperty(textProperty, direction) { 811 let index = this.textProps.indexOf(textProperty); 812 813 if (direction === Services.focus.MOVEFOCUS_FORWARD) { 814 for (++index; index < this.textProps.length; ++index) { 815 // The prop could be invisible or a hidden unused variable 816 if (this.textProps[index].editor) { 817 break; 818 } 819 } 820 if (index === this.textProps.length) { 821 textProperty.rule.editor.closeBrace.click(); 822 } else { 823 this.textProps[index].editor.nameSpan.click(); 824 } 825 } else if (direction === Services.focus.MOVEFOCUS_BACKWARD) { 826 for (--index; index >= 0; --index) { 827 // The prop could be invisible or a hidden unused variable 828 if (this.textProps[index].editor) { 829 break; 830 } 831 } 832 if (index < 0) { 833 textProperty.editor.ruleEditor.selectorText.click(); 834 } else { 835 this.textProps[index].editor.valueSpan.click(); 836 } 837 } 838 } 839 840 /** 841 * Return a string representation of the rule. 842 */ 843 stringifyRule() { 844 const selectorText = this.selectorText; 845 let cssText = ""; 846 const terminator = Services.appinfo.OS === "WINNT" ? "\r\n" : "\n"; 847 848 for (const textProp of this.textProps) { 849 if (!textProp.invisible) { 850 cssText += "\t" + textProp.stringifyProperty() + terminator; 851 } 852 } 853 854 return selectorText + " {" + terminator + cssText + "}"; 855 } 856 857 /** 858 * @returns {boolean} Whether or not the rule is in a layer 859 */ 860 isInLayer() { 861 return this.domRule.ancestorData.some(({ type }) => type === "layer"); 862 } 863 864 /** 865 * Return whether this rule and the one passed are in the same layer, 866 * (as in described in the spec; this is not checking that the 2 rules are children 867 * of the same CSSLayerBlockRule) 868 * 869 * @param {Rule} otherRule: The rule we want to compare with 870 * @returns {boolean} 871 */ 872 isInDifferentLayer(otherRule) { 873 const filterLayer = ({ type }) => type === "layer"; 874 const thisLayers = this.domRule.ancestorData.filter(filterLayer); 875 const otherRuleLayers = otherRule.domRule.ancestorData.filter(filterLayer); 876 877 if (thisLayers.length !== otherRuleLayers.length) { 878 return true; 879 } 880 881 return thisLayers.some((layer, i) => { 882 const otherRuleLayer = otherRuleLayers[i]; 883 // For named layers, we can compare the layer name directly, since we want to identify 884 // the actual layer, not the specific CSSLayerBlockRule. 885 // For nameless layers though, we don't have a choice and we can only identify them 886 // via their CSSLayerBlockRule, so we're using the rule actorID. 887 return ( 888 (layer.value || layer.actorID) !== 889 (otherRuleLayer.value || otherRuleLayer.actorID) 890 ); 891 }); 892 } 893 894 /** 895 * @returns {boolean} Whether or not the rule is in a @starting-style rule 896 */ 897 isInStartingStyle() { 898 return this.domRule.ancestorData.some( 899 ({ type }) => type === "starting-style" 900 ); 901 } 902 903 /** 904 * @returns {boolean} Whether or not the rule can be edited 905 */ 906 isEditable() { 907 return ( 908 !this.isSystem && 909 this.domRule.type !== PRES_HINTS && 910 // FIXME: Should be removed as part of Bug 2004046 911 this.domRule.className !== "CSSPositionTryRule" 912 ); 913 } 914 915 /** 916 * See whether this rule has any non-invisible properties. 917 * 918 * @return {boolean} true if there is any visible property, or false 919 * if all properties are invisible 920 */ 921 hasAnyVisibleProperties() { 922 for (const prop of this.textProps) { 923 if (!prop.invisible) { 924 return true; 925 } 926 } 927 return false; 928 } 929 } 930 931 module.exports = Rule;