browser/devtools/styleinspector/rule-view.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* -*- Mode: javascript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 const {Cc, Ci, Cu} = require("chrome");
michael@0 10 const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 11 const {CssLogic} = require("devtools/styleinspector/css-logic");
michael@0 12 const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
michael@0 13 const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
michael@0 14 const {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
michael@0 15 const {Tooltip, SwatchColorPickerTooltip} = require("devtools/shared/widgets/Tooltip");
michael@0 16 const {OutputParser} = require("devtools/output-parser");
michael@0 17 const {PrefObserver, PREF_ORIG_SOURCES} = require("devtools/styleeditor/utils");
michael@0 18 const {parseSingleValue, parseDeclarations} = require("devtools/styleinspector/css-parsing-utils");
michael@0 19
michael@0 20 Cu.import("resource://gre/modules/Services.jsm");
michael@0 21 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 22
michael@0 23 const HTML_NS = "http://www.w3.org/1999/xhtml";
michael@0 24 const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
michael@0 25
michael@0 26 /**
michael@0 27 * These regular expressions are adapted from firebug's css.js, and are
michael@0 28 * used to parse CSSStyleDeclaration's cssText attribute.
michael@0 29 */
michael@0 30
michael@0 31 // Used to split on css line separators
michael@0 32 const CSS_LINE_RE = /(?:[^;\(]*(?:\([^\)]*?\))?[^;\(]*)*;?/g;
michael@0 33
michael@0 34 // Used to parse a single property line.
michael@0 35 const CSS_PROP_RE = /\s*([^:\s]*)\s*:\s*(.*?)\s*(?:! (important))?;?$/;
michael@0 36
michael@0 37 // Used to parse an external resource from a property value
michael@0 38 const CSS_RESOURCE_RE = /url\([\'\"]?(.*?)[\'\"]?\)/;
michael@0 39
michael@0 40 const IOService = Cc["@mozilla.org/network/io-service;1"]
michael@0 41 .getService(Ci.nsIIOService);
michael@0 42
michael@0 43 function promiseWarn(err) {
michael@0 44 console.error(err);
michael@0 45 return promise.reject(err);
michael@0 46 }
michael@0 47
michael@0 48 /**
michael@0 49 * To figure out how shorthand properties are interpreted by the
michael@0 50 * engine, we will set properties on a dummy element and observe
michael@0 51 * how their .style attribute reflects them as computed values.
michael@0 52 * This function creates the document in which those dummy elements
michael@0 53 * will be created.
michael@0 54 */
michael@0 55 var gDummyPromise;
michael@0 56 function createDummyDocument() {
michael@0 57 if (gDummyPromise) {
michael@0 58 return gDummyPromise;
michael@0 59 }
michael@0 60 const { getDocShell, create: makeFrame } = require("sdk/frame/utils");
michael@0 61
michael@0 62 let frame = makeFrame(Services.appShell.hiddenDOMWindow.document, {
michael@0 63 nodeName: "iframe",
michael@0 64 namespaceURI: "http://www.w3.org/1999/xhtml",
michael@0 65 allowJavascript: false,
michael@0 66 allowPlugins: false,
michael@0 67 allowAuth: false
michael@0 68 });
michael@0 69 let docShell = getDocShell(frame);
michael@0 70 let eventTarget = docShell.chromeEventHandler;
michael@0 71 docShell.createAboutBlankContentViewer(Cc["@mozilla.org/nullprincipal;1"].createInstance(Ci.nsIPrincipal));
michael@0 72 let window = docShell.contentViewer.DOMDocument.defaultView;
michael@0 73 window.location = "data:text/html,<html></html>";
michael@0 74 let deferred = promise.defer();
michael@0 75 eventTarget.addEventListener("DOMContentLoaded", function handler(event) {
michael@0 76 eventTarget.removeEventListener("DOMContentLoaded", handler, false);
michael@0 77 deferred.resolve(window.document);
michael@0 78 frame.remove();
michael@0 79 }, false);
michael@0 80 gDummyPromise = deferred.promise;
michael@0 81 return gDummyPromise;
michael@0 82 }
michael@0 83
michael@0 84 /**
michael@0 85 * Our model looks like this:
michael@0 86 *
michael@0 87 * ElementStyle:
michael@0 88 * Responsible for keeping track of which properties are overridden.
michael@0 89 * Maintains a list of Rule objects that apply to the element.
michael@0 90 * Rule:
michael@0 91 * Manages a single style declaration or rule.
michael@0 92 * Responsible for applying changes to the properties in a rule.
michael@0 93 * Maintains a list of TextProperty objects.
michael@0 94 * TextProperty:
michael@0 95 * Manages a single property from the cssText attribute of the
michael@0 96 * relevant declaration.
michael@0 97 * Maintains a list of computed properties that come from this
michael@0 98 * property declaration.
michael@0 99 * Changes to the TextProperty are sent to its related Rule for
michael@0 100 * application.
michael@0 101 */
michael@0 102
michael@0 103 /**
michael@0 104 * ElementStyle maintains a list of Rule objects for a given element.
michael@0 105 *
michael@0 106 * @param {Element} aElement
michael@0 107 * The element whose style we are viewing.
michael@0 108 * @param {object} aStore
michael@0 109 * The ElementStyle can use this object to store metadata
michael@0 110 * that might outlast the rule view, particularly the current
michael@0 111 * set of disabled properties.
michael@0 112 * @param {PageStyleFront} aPageStyle
michael@0 113 * Front for the page style actor that will be providing
michael@0 114 * the style information.
michael@0 115 *
michael@0 116 * @constructor
michael@0 117 */
michael@0 118 function ElementStyle(aElement, aStore, aPageStyle) {
michael@0 119 this.element = aElement;
michael@0 120 this.store = aStore || {};
michael@0 121 this.pageStyle = aPageStyle;
michael@0 122
michael@0 123 // We don't want to overwrite this.store.userProperties so we only create it
michael@0 124 // if it doesn't already exist.
michael@0 125 if (!("userProperties" in this.store)) {
michael@0 126 this.store.userProperties = new UserProperties();
michael@0 127 }
michael@0 128
michael@0 129 if (!("disabled" in this.store)) {
michael@0 130 this.store.disabled = new WeakMap();
michael@0 131 }
michael@0 132 }
michael@0 133
michael@0 134 // We're exporting _ElementStyle for unit tests.
michael@0 135 exports._ElementStyle = ElementStyle;
michael@0 136
michael@0 137 ElementStyle.prototype = {
michael@0 138 // The element we're looking at.
michael@0 139 element: null,
michael@0 140
michael@0 141 // Empty, unconnected element of the same type as this node, used
michael@0 142 // to figure out how shorthand properties will be parsed.
michael@0 143 dummyElement: null,
michael@0 144
michael@0 145 init: function()
michael@0 146 {
michael@0 147 // To figure out how shorthand properties are interpreted by the
michael@0 148 // engine, we will set properties on a dummy element and observe
michael@0 149 // how their .style attribute reflects them as computed values.
michael@0 150 return this.dummyElementPromise = createDummyDocument().then(document => {
michael@0 151 this.dummyElement = document.createElementNS(this.element.namespaceURI,
michael@0 152 this.element.tagName);
michael@0 153 document.documentElement.appendChild(this.dummyElement);
michael@0 154 return this.dummyElement;
michael@0 155 }).then(null, promiseWarn);
michael@0 156 },
michael@0 157
michael@0 158 destroy: function() {
michael@0 159 this.dummyElement = null;
michael@0 160 this.dummyElementPromise.then(dummyElement => {
michael@0 161 if (dummyElement.parentNode) {
michael@0 162 dummyElement.parentNode.removeChild(dummyElement);
michael@0 163 }
michael@0 164 this.dummyElementPromise = null;
michael@0 165 });
michael@0 166 },
michael@0 167
michael@0 168 /**
michael@0 169 * Called by the Rule object when it has been changed through the
michael@0 170 * setProperty* methods.
michael@0 171 */
michael@0 172 _changed: function() {
michael@0 173 if (this.onChanged) {
michael@0 174 this.onChanged();
michael@0 175 }
michael@0 176 },
michael@0 177
michael@0 178 /**
michael@0 179 * Refresh the list of rules to be displayed for the active element.
michael@0 180 * Upon completion, this.rules[] will hold a list of Rule objects.
michael@0 181 *
michael@0 182 * Returns a promise that will be resolved when the elementStyle is
michael@0 183 * ready.
michael@0 184 */
michael@0 185 populate: function() {
michael@0 186 let populated = this.pageStyle.getApplied(this.element, {
michael@0 187 inherited: true,
michael@0 188 matchedSelectors: true
michael@0 189 }).then(entries => {
michael@0 190 // Make sure the dummy element has been created before continuing...
michael@0 191 return this.dummyElementPromise.then(() => {
michael@0 192 if (this.populated != populated) {
michael@0 193 // Don't care anymore.
michael@0 194 return promise.reject("unused");
michael@0 195 }
michael@0 196
michael@0 197 // Store the current list of rules (if any) during the population
michael@0 198 // process. They will be reused if possible.
michael@0 199 this._refreshRules = this.rules;
michael@0 200
michael@0 201 this.rules = [];
michael@0 202
michael@0 203 for (let entry of entries) {
michael@0 204 this._maybeAddRule(entry);
michael@0 205 }
michael@0 206
michael@0 207 // Mark overridden computed styles.
michael@0 208 this.markOverriddenAll();
michael@0 209
michael@0 210 this._sortRulesForPseudoElement();
michael@0 211
michael@0 212 // We're done with the previous list of rules.
michael@0 213 delete this._refreshRules;
michael@0 214
michael@0 215 return null;
michael@0 216 });
michael@0 217 }).then(null, promiseWarn);
michael@0 218 this.populated = populated;
michael@0 219 return this.populated;
michael@0 220 },
michael@0 221
michael@0 222 /**
michael@0 223 * Put pseudo elements in front of others.
michael@0 224 */
michael@0 225 _sortRulesForPseudoElement: function() {
michael@0 226 this.rules = this.rules.sort((a, b) => {
michael@0 227 return (a.pseudoElement || "z") > (b.pseudoElement || "z");
michael@0 228 });
michael@0 229 },
michael@0 230
michael@0 231 /**
michael@0 232 * Add a rule if it's one we care about. Filters out duplicates and
michael@0 233 * inherited styles with no inherited properties.
michael@0 234 *
michael@0 235 * @param {object} aOptions
michael@0 236 * Options for creating the Rule, see the Rule constructor.
michael@0 237 *
michael@0 238 * @return {bool} true if we added the rule.
michael@0 239 */
michael@0 240 _maybeAddRule: function(aOptions) {
michael@0 241 // If we've already included this domRule (for example, when a
michael@0 242 // common selector is inherited), ignore it.
michael@0 243 if (aOptions.rule &&
michael@0 244 this.rules.some(function(rule) rule.domRule === aOptions.rule)) {
michael@0 245 return false;
michael@0 246 }
michael@0 247
michael@0 248 if (aOptions.system) {
michael@0 249 return false;
michael@0 250 }
michael@0 251
michael@0 252 let rule = null;
michael@0 253
michael@0 254 // If we're refreshing and the rule previously existed, reuse the
michael@0 255 // Rule object.
michael@0 256 if (this._refreshRules) {
michael@0 257 for (let r of this._refreshRules) {
michael@0 258 if (r.matches(aOptions)) {
michael@0 259 rule = r;
michael@0 260 rule.refresh(aOptions);
michael@0 261 break;
michael@0 262 }
michael@0 263 }
michael@0 264 }
michael@0 265
michael@0 266 // If this is a new rule, create its Rule object.
michael@0 267 if (!rule) {
michael@0 268 rule = new Rule(this, aOptions);
michael@0 269 }
michael@0 270
michael@0 271 // Ignore inherited rules with no properties.
michael@0 272 if (aOptions.inherited && rule.textProps.length == 0) {
michael@0 273 return false;
michael@0 274 }
michael@0 275
michael@0 276 this.rules.push(rule);
michael@0 277 return true;
michael@0 278 },
michael@0 279
michael@0 280 /**
michael@0 281 * Calls markOverridden with all supported pseudo elements
michael@0 282 */
michael@0 283 markOverriddenAll: function() {
michael@0 284 this.markOverridden();
michael@0 285 for (let pseudo of PSEUDO_ELEMENTS) {
michael@0 286 this.markOverridden(pseudo);
michael@0 287 }
michael@0 288 },
michael@0 289
michael@0 290 /**
michael@0 291 * Mark the properties listed in this.rules for a given pseudo element
michael@0 292 * with an overridden flag if an earlier property overrides it.
michael@0 293 * @param {string} pseudo
michael@0 294 * Which pseudo element to flag as overridden.
michael@0 295 * Empty string or undefined will default to no pseudo element.
michael@0 296 */
michael@0 297 markOverridden: function(pseudo="") {
michael@0 298 // Gather all the text properties applied by these rules, ordered
michael@0 299 // from more- to less-specific.
michael@0 300 let textProps = [];
michael@0 301 for (let rule of this.rules) {
michael@0 302 if (rule.pseudoElement == pseudo) {
michael@0 303 textProps = textProps.concat(rule.textProps.slice(0).reverse());
michael@0 304 }
michael@0 305 }
michael@0 306
michael@0 307 // Gather all the computed properties applied by those text
michael@0 308 // properties.
michael@0 309 let computedProps = [];
michael@0 310 for (let textProp of textProps) {
michael@0 311 computedProps = computedProps.concat(textProp.computed);
michael@0 312 }
michael@0 313
michael@0 314 // Walk over the computed properties. As we see a property name
michael@0 315 // for the first time, mark that property's name as taken by this
michael@0 316 // property.
michael@0 317 //
michael@0 318 // If we come across a property whose name is already taken, check
michael@0 319 // its priority against the property that was found first:
michael@0 320 //
michael@0 321 // If the new property is a higher priority, mark the old
michael@0 322 // property overridden and mark the property name as taken by
michael@0 323 // the new property.
michael@0 324 //
michael@0 325 // If the new property is a lower or equal priority, mark it as
michael@0 326 // overridden.
michael@0 327 //
michael@0 328 // _overriddenDirty will be set on each prop, indicating whether its
michael@0 329 // dirty status changed during this pass.
michael@0 330 let taken = {};
michael@0 331 for (let computedProp of computedProps) {
michael@0 332 let earlier = taken[computedProp.name];
michael@0 333 let overridden;
michael@0 334 if (earlier &&
michael@0 335 computedProp.priority === "important" &&
michael@0 336 earlier.priority !== "important") {
michael@0 337 // New property is higher priority. Mark the earlier property
michael@0 338 // overridden (which will reverse its dirty state).
michael@0 339 earlier._overriddenDirty = !earlier._overriddenDirty;
michael@0 340 earlier.overridden = true;
michael@0 341 overridden = false;
michael@0 342 } else {
michael@0 343 overridden = !!earlier;
michael@0 344 }
michael@0 345
michael@0 346 computedProp._overriddenDirty = (!!computedProp.overridden != overridden);
michael@0 347 computedProp.overridden = overridden;
michael@0 348 if (!computedProp.overridden && computedProp.textProp.enabled) {
michael@0 349 taken[computedProp.name] = computedProp;
michael@0 350 }
michael@0 351 }
michael@0 352
michael@0 353 // For each TextProperty, mark it overridden if all of its
michael@0 354 // computed properties are marked overridden. Update the text
michael@0 355 // property's associated editor, if any. This will clear the
michael@0 356 // _overriddenDirty state on all computed properties.
michael@0 357 for (let textProp of textProps) {
michael@0 358 // _updatePropertyOverridden will return true if the
michael@0 359 // overridden state has changed for the text property.
michael@0 360 if (this._updatePropertyOverridden(textProp)) {
michael@0 361 textProp.updateEditor();
michael@0 362 }
michael@0 363 }
michael@0 364 },
michael@0 365
michael@0 366 /**
michael@0 367 * Mark a given TextProperty as overridden or not depending on the
michael@0 368 * state of its computed properties. Clears the _overriddenDirty state
michael@0 369 * on all computed properties.
michael@0 370 *
michael@0 371 * @param {TextProperty} aProp
michael@0 372 * The text property to update.
michael@0 373 *
michael@0 374 * @return {bool} true if the TextProperty's overridden state (or any of its
michael@0 375 * computed properties overridden state) changed.
michael@0 376 */
michael@0 377 _updatePropertyOverridden: function(aProp) {
michael@0 378 let overridden = true;
michael@0 379 let dirty = false;
michael@0 380 for each (let computedProp in aProp.computed) {
michael@0 381 if (!computedProp.overridden) {
michael@0 382 overridden = false;
michael@0 383 }
michael@0 384 dirty = computedProp._overriddenDirty || dirty;
michael@0 385 delete computedProp._overriddenDirty;
michael@0 386 }
michael@0 387
michael@0 388 dirty = (!!aProp.overridden != overridden) || dirty;
michael@0 389 aProp.overridden = overridden;
michael@0 390 return dirty;
michael@0 391 }
michael@0 392 };
michael@0 393
michael@0 394 /**
michael@0 395 * A single style rule or declaration.
michael@0 396 *
michael@0 397 * @param {ElementStyle} aElementStyle
michael@0 398 * The ElementStyle to which this rule belongs.
michael@0 399 * @param {object} aOptions
michael@0 400 * The information used to construct this rule. Properties include:
michael@0 401 * rule: A StyleRuleActor
michael@0 402 * inherited: An element this rule was inherited from. If omitted,
michael@0 403 * the rule applies directly to the current element.
michael@0 404 * @constructor
michael@0 405 */
michael@0 406 function Rule(aElementStyle, aOptions) {
michael@0 407 this.elementStyle = aElementStyle;
michael@0 408 this.domRule = aOptions.rule || null;
michael@0 409 this.style = aOptions.rule;
michael@0 410 this.matchedSelectors = aOptions.matchedSelectors || [];
michael@0 411 this.pseudoElement = aOptions.pseudoElement || "";
michael@0 412
michael@0 413 this.inherited = aOptions.inherited || null;
michael@0 414 this._modificationDepth = 0;
michael@0 415
michael@0 416 if (this.domRule) {
michael@0 417 let parentRule = this.domRule.parentRule;
michael@0 418 if (parentRule && parentRule.type == Ci.nsIDOMCSSRule.MEDIA_RULE) {
michael@0 419 this.mediaText = parentRule.mediaText;
michael@0 420 }
michael@0 421 }
michael@0 422
michael@0 423 // Populate the text properties with the style's current cssText
michael@0 424 // value, and add in any disabled properties from the store.
michael@0 425 this.textProps = this._getTextProperties();
michael@0 426 this.textProps = this.textProps.concat(this._getDisabledProperties());
michael@0 427 }
michael@0 428
michael@0 429 Rule.prototype = {
michael@0 430 mediaText: "",
michael@0 431
michael@0 432 get title() {
michael@0 433 if (this._title) {
michael@0 434 return this._title;
michael@0 435 }
michael@0 436 this._title = CssLogic.shortSource(this.sheet);
michael@0 437 if (this.domRule.type !== ELEMENT_STYLE) {
michael@0 438 this._title += ":" + this.ruleLine;
michael@0 439 }
michael@0 440
michael@0 441 this._title = this._title + (this.mediaText ? " @media " + this.mediaText : "");
michael@0 442 return this._title;
michael@0 443 },
michael@0 444
michael@0 445 get inheritedSource() {
michael@0 446 if (this._inheritedSource) {
michael@0 447 return this._inheritedSource;
michael@0 448 }
michael@0 449 this._inheritedSource = "";
michael@0 450 if (this.inherited) {
michael@0 451 let eltText = this.inherited.tagName.toLowerCase();
michael@0 452 if (this.inherited.id) {
michael@0 453 eltText += "#" + this.inherited.id;
michael@0 454 }
michael@0 455 this._inheritedSource =
michael@0 456 CssLogic._strings.formatStringFromName("rule.inheritedFrom", [eltText], 1);
michael@0 457 }
michael@0 458 return this._inheritedSource;
michael@0 459 },
michael@0 460
michael@0 461 get selectorText() {
michael@0 462 return this.domRule.selectors ? this.domRule.selectors.join(", ") : CssLogic.l10n("rule.sourceElement");
michael@0 463 },
michael@0 464
michael@0 465 /**
michael@0 466 * The rule's stylesheet.
michael@0 467 */
michael@0 468 get sheet() {
michael@0 469 return this.domRule ? this.domRule.parentStyleSheet : null;
michael@0 470 },
michael@0 471
michael@0 472 /**
michael@0 473 * The rule's line within a stylesheet
michael@0 474 */
michael@0 475 get ruleLine() {
michael@0 476 return this.domRule ? this.domRule.line : null;
michael@0 477 },
michael@0 478
michael@0 479 /**
michael@0 480 * The rule's column within a stylesheet
michael@0 481 */
michael@0 482 get ruleColumn() {
michael@0 483 return this.domRule ? this.domRule.column : null;
michael@0 484 },
michael@0 485
michael@0 486 /**
michael@0 487 * Get display name for this rule based on the original source
michael@0 488 * for this rule's style sheet.
michael@0 489 *
michael@0 490 * @return {Promise}
michael@0 491 * Promise which resolves with location as an object containing
michael@0 492 * both the full and short version of the source string.
michael@0 493 */
michael@0 494 getOriginalSourceStrings: function() {
michael@0 495 if (this._originalSourceStrings) {
michael@0 496 return promise.resolve(this._originalSourceStrings);
michael@0 497 }
michael@0 498 return this.domRule.getOriginalLocation().then(({href, line}) => {
michael@0 499 let sourceStrings = {
michael@0 500 full: href + ":" + line,
michael@0 501 short: CssLogic.shortSource({href: href}) + ":" + line
michael@0 502 };
michael@0 503
michael@0 504 this._originalSourceStrings = sourceStrings;
michael@0 505 return sourceStrings;
michael@0 506 });
michael@0 507 },
michael@0 508
michael@0 509 /**
michael@0 510 * Returns true if the rule matches the creation options
michael@0 511 * specified.
michael@0 512 *
michael@0 513 * @param {object} aOptions
michael@0 514 * Creation options. See the Rule constructor for documentation.
michael@0 515 */
michael@0 516 matches: function(aOptions) {
michael@0 517 return this.style === aOptions.rule;
michael@0 518 },
michael@0 519
michael@0 520 /**
michael@0 521 * Create a new TextProperty to include in the rule.
michael@0 522 *
michael@0 523 * @param {string} aName
michael@0 524 * The text property name (such as "background" or "border-top").
michael@0 525 * @param {string} aValue
michael@0 526 * The property's value (not including priority).
michael@0 527 * @param {string} aPriority
michael@0 528 * The property's priority (either "important" or an empty string).
michael@0 529 * @param {TextProperty} aSiblingProp
michael@0 530 * Optional, property next to which the new property will be added.
michael@0 531 */
michael@0 532 createProperty: function(aName, aValue, aPriority, aSiblingProp) {
michael@0 533 let prop = new TextProperty(this, aName, aValue, aPriority);
michael@0 534
michael@0 535 if (aSiblingProp) {
michael@0 536 let ind = this.textProps.indexOf(aSiblingProp);
michael@0 537 this.textProps.splice(ind + 1, 0, prop);
michael@0 538 }
michael@0 539 else {
michael@0 540 this.textProps.push(prop);
michael@0 541 }
michael@0 542
michael@0 543 this.applyProperties();
michael@0 544 return prop;
michael@0 545 },
michael@0 546
michael@0 547 /**
michael@0 548 * Reapply all the properties in this rule, and update their
michael@0 549 * computed styles. Store disabled properties in the element
michael@0 550 * style's store. Will re-mark overridden properties.
michael@0 551 *
michael@0 552 * @param {string} [aName]
michael@0 553 * A text property name (such as "background" or "border-top") used
michael@0 554 * when calling from setPropertyValue & setPropertyName to signify
michael@0 555 * that the property should be saved in store.userProperties.
michael@0 556 */
michael@0 557 applyProperties: function(aModifications, aName) {
michael@0 558 this.elementStyle.markOverriddenAll();
michael@0 559
michael@0 560 if (!aModifications) {
michael@0 561 aModifications = this.style.startModifyingProperties();
michael@0 562 }
michael@0 563 let disabledProps = [];
michael@0 564 let store = this.elementStyle.store;
michael@0 565
michael@0 566 for (let prop of this.textProps) {
michael@0 567 if (!prop.enabled) {
michael@0 568 disabledProps.push({
michael@0 569 name: prop.name,
michael@0 570 value: prop.value,
michael@0 571 priority: prop.priority
michael@0 572 });
michael@0 573 continue;
michael@0 574 }
michael@0 575 if (prop.value.trim() === "") {
michael@0 576 continue;
michael@0 577 }
michael@0 578
michael@0 579 aModifications.setProperty(prop.name, prop.value, prop.priority);
michael@0 580
michael@0 581 prop.updateComputed();
michael@0 582 }
michael@0 583
michael@0 584 // Store disabled properties in the disabled store.
michael@0 585 let disabled = this.elementStyle.store.disabled;
michael@0 586 if (disabledProps.length > 0) {
michael@0 587 disabled.set(this.style, disabledProps);
michael@0 588 } else {
michael@0 589 disabled.delete(this.style);
michael@0 590 }
michael@0 591
michael@0 592 let promise = aModifications.apply().then(() => {
michael@0 593 let cssProps = {};
michael@0 594 for (let cssProp of parseDeclarations(this.style.cssText)) {
michael@0 595 cssProps[cssProp.name] = cssProp;
michael@0 596 }
michael@0 597
michael@0 598 for (let textProp of this.textProps) {
michael@0 599 if (!textProp.enabled) {
michael@0 600 continue;
michael@0 601 }
michael@0 602 let cssProp = cssProps[textProp.name];
michael@0 603
michael@0 604 if (!cssProp) {
michael@0 605 cssProp = {
michael@0 606 name: textProp.name,
michael@0 607 value: "",
michael@0 608 priority: ""
michael@0 609 };
michael@0 610 }
michael@0 611
michael@0 612 if (aName && textProp.name == aName) {
michael@0 613 store.userProperties.setProperty(
michael@0 614 this.style,
michael@0 615 textProp.name,
michael@0 616 textProp.value);
michael@0 617 }
michael@0 618 textProp.priority = cssProp.priority;
michael@0 619 }
michael@0 620
michael@0 621 this.elementStyle.markOverriddenAll();
michael@0 622
michael@0 623 if (promise === this._applyingModifications) {
michael@0 624 this._applyingModifications = null;
michael@0 625 }
michael@0 626
michael@0 627 this.elementStyle._changed();
michael@0 628 }).then(null, promiseWarn);
michael@0 629
michael@0 630 this._applyingModifications = promise;
michael@0 631 return promise;
michael@0 632 },
michael@0 633
michael@0 634 /**
michael@0 635 * Renames a property.
michael@0 636 *
michael@0 637 * @param {TextProperty} aProperty
michael@0 638 * The property to rename.
michael@0 639 * @param {string} aName
michael@0 640 * The new property name (such as "background" or "border-top").
michael@0 641 */
michael@0 642 setPropertyName: function(aProperty, aName) {
michael@0 643 if (aName === aProperty.name) {
michael@0 644 return;
michael@0 645 }
michael@0 646 let modifications = this.style.startModifyingProperties();
michael@0 647 modifications.removeProperty(aProperty.name);
michael@0 648 aProperty.name = aName;
michael@0 649 this.applyProperties(modifications, aName);
michael@0 650 },
michael@0 651
michael@0 652 /**
michael@0 653 * Sets the value and priority of a property, then reapply all properties.
michael@0 654 *
michael@0 655 * @param {TextProperty} aProperty
michael@0 656 * The property to manipulate.
michael@0 657 * @param {string} aValue
michael@0 658 * The property's value (not including priority).
michael@0 659 * @param {string} aPriority
michael@0 660 * The property's priority (either "important" or an empty string).
michael@0 661 */
michael@0 662 setPropertyValue: function(aProperty, aValue, aPriority) {
michael@0 663 if (aValue === aProperty.value && aPriority === aProperty.priority) {
michael@0 664 return;
michael@0 665 }
michael@0 666
michael@0 667 aProperty.value = aValue;
michael@0 668 aProperty.priority = aPriority;
michael@0 669 this.applyProperties(null, aProperty.name);
michael@0 670 },
michael@0 671
michael@0 672 /**
michael@0 673 * Just sets the value and priority of a property, in order to preview its
michael@0 674 * effect on the content document.
michael@0 675 *
michael@0 676 * @param {TextProperty} aProperty
michael@0 677 * The property which value will be previewed
michael@0 678 * @param {String} aValue
michael@0 679 * The value to be used for the preview
michael@0 680 * @param {String} aPriority
michael@0 681 * The property's priority (either "important" or an empty string).
michael@0 682 */
michael@0 683 previewPropertyValue: function(aProperty, aValue, aPriority) {
michael@0 684 let modifications = this.style.startModifyingProperties();
michael@0 685 modifications.setProperty(aProperty.name, aValue, aPriority);
michael@0 686 modifications.apply();
michael@0 687 },
michael@0 688
michael@0 689 /**
michael@0 690 * Disables or enables given TextProperty.
michael@0 691 *
michael@0 692 * @param {TextProperty} aProperty
michael@0 693 * The property to enable/disable
michael@0 694 * @param {Boolean} aValue
michael@0 695 */
michael@0 696 setPropertyEnabled: function(aProperty, aValue) {
michael@0 697 aProperty.enabled = !!aValue;
michael@0 698 let modifications = this.style.startModifyingProperties();
michael@0 699 if (!aProperty.enabled) {
michael@0 700 modifications.removeProperty(aProperty.name);
michael@0 701 }
michael@0 702 this.applyProperties(modifications);
michael@0 703 },
michael@0 704
michael@0 705 /**
michael@0 706 * Remove a given TextProperty from the rule and update the rule
michael@0 707 * accordingly.
michael@0 708 *
michael@0 709 * @param {TextProperty} aProperty
michael@0 710 * The property to be removed
michael@0 711 */
michael@0 712 removeProperty: function(aProperty) {
michael@0 713 this.textProps = this.textProps.filter(function(prop) prop != aProperty);
michael@0 714 let modifications = this.style.startModifyingProperties();
michael@0 715 modifications.removeProperty(aProperty.name);
michael@0 716 // Need to re-apply properties in case removing this TextProperty
michael@0 717 // exposes another one.
michael@0 718 this.applyProperties(modifications);
michael@0 719 },
michael@0 720
michael@0 721 /**
michael@0 722 * Get the list of TextProperties from the style. Needs
michael@0 723 * to parse the style's cssText.
michael@0 724 */
michael@0 725 _getTextProperties: function() {
michael@0 726 let textProps = [];
michael@0 727 let store = this.elementStyle.store;
michael@0 728 let props = parseDeclarations(this.style.cssText);
michael@0 729 for (let prop of props) {
michael@0 730 let name = prop.name;
michael@0 731 if (this.inherited && !domUtils.isInheritedProperty(name)) {
michael@0 732 continue;
michael@0 733 }
michael@0 734 let value = store.userProperties.getProperty(this.style, name, prop.value);
michael@0 735 let textProp = new TextProperty(this, name, value, prop.priority);
michael@0 736 textProps.push(textProp);
michael@0 737 }
michael@0 738
michael@0 739 return textProps;
michael@0 740 },
michael@0 741
michael@0 742 /**
michael@0 743 * Return the list of disabled properties from the store for this rule.
michael@0 744 */
michael@0 745 _getDisabledProperties: function() {
michael@0 746 let store = this.elementStyle.store;
michael@0 747
michael@0 748 // Include properties from the disabled property store, if any.
michael@0 749 let disabledProps = store.disabled.get(this.style);
michael@0 750 if (!disabledProps) {
michael@0 751 return [];
michael@0 752 }
michael@0 753
michael@0 754 let textProps = [];
michael@0 755
michael@0 756 for each (let prop in disabledProps) {
michael@0 757 let value = store.userProperties.getProperty(this.style, prop.name, prop.value);
michael@0 758 let textProp = new TextProperty(this, prop.name, value, prop.priority);
michael@0 759 textProp.enabled = false;
michael@0 760 textProps.push(textProp);
michael@0 761 }
michael@0 762
michael@0 763 return textProps;
michael@0 764 },
michael@0 765
michael@0 766 /**
michael@0 767 * Reread the current state of the rules and rebuild text
michael@0 768 * properties as needed.
michael@0 769 */
michael@0 770 refresh: function(aOptions) {
michael@0 771 this.matchedSelectors = aOptions.matchedSelectors || [];
michael@0 772 let newTextProps = this._getTextProperties();
michael@0 773
michael@0 774 // Update current properties for each property present on the style.
michael@0 775 // This will mark any touched properties with _visited so we
michael@0 776 // can detect properties that weren't touched (because they were
michael@0 777 // removed from the style).
michael@0 778 // Also keep track of properties that didn't exist in the current set
michael@0 779 // of properties.
michael@0 780 let brandNewProps = [];
michael@0 781 for (let newProp of newTextProps) {
michael@0 782 if (!this._updateTextProperty(newProp)) {
michael@0 783 brandNewProps.push(newProp);
michael@0 784 }
michael@0 785 }
michael@0 786
michael@0 787 // Refresh editors and disabled state for all the properties that
michael@0 788 // were updated.
michael@0 789 for (let prop of this.textProps) {
michael@0 790 // Properties that weren't touched during the update
michael@0 791 // process must no longer exist on the node. Mark them disabled.
michael@0 792 if (!prop._visited) {
michael@0 793 prop.enabled = false;
michael@0 794 prop.updateEditor();
michael@0 795 } else {
michael@0 796 delete prop._visited;
michael@0 797 }
michael@0 798 }
michael@0 799
michael@0 800 // Add brand new properties.
michael@0 801 this.textProps = this.textProps.concat(brandNewProps);
michael@0 802
michael@0 803 // Refresh the editor if one already exists.
michael@0 804 if (this.editor) {
michael@0 805 this.editor.populate();
michael@0 806 }
michael@0 807 },
michael@0 808
michael@0 809 /**
michael@0 810 * Update the current TextProperties that match a given property
michael@0 811 * from the cssText. Will choose one existing TextProperty to update
michael@0 812 * with the new property's value, and will disable all others.
michael@0 813 *
michael@0 814 * When choosing the best match to reuse, properties will be chosen
michael@0 815 * by assigning a rank and choosing the highest-ranked property:
michael@0 816 * Name, value, and priority match, enabled. (6)
michael@0 817 * Name, value, and priority match, disabled. (5)
michael@0 818 * Name and value match, enabled. (4)
michael@0 819 * Name and value match, disabled. (3)
michael@0 820 * Name matches, enabled. (2)
michael@0 821 * Name matches, disabled. (1)
michael@0 822 *
michael@0 823 * If no existing properties match the property, nothing happens.
michael@0 824 *
michael@0 825 * @param {TextProperty} aNewProp
michael@0 826 * The current version of the property, as parsed from the
michael@0 827 * cssText in Rule._getTextProperties().
michael@0 828 *
michael@0 829 * @return {bool} true if a property was updated, false if no properties
michael@0 830 * were updated.
michael@0 831 */
michael@0 832 _updateTextProperty: function(aNewProp) {
michael@0 833 let match = { rank: 0, prop: null };
michael@0 834
michael@0 835 for each (let prop in this.textProps) {
michael@0 836 if (prop.name != aNewProp.name)
michael@0 837 continue;
michael@0 838
michael@0 839 // Mark this property visited.
michael@0 840 prop._visited = true;
michael@0 841
michael@0 842 // Start at rank 1 for matching name.
michael@0 843 let rank = 1;
michael@0 844
michael@0 845 // Value and Priority matches add 2 to the rank.
michael@0 846 // Being enabled adds 1. This ranks better matches higher,
michael@0 847 // with priority breaking ties.
michael@0 848 if (prop.value === aNewProp.value) {
michael@0 849 rank += 2;
michael@0 850 if (prop.priority === aNewProp.priority) {
michael@0 851 rank += 2;
michael@0 852 }
michael@0 853 }
michael@0 854
michael@0 855 if (prop.enabled) {
michael@0 856 rank += 1;
michael@0 857 }
michael@0 858
michael@0 859 if (rank > match.rank) {
michael@0 860 if (match.prop) {
michael@0 861 // We outrank a previous match, disable it.
michael@0 862 match.prop.enabled = false;
michael@0 863 match.prop.updateEditor();
michael@0 864 }
michael@0 865 match.rank = rank;
michael@0 866 match.prop = prop;
michael@0 867 } else if (rank) {
michael@0 868 // A previous match outranks us, disable ourself.
michael@0 869 prop.enabled = false;
michael@0 870 prop.updateEditor();
michael@0 871 }
michael@0 872 }
michael@0 873
michael@0 874 // If we found a match, update its value with the new text property
michael@0 875 // value.
michael@0 876 if (match.prop) {
michael@0 877 match.prop.set(aNewProp);
michael@0 878 return true;
michael@0 879 }
michael@0 880
michael@0 881 return false;
michael@0 882 },
michael@0 883
michael@0 884 /**
michael@0 885 * Jump between editable properties in the UI. Will begin editing the next
michael@0 886 * name, if possible. If this is the last element in the set, then begin
michael@0 887 * editing the previous value. If this is the *only* element in the set,
michael@0 888 * then settle for focusing the new property editor.
michael@0 889 *
michael@0 890 * @param {TextProperty} aTextProperty
michael@0 891 * The text property that will be left to focus on a sibling.
michael@0 892 *
michael@0 893 */
michael@0 894 editClosestTextProperty: function(aTextProperty) {
michael@0 895 let index = this.textProps.indexOf(aTextProperty);
michael@0 896 let previous = false;
michael@0 897
michael@0 898 // If this is the last element, move to the previous instead of next
michael@0 899 if (index === this.textProps.length - 1) {
michael@0 900 index = index - 1;
michael@0 901 previous = true;
michael@0 902 }
michael@0 903 else {
michael@0 904 index = index + 1;
michael@0 905 }
michael@0 906
michael@0 907 let nextProp = this.textProps[index];
michael@0 908
michael@0 909 // If possible, begin editing the next name or previous value.
michael@0 910 // Otherwise, settle for focusing the new property element.
michael@0 911 if (nextProp) {
michael@0 912 if (previous) {
michael@0 913 nextProp.editor.valueSpan.click();
michael@0 914 } else {
michael@0 915 nextProp.editor.nameSpan.click();
michael@0 916 }
michael@0 917 } else {
michael@0 918 aTextProperty.rule.editor.closeBrace.focus();
michael@0 919 }
michael@0 920 }
michael@0 921 };
michael@0 922
michael@0 923 /**
michael@0 924 * A single property in a rule's cssText.
michael@0 925 *
michael@0 926 * @param {Rule} aRule
michael@0 927 * The rule this TextProperty came from.
michael@0 928 * @param {string} aName
michael@0 929 * The text property name (such as "background" or "border-top").
michael@0 930 * @param {string} aValue
michael@0 931 * The property's value (not including priority).
michael@0 932 * @param {string} aPriority
michael@0 933 * The property's priority (either "important" or an empty string).
michael@0 934 *
michael@0 935 */
michael@0 936 function TextProperty(aRule, aName, aValue, aPriority) {
michael@0 937 this.rule = aRule;
michael@0 938 this.name = aName;
michael@0 939 this.value = aValue;
michael@0 940 this.priority = aPriority;
michael@0 941 this.enabled = true;
michael@0 942 this.updateComputed();
michael@0 943 }
michael@0 944
michael@0 945 TextProperty.prototype = {
michael@0 946 /**
michael@0 947 * Update the editor associated with this text property,
michael@0 948 * if any.
michael@0 949 */
michael@0 950 updateEditor: function() {
michael@0 951 if (this.editor) {
michael@0 952 this.editor.update();
michael@0 953 }
michael@0 954 },
michael@0 955
michael@0 956 /**
michael@0 957 * Update the list of computed properties for this text property.
michael@0 958 */
michael@0 959 updateComputed: function() {
michael@0 960 if (!this.name) {
michael@0 961 return;
michael@0 962 }
michael@0 963
michael@0 964 // This is a bit funky. To get the list of computed properties
michael@0 965 // for this text property, we'll set the property on a dummy element
michael@0 966 // and see what the computed style looks like.
michael@0 967 let dummyElement = this.rule.elementStyle.dummyElement;
michael@0 968 let dummyStyle = dummyElement.style;
michael@0 969 dummyStyle.cssText = "";
michael@0 970 dummyStyle.setProperty(this.name, this.value, this.priority);
michael@0 971
michael@0 972 this.computed = [];
michael@0 973 for (let i = 0, n = dummyStyle.length; i < n; i++) {
michael@0 974 let prop = dummyStyle.item(i);
michael@0 975 this.computed.push({
michael@0 976 textProp: this,
michael@0 977 name: prop,
michael@0 978 value: dummyStyle.getPropertyValue(prop),
michael@0 979 priority: dummyStyle.getPropertyPriority(prop),
michael@0 980 });
michael@0 981 }
michael@0 982 },
michael@0 983
michael@0 984 /**
michael@0 985 * Set all the values from another TextProperty instance into
michael@0 986 * this TextProperty instance.
michael@0 987 *
michael@0 988 * @param {TextProperty} aOther
michael@0 989 * The other TextProperty instance.
michael@0 990 */
michael@0 991 set: function(aOther) {
michael@0 992 let changed = false;
michael@0 993 for (let item of ["name", "value", "priority", "enabled"]) {
michael@0 994 if (this[item] != aOther[item]) {
michael@0 995 this[item] = aOther[item];
michael@0 996 changed = true;
michael@0 997 }
michael@0 998 }
michael@0 999
michael@0 1000 if (changed) {
michael@0 1001 this.updateEditor();
michael@0 1002 }
michael@0 1003 },
michael@0 1004
michael@0 1005 setValue: function(aValue, aPriority) {
michael@0 1006 this.rule.setPropertyValue(this, aValue, aPriority);
michael@0 1007 this.updateEditor();
michael@0 1008 },
michael@0 1009
michael@0 1010 setName: function(aName) {
michael@0 1011 this.rule.setPropertyName(this, aName);
michael@0 1012 this.updateEditor();
michael@0 1013 },
michael@0 1014
michael@0 1015 setEnabled: function(aValue) {
michael@0 1016 this.rule.setPropertyEnabled(this, aValue);
michael@0 1017 this.updateEditor();
michael@0 1018 },
michael@0 1019
michael@0 1020 remove: function() {
michael@0 1021 this.rule.removeProperty(this);
michael@0 1022 }
michael@0 1023 };
michael@0 1024
michael@0 1025
michael@0 1026 /**
michael@0 1027 * View hierarchy mostly follows the model hierarchy.
michael@0 1028 *
michael@0 1029 * CssRuleView:
michael@0 1030 * Owns an ElementStyle and creates a list of RuleEditors for its
michael@0 1031 * Rules.
michael@0 1032 * RuleEditor:
michael@0 1033 * Owns a Rule object and creates a list of TextPropertyEditors
michael@0 1034 * for its TextProperties.
michael@0 1035 * Manages creation of new text properties.
michael@0 1036 * TextPropertyEditor:
michael@0 1037 * Owns a TextProperty object.
michael@0 1038 * Manages changes to the TextProperty.
michael@0 1039 * Can be expanded to display computed properties.
michael@0 1040 * Can mark a property disabled or enabled.
michael@0 1041 */
michael@0 1042
michael@0 1043 /**
michael@0 1044 * CssRuleView is a view of the style rules and declarations that
michael@0 1045 * apply to a given element. After construction, the 'element'
michael@0 1046 * property will be available with the user interface.
michael@0 1047 *
michael@0 1048 * @param {Inspector} aInspector
michael@0 1049 * @param {Document} aDoc
michael@0 1050 * The document that will contain the rule view.
michael@0 1051 * @param {object} aStore
michael@0 1052 * The CSS rule view can use this object to store metadata
michael@0 1053 * that might outlast the rule view, particularly the current
michael@0 1054 * set of disabled properties.
michael@0 1055 * @param {PageStyleFront} aPageStyle
michael@0 1056 * The PageStyleFront for communicating with the remote server.
michael@0 1057 * @constructor
michael@0 1058 */
michael@0 1059 function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
michael@0 1060 this.inspector = aInspector;
michael@0 1061 this.doc = aDoc;
michael@0 1062 this.store = aStore || {};
michael@0 1063 this.pageStyle = aPageStyle;
michael@0 1064 this.element = this.doc.createElementNS(HTML_NS, "div");
michael@0 1065 this.element.className = "ruleview devtools-monospace";
michael@0 1066 this.element.flex = 1;
michael@0 1067
michael@0 1068 this._outputParser = new OutputParser();
michael@0 1069
michael@0 1070 this._buildContextMenu = this._buildContextMenu.bind(this);
michael@0 1071 this._contextMenuUpdate = this._contextMenuUpdate.bind(this);
michael@0 1072 this._onSelectAll = this._onSelectAll.bind(this);
michael@0 1073 this._onCopy = this._onCopy.bind(this);
michael@0 1074 this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
michael@0 1075
michael@0 1076 this.element.addEventListener("copy", this._onCopy);
michael@0 1077
michael@0 1078 this._handlePrefChange = this._handlePrefChange.bind(this);
michael@0 1079 gDevTools.on("pref-changed", this._handlePrefChange);
michael@0 1080
michael@0 1081 this._onSourcePrefChanged = this._onSourcePrefChanged.bind(this);
michael@0 1082 this._prefObserver = new PrefObserver("devtools.");
michael@0 1083 this._prefObserver.on(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
michael@0 1084
michael@0 1085 let options = {
michael@0 1086 autoSelect: true,
michael@0 1087 theme: "auto"
michael@0 1088 };
michael@0 1089 this.popup = new AutocompletePopup(aDoc.defaultView.parent.document, options);
michael@0 1090
michael@0 1091 // Create a tooltip for previewing things in the rule view (images for now)
michael@0 1092 this.previewTooltip = new Tooltip(this.inspector.panelDoc);
michael@0 1093 this.previewTooltip.startTogglingOnHover(this.element,
michael@0 1094 this._onTooltipTargetHover.bind(this));
michael@0 1095
michael@0 1096 // Also create a more complex tooltip for editing colors with the spectrum
michael@0 1097 // color picker
michael@0 1098 this.colorPicker = new SwatchColorPickerTooltip(this.inspector.panelDoc);
michael@0 1099
michael@0 1100 this._buildContextMenu();
michael@0 1101 this._showEmpty();
michael@0 1102 }
michael@0 1103
michael@0 1104 exports.CssRuleView = CssRuleView;
michael@0 1105
michael@0 1106 CssRuleView.prototype = {
michael@0 1107 // The element that we're inspecting.
michael@0 1108 _viewedElement: null,
michael@0 1109
michael@0 1110 /**
michael@0 1111 * Build the context menu.
michael@0 1112 */
michael@0 1113 _buildContextMenu: function() {
michael@0 1114 let doc = this.doc.defaultView.parent.document;
michael@0 1115
michael@0 1116 this._contextmenu = doc.createElementNS(XUL_NS, "menupopup");
michael@0 1117 this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
michael@0 1118 this._contextmenu.id = "rule-view-context-menu";
michael@0 1119
michael@0 1120 this.menuitemSelectAll = createMenuItem(this._contextmenu, {
michael@0 1121 label: "ruleView.contextmenu.selectAll",
michael@0 1122 accesskey: "ruleView.contextmenu.selectAll.accessKey",
michael@0 1123 command: this._onSelectAll
michael@0 1124 });
michael@0 1125 this.menuitemCopy = createMenuItem(this._contextmenu, {
michael@0 1126 label: "ruleView.contextmenu.copy",
michael@0 1127 accesskey: "ruleView.contextmenu.copy.accessKey",
michael@0 1128 command: this._onCopy
michael@0 1129 });
michael@0 1130 this.menuitemSources= createMenuItem(this._contextmenu, {
michael@0 1131 label: "ruleView.contextmenu.showOrigSources",
michael@0 1132 accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
michael@0 1133 command: this._onToggleOrigSources
michael@0 1134 });
michael@0 1135
michael@0 1136 let popupset = doc.documentElement.querySelector("popupset");
michael@0 1137 if (!popupset) {
michael@0 1138 popupset = doc.createElementNS(XUL_NS, "popupset");
michael@0 1139 doc.documentElement.appendChild(popupset);
michael@0 1140 }
michael@0 1141
michael@0 1142 popupset.appendChild(this._contextmenu);
michael@0 1143 },
michael@0 1144
michael@0 1145 /**
michael@0 1146 * Which type of hover-tooltip should be shown for the given element?
michael@0 1147 * This depends on the element: does it contain an image URL, a CSS transform,
michael@0 1148 * a font-family, ...
michael@0 1149 * @param {DOMNode} el The element to test
michael@0 1150 * @return {String} The type of hover-tooltip
michael@0 1151 */
michael@0 1152 _getHoverTooltipTypeForTarget: function(el) {
michael@0 1153 let prop = el.textProperty;
michael@0 1154
michael@0 1155 // Test for css transform
michael@0 1156 if (prop && prop.name === "transform") {
michael@0 1157 return "transform";
michael@0 1158 }
michael@0 1159
michael@0 1160 // Test for image
michael@0 1161 let isUrl = el.classList.contains("theme-link") &&
michael@0 1162 el.parentNode.classList.contains("ruleview-propertyvalue");
michael@0 1163 if (this.inspector.hasUrlToImageDataResolver && isUrl) {
michael@0 1164 return "image";
michael@0 1165 }
michael@0 1166
michael@0 1167 // Test for font-family
michael@0 1168 let propertyRoot = el.parentNode;
michael@0 1169 let propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname");
michael@0 1170 if (!propertyNameNode) {
michael@0 1171 propertyRoot = propertyRoot.parentNode;
michael@0 1172 propertyNameNode = propertyRoot.querySelector(".ruleview-propertyname");
michael@0 1173 }
michael@0 1174 let propertyName;
michael@0 1175 if (propertyNameNode) {
michael@0 1176 propertyName = propertyNameNode.textContent;
michael@0 1177 }
michael@0 1178 if (propertyName === "font-family" && el.classList.contains("ruleview-propertyvalue")) {
michael@0 1179 return "font";
michael@0 1180 }
michael@0 1181 },
michael@0 1182
michael@0 1183 /**
michael@0 1184 * Executed by the tooltip when the pointer hovers over an element of the view.
michael@0 1185 * Used to decide whether the tooltip should be shown or not and to actually
michael@0 1186 * put content in it.
michael@0 1187 * Checks if the hovered target is a css value we support tooltips for.
michael@0 1188 * @param {DOMNode} target
michael@0 1189 * @return {Boolean|Promise} Either a boolean or a promise, used by the
michael@0 1190 * Tooltip class to wait for the content to be put in the tooltip and finally
michael@0 1191 * decide whether or not the tooltip should be shown.
michael@0 1192 */
michael@0 1193 _onTooltipTargetHover: function(target) {
michael@0 1194 let tooltipType = this._getHoverTooltipTypeForTarget(target);
michael@0 1195 if (!tooltipType) {
michael@0 1196 return false;
michael@0 1197 }
michael@0 1198
michael@0 1199 if (this.colorPicker.tooltip.isShown()) {
michael@0 1200 this.colorPicker.revert();
michael@0 1201 this.colorPicker.hide();
michael@0 1202 }
michael@0 1203
michael@0 1204 if (tooltipType === "transform") {
michael@0 1205 return this.previewTooltip.setCssTransformContent(target.textProperty.value,
michael@0 1206 this.pageStyle, this._viewedElement);
michael@0 1207 }
michael@0 1208 if (tooltipType === "image") {
michael@0 1209 let prop = target.parentNode.textProperty;
michael@0 1210 let dim = Services.prefs.getIntPref("devtools.inspector.imagePreviewTooltipSize");
michael@0 1211 let uri = CssLogic.getBackgroundImageUriFromProperty(prop.value, prop.rule.domRule.href);
michael@0 1212 return this.previewTooltip.setRelativeImageContent(uri, this.inspector.inspector, dim);
michael@0 1213 }
michael@0 1214 if (tooltipType === "font") {
michael@0 1215 this.previewTooltip.setFontFamilyContent(target.textContent);
michael@0 1216 return true;
michael@0 1217 }
michael@0 1218
michael@0 1219 return false;
michael@0 1220 },
michael@0 1221
michael@0 1222 /**
michael@0 1223 * Update the context menu. This means enabling or disabling menuitems as
michael@0 1224 * appropriate.
michael@0 1225 */
michael@0 1226 _contextMenuUpdate: function() {
michael@0 1227 let win = this.doc.defaultView;
michael@0 1228
michael@0 1229 // Copy selection.
michael@0 1230 let selection = win.getSelection();
michael@0 1231 let copy;
michael@0 1232
michael@0 1233 if (selection.toString()) {
michael@0 1234 // Panel text selected
michael@0 1235 copy = true;
michael@0 1236 } else if (selection.anchorNode) {
michael@0 1237 // input type="text"
michael@0 1238 let { selectionStart, selectionEnd } = this.doc.popupNode;
michael@0 1239
michael@0 1240 if (isFinite(selectionStart) && isFinite(selectionEnd) &&
michael@0 1241 selectionStart !== selectionEnd) {
michael@0 1242 copy = true;
michael@0 1243 }
michael@0 1244 } else {
michael@0 1245 // No text selected, disable copy.
michael@0 1246 copy = false;
michael@0 1247 }
michael@0 1248
michael@0 1249 this.menuitemCopy.disabled = !copy;
michael@0 1250
michael@0 1251 let label = "ruleView.contextmenu.showOrigSources";
michael@0 1252 if (Services.prefs.getBoolPref(PREF_ORIG_SOURCES)) {
michael@0 1253 label = "ruleView.contextmenu.showCSSSources";
michael@0 1254 }
michael@0 1255 this.menuitemSources.setAttribute("label",
michael@0 1256 _strings.GetStringFromName(label));
michael@0 1257
michael@0 1258 let accessKey = label + ".accessKey";
michael@0 1259 this.menuitemSources.setAttribute("accesskey",
michael@0 1260 _strings.GetStringFromName(accessKey));
michael@0 1261 },
michael@0 1262
michael@0 1263 /**
michael@0 1264 * Select all text.
michael@0 1265 */
michael@0 1266 _onSelectAll: function() {
michael@0 1267 let win = this.doc.defaultView;
michael@0 1268 let selection = win.getSelection();
michael@0 1269
michael@0 1270 selection.selectAllChildren(this.doc.documentElement);
michael@0 1271 },
michael@0 1272
michael@0 1273 /**
michael@0 1274 * Copy selected text from the rule view.
michael@0 1275 *
michael@0 1276 * @param {Event} event
michael@0 1277 * The event object.
michael@0 1278 */
michael@0 1279 _onCopy: function(event) {
michael@0 1280 try {
michael@0 1281 let target = event.target;
michael@0 1282 let text;
michael@0 1283
michael@0 1284 if (event.target.nodeName === "menuitem") {
michael@0 1285 target = this.doc.popupNode;
michael@0 1286 }
michael@0 1287
michael@0 1288 if (target.nodeName == "input") {
michael@0 1289 let start = Math.min(target.selectionStart, target.selectionEnd);
michael@0 1290 let end = Math.max(target.selectionStart, target.selectionEnd);
michael@0 1291 let count = end - start;
michael@0 1292 text = target.value.substr(start, count);
michael@0 1293 } else {
michael@0 1294 let win = this.doc.defaultView;
michael@0 1295 let selection = win.getSelection();
michael@0 1296
michael@0 1297 text = selection.toString();
michael@0 1298
michael@0 1299 // Remove any double newlines.
michael@0 1300 text = text.replace(/(\r?\n)\r?\n/g, "$1");
michael@0 1301
michael@0 1302 // Remove "inline"
michael@0 1303 let inline = _strings.GetStringFromName("rule.sourceInline");
michael@0 1304 let rx = new RegExp("^" + inline + "\\r?\\n?", "g");
michael@0 1305 text = text.replace(rx, "");
michael@0 1306 }
michael@0 1307
michael@0 1308 clipboardHelper.copyString(text, this.doc);
michael@0 1309 event.preventDefault();
michael@0 1310 } catch(e) {
michael@0 1311 console.error(e);
michael@0 1312 }
michael@0 1313 },
michael@0 1314
michael@0 1315 /**
michael@0 1316 * Toggle the original sources pref.
michael@0 1317 */
michael@0 1318 _onToggleOrigSources: function() {
michael@0 1319 let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
michael@0 1320 Services.prefs.setBoolPref(PREF_ORIG_SOURCES, !isEnabled);
michael@0 1321 },
michael@0 1322
michael@0 1323 setPageStyle: function(aPageStyle) {
michael@0 1324 this.pageStyle = aPageStyle;
michael@0 1325 },
michael@0 1326
michael@0 1327 /**
michael@0 1328 * Return {bool} true if the rule view currently has an input editor visible.
michael@0 1329 */
michael@0 1330 get isEditing() {
michael@0 1331 return this.element.querySelectorAll(".styleinspector-propertyeditor").length > 0
michael@0 1332 || this.colorPicker.tooltip.isShown();
michael@0 1333 },
michael@0 1334
michael@0 1335 _handlePrefChange: function(event, data) {
michael@0 1336 if (data.pref == "devtools.defaultColorUnit") {
michael@0 1337 let element = this._viewedElement;
michael@0 1338 this._viewedElement = null;
michael@0 1339 this.highlight(element);
michael@0 1340 }
michael@0 1341 },
michael@0 1342
michael@0 1343 _onSourcePrefChanged: function() {
michael@0 1344 if (this.menuitemSources) {
michael@0 1345 let isEnabled = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
michael@0 1346 this.menuitemSources.setAttribute("checked", isEnabled);
michael@0 1347 }
michael@0 1348
michael@0 1349 // update text of source links
michael@0 1350 for (let rule of this._elementStyle.rules) {
michael@0 1351 if (rule.editor) {
michael@0 1352 rule.editor.updateSourceLink();
michael@0 1353 }
michael@0 1354 }
michael@0 1355 },
michael@0 1356
michael@0 1357 destroy: function() {
michael@0 1358 this.clear();
michael@0 1359
michael@0 1360 gDummyPromise = null;
michael@0 1361 gDevTools.off("pref-changed", this._handlePrefChange);
michael@0 1362
michael@0 1363 this._prefObserver.off(PREF_ORIG_SOURCES, this._onSourcePrefChanged);
michael@0 1364 this._prefObserver.destroy();
michael@0 1365
michael@0 1366 this.element.removeEventListener("copy", this._onCopy);
michael@0 1367 delete this._onCopy;
michael@0 1368
michael@0 1369 delete this._outputParser;
michael@0 1370
michael@0 1371 // Remove context menu
michael@0 1372 if (this._contextmenu) {
michael@0 1373 // Destroy the Select All menuitem.
michael@0 1374 this.menuitemSelectAll.removeEventListener("command", this._onSelectAll);
michael@0 1375 this.menuitemSelectAll = null;
michael@0 1376
michael@0 1377 // Destroy the Copy menuitem.
michael@0 1378 this.menuitemCopy.removeEventListener("command", this._onCopy);
michael@0 1379 this.menuitemCopy = null;
michael@0 1380
michael@0 1381 this.menuitemSources.removeEventListener("command", this._onToggleOrigSources);
michael@0 1382 this.menuitemSources = null;
michael@0 1383
michael@0 1384 // Destroy the context menu.
michael@0 1385 this._contextmenu.removeEventListener("popupshowing", this._contextMenuUpdate);
michael@0 1386 this._contextmenu.parentNode.removeChild(this._contextmenu);
michael@0 1387 this._contextmenu = null;
michael@0 1388 }
michael@0 1389
michael@0 1390 // We manage the popupNode ourselves so we also need to destroy it.
michael@0 1391 this.doc.popupNode = null;
michael@0 1392
michael@0 1393 this.previewTooltip.stopTogglingOnHover(this.element);
michael@0 1394 this.previewTooltip.destroy();
michael@0 1395 this.colorPicker.destroy();
michael@0 1396
michael@0 1397 if (this.element.parentNode) {
michael@0 1398 this.element.parentNode.removeChild(this.element);
michael@0 1399 }
michael@0 1400
michael@0 1401 if (this.elementStyle) {
michael@0 1402 this.elementStyle.destroy();
michael@0 1403 }
michael@0 1404
michael@0 1405 this.popup.destroy();
michael@0 1406 },
michael@0 1407
michael@0 1408 /**
michael@0 1409 * Update the highlighted element.
michael@0 1410 *
michael@0 1411 * @param {NodeActor} aElement
michael@0 1412 * The node whose style rules we'll inspect.
michael@0 1413 */
michael@0 1414 highlight: function(aElement) {
michael@0 1415 if (this._viewedElement === aElement) {
michael@0 1416 return promise.resolve(undefined);
michael@0 1417 }
michael@0 1418
michael@0 1419 this.clear();
michael@0 1420
michael@0 1421 if (this._elementStyle) {
michael@0 1422 delete this._elementStyle;
michael@0 1423 }
michael@0 1424
michael@0 1425 this._viewedElement = aElement;
michael@0 1426 if (!this._viewedElement) {
michael@0 1427 this._showEmpty();
michael@0 1428 return promise.resolve(undefined);
michael@0 1429 }
michael@0 1430
michael@0 1431 this._elementStyle = new ElementStyle(aElement, this.store, this.pageStyle);
michael@0 1432 return this._elementStyle.init().then(() => {
michael@0 1433 return this._populate();
michael@0 1434 }).then(() => {
michael@0 1435 // A new node may already be selected, in which this._elementStyle will
michael@0 1436 // be null.
michael@0 1437 if (this._elementStyle) {
michael@0 1438 this._elementStyle.onChanged = () => {
michael@0 1439 this._changed();
michael@0 1440 };
michael@0 1441 }
michael@0 1442 }).then(null, console.error);
michael@0 1443 },
michael@0 1444
michael@0 1445 /**
michael@0 1446 * Update the rules for the currently highlighted element.
michael@0 1447 */
michael@0 1448 nodeChanged: function() {
michael@0 1449 // Ignore refreshes during editing or when no element is selected.
michael@0 1450 if (this.isEditing || !this._elementStyle) {
michael@0 1451 return;
michael@0 1452 }
michael@0 1453
michael@0 1454 this._clearRules();
michael@0 1455
michael@0 1456 // Repopulate the element style.
michael@0 1457 this._populate();
michael@0 1458 },
michael@0 1459
michael@0 1460 _populate: function() {
michael@0 1461 let elementStyle = this._elementStyle;
michael@0 1462 return this._elementStyle.populate().then(() => {
michael@0 1463 if (this._elementStyle != elementStyle) {
michael@0 1464 return;
michael@0 1465 }
michael@0 1466 this._createEditors();
michael@0 1467
michael@0 1468 // Notify anyone that cares that we refreshed.
michael@0 1469 var evt = this.doc.createEvent("Events");
michael@0 1470 evt.initEvent("CssRuleViewRefreshed", true, false);
michael@0 1471 this.element.dispatchEvent(evt);
michael@0 1472 return undefined;
michael@0 1473 }).then(null, promiseWarn);
michael@0 1474 },
michael@0 1475
michael@0 1476 /**
michael@0 1477 * Show the user that the rule view has no node selected.
michael@0 1478 */
michael@0 1479 _showEmpty: function() {
michael@0 1480 if (this.doc.getElementById("noResults") > 0) {
michael@0 1481 return;
michael@0 1482 }
michael@0 1483
michael@0 1484 createChild(this.element, "div", {
michael@0 1485 id: "noResults",
michael@0 1486 textContent: CssLogic.l10n("rule.empty")
michael@0 1487 });
michael@0 1488 },
michael@0 1489
michael@0 1490 /**
michael@0 1491 * Clear the rules.
michael@0 1492 */
michael@0 1493 _clearRules: function() {
michael@0 1494 while (this.element.hasChildNodes()) {
michael@0 1495 this.element.removeChild(this.element.lastChild);
michael@0 1496 }
michael@0 1497 },
michael@0 1498
michael@0 1499 /**
michael@0 1500 * Clear the rule view.
michael@0 1501 */
michael@0 1502 clear: function() {
michael@0 1503 this._clearRules();
michael@0 1504 this._viewedElement = null;
michael@0 1505 this._elementStyle = null;
michael@0 1506
michael@0 1507 this.previewTooltip.hide();
michael@0 1508 this.colorPicker.hide();
michael@0 1509 },
michael@0 1510
michael@0 1511 /**
michael@0 1512 * Called when the user has made changes to the ElementStyle.
michael@0 1513 * Emits an event that clients can listen to.
michael@0 1514 */
michael@0 1515 _changed: function() {
michael@0 1516 var evt = this.doc.createEvent("Events");
michael@0 1517 evt.initEvent("CssRuleViewChanged", true, false);
michael@0 1518 this.element.dispatchEvent(evt);
michael@0 1519 },
michael@0 1520
michael@0 1521 /**
michael@0 1522 * Text for header that shows above rules for this element
michael@0 1523 */
michael@0 1524 get selectedElementLabel() {
michael@0 1525 if (this._selectedElementLabel) {
michael@0 1526 return this._selectedElementLabel;
michael@0 1527 }
michael@0 1528 this._selectedElementLabel = CssLogic.l10n("rule.selectedElement");
michael@0 1529 return this._selectedElementLabel;
michael@0 1530 },
michael@0 1531
michael@0 1532 /**
michael@0 1533 * Text for header that shows above rules for pseudo elements
michael@0 1534 */
michael@0 1535 get pseudoElementLabel() {
michael@0 1536 if (this._pseudoElementLabel) {
michael@0 1537 return this._pseudoElementLabel;
michael@0 1538 }
michael@0 1539 this._pseudoElementLabel = CssLogic.l10n("rule.pseudoElement");
michael@0 1540 return this._pseudoElementLabel;
michael@0 1541 },
michael@0 1542
michael@0 1543 togglePseudoElementVisibility: function(value) {
michael@0 1544 this._showPseudoElements = !!value;
michael@0 1545 let isOpen = this.showPseudoElements;
michael@0 1546
michael@0 1547 Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
michael@0 1548 isOpen);
michael@0 1549
michael@0 1550 this.element.classList.toggle("show-pseudo-elements", isOpen);
michael@0 1551
michael@0 1552 if (this.pseudoElementTwisty) {
michael@0 1553 if (isOpen) {
michael@0 1554 this.pseudoElementTwisty.setAttribute("open", "true");
michael@0 1555 }
michael@0 1556 else {
michael@0 1557 this.pseudoElementTwisty.removeAttribute("open");
michael@0 1558 }
michael@0 1559 }
michael@0 1560 },
michael@0 1561
michael@0 1562 get showPseudoElements() {
michael@0 1563 if (this._showPseudoElements === undefined) {
michael@0 1564 this._showPseudoElements =
michael@0 1565 Services.prefs.getBoolPref("devtools.inspector.show_pseudo_elements");
michael@0 1566 }
michael@0 1567 return this._showPseudoElements;
michael@0 1568 },
michael@0 1569
michael@0 1570 _getRuleViewHeaderClassName: function(isPseudo) {
michael@0 1571 let baseClassName = "theme-gutter ruleview-header";
michael@0 1572 return isPseudo ? baseClassName + " ruleview-expandable-header" : baseClassName;
michael@0 1573 },
michael@0 1574
michael@0 1575 /**
michael@0 1576 * Creates editor UI for each of the rules in _elementStyle.
michael@0 1577 */
michael@0 1578 _createEditors: function() {
michael@0 1579 // Run through the current list of rules, attaching
michael@0 1580 // their editors in order. Create editors if needed.
michael@0 1581 let lastInheritedSource = "";
michael@0 1582 let seenPseudoElement = false;
michael@0 1583 let seenNormalElement = false;
michael@0 1584
michael@0 1585 for (let rule of this._elementStyle.rules) {
michael@0 1586 if (rule.domRule.system) {
michael@0 1587 continue;
michael@0 1588 }
michael@0 1589
michael@0 1590 // Only print header for this element if there are pseudo elements
michael@0 1591 if (seenPseudoElement && !seenNormalElement && !rule.pseudoElement) {
michael@0 1592 seenNormalElement = true;
michael@0 1593 let div = this.doc.createElementNS(HTML_NS, "div");
michael@0 1594 div.className = this._getRuleViewHeaderClassName();
michael@0 1595 div.textContent = this.selectedElementLabel;
michael@0 1596 this.element.appendChild(div);
michael@0 1597 }
michael@0 1598
michael@0 1599 let inheritedSource = rule.inheritedSource;
michael@0 1600 if (inheritedSource != lastInheritedSource) {
michael@0 1601 let div = this.doc.createElementNS(HTML_NS, "div");
michael@0 1602 div.className = this._getRuleViewHeaderClassName();
michael@0 1603 div.textContent = inheritedSource;
michael@0 1604 lastInheritedSource = inheritedSource;
michael@0 1605 this.element.appendChild(div);
michael@0 1606 }
michael@0 1607
michael@0 1608 if (!seenPseudoElement && rule.pseudoElement) {
michael@0 1609 seenPseudoElement = true;
michael@0 1610
michael@0 1611 let div = this.doc.createElementNS(HTML_NS, "div");
michael@0 1612 div.className = this._getRuleViewHeaderClassName(true);
michael@0 1613 div.textContent = this.pseudoElementLabel;
michael@0 1614 div.addEventListener("dblclick", () => {
michael@0 1615 this.togglePseudoElementVisibility(!this.showPseudoElements);
michael@0 1616 }, false);
michael@0 1617
michael@0 1618 let twisty = this.pseudoElementTwisty =
michael@0 1619 this.doc.createElementNS(HTML_NS, "span");
michael@0 1620 twisty.className = "ruleview-expander theme-twisty";
michael@0 1621 twisty.addEventListener("click", () => {
michael@0 1622 this.togglePseudoElementVisibility(!this.showPseudoElements);
michael@0 1623 }, false);
michael@0 1624
michael@0 1625 div.insertBefore(twisty, div.firstChild);
michael@0 1626 this.element.appendChild(div);
michael@0 1627 }
michael@0 1628
michael@0 1629 if (!rule.editor) {
michael@0 1630 rule.editor = new RuleEditor(this, rule);
michael@0 1631 }
michael@0 1632
michael@0 1633 this.element.appendChild(rule.editor.element);
michael@0 1634 }
michael@0 1635
michael@0 1636 this.togglePseudoElementVisibility(this.showPseudoElements);
michael@0 1637 }
michael@0 1638 };
michael@0 1639
michael@0 1640 /**
michael@0 1641 * Create a RuleEditor.
michael@0 1642 *
michael@0 1643 * @param {CssRuleView} aRuleView
michael@0 1644 * The CssRuleView containg the document holding this rule editor.
michael@0 1645 * @param {Rule} aRule
michael@0 1646 * The Rule object we're editing.
michael@0 1647 * @constructor
michael@0 1648 */
michael@0 1649 function RuleEditor(aRuleView, aRule) {
michael@0 1650 this.ruleView = aRuleView;
michael@0 1651 this.doc = this.ruleView.doc;
michael@0 1652 this.rule = aRule;
michael@0 1653
michael@0 1654 this._onNewProperty = this._onNewProperty.bind(this);
michael@0 1655 this._newPropertyDestroy = this._newPropertyDestroy.bind(this);
michael@0 1656
michael@0 1657 this._create();
michael@0 1658 }
michael@0 1659
michael@0 1660 RuleEditor.prototype = {
michael@0 1661 _create: function() {
michael@0 1662 this.element = this.doc.createElementNS(HTML_NS, "div");
michael@0 1663 this.element.className = "ruleview-rule theme-separator";
michael@0 1664 this.element._ruleEditor = this;
michael@0 1665 if (this.rule.pseudoElement) {
michael@0 1666 this.element.classList.add("ruleview-rule-pseudo-element");
michael@0 1667 }
michael@0 1668
michael@0 1669 // Give a relative position for the inplace editor's measurement
michael@0 1670 // span to be placed absolutely against.
michael@0 1671 this.element.style.position = "relative";
michael@0 1672
michael@0 1673 // Add the source link.
michael@0 1674 let source = createChild(this.element, "div", {
michael@0 1675 class: "ruleview-rule-source theme-link"
michael@0 1676 });
michael@0 1677 source.addEventListener("click", function() {
michael@0 1678 let rule = this.rule.domRule;
michael@0 1679 let evt = this.doc.createEvent("CustomEvent");
michael@0 1680 evt.initCustomEvent("CssRuleViewCSSLinkClicked", true, false, {
michael@0 1681 rule: rule,
michael@0 1682 });
michael@0 1683 this.element.dispatchEvent(evt);
michael@0 1684 }.bind(this));
michael@0 1685 let sourceLabel = this.doc.createElementNS(XUL_NS, "label");
michael@0 1686 sourceLabel.setAttribute("crop", "center");
michael@0 1687 sourceLabel.classList.add("source-link-label");
michael@0 1688 source.appendChild(sourceLabel);
michael@0 1689
michael@0 1690 this.updateSourceLink();
michael@0 1691
michael@0 1692 let code = createChild(this.element, "div", {
michael@0 1693 class: "ruleview-code"
michael@0 1694 });
michael@0 1695
michael@0 1696 let header = createChild(code, "div", {});
michael@0 1697
michael@0 1698 this.selectorText = createChild(header, "span", {
michael@0 1699 class: "ruleview-selector theme-fg-color3"
michael@0 1700 });
michael@0 1701
michael@0 1702 this.openBrace = createChild(header, "span", {
michael@0 1703 class: "ruleview-ruleopen",
michael@0 1704 textContent: " {"
michael@0 1705 });
michael@0 1706
michael@0 1707 code.addEventListener("click", function() {
michael@0 1708 let selection = this.doc.defaultView.getSelection();
michael@0 1709 if (selection.isCollapsed) {
michael@0 1710 this.newProperty();
michael@0 1711 }
michael@0 1712 }.bind(this), false);
michael@0 1713
michael@0 1714 this.element.addEventListener("mousedown", function() {
michael@0 1715 this.doc.defaultView.focus();
michael@0 1716 }.bind(this), false);
michael@0 1717
michael@0 1718 this.element.addEventListener("contextmenu", event => {
michael@0 1719 try {
michael@0 1720 // In the sidebar we do not have this.doc.popupNode so we need to save
michael@0 1721 // the node ourselves.
michael@0 1722 this.doc.popupNode = event.explicitOriginalTarget;
michael@0 1723 let win = this.doc.defaultView;
michael@0 1724 win.focus();
michael@0 1725
michael@0 1726 this.ruleView._contextmenu.openPopupAtScreen(
michael@0 1727 event.screenX, event.screenY, true);
michael@0 1728
michael@0 1729 } catch(e) {
michael@0 1730 console.error(e);
michael@0 1731 }
michael@0 1732 }, false);
michael@0 1733
michael@0 1734 this.propertyList = createChild(code, "ul", {
michael@0 1735 class: "ruleview-propertylist"
michael@0 1736 });
michael@0 1737
michael@0 1738 this.populate();
michael@0 1739
michael@0 1740 this.closeBrace = createChild(code, "div", {
michael@0 1741 class: "ruleview-ruleclose",
michael@0 1742 tabindex: "0",
michael@0 1743 textContent: "}"
michael@0 1744 });
michael@0 1745
michael@0 1746 // Create a property editor when the close brace is clicked.
michael@0 1747 editableItem({ element: this.closeBrace }, (aElement) => {
michael@0 1748 this.newProperty();
michael@0 1749 });
michael@0 1750 },
michael@0 1751
michael@0 1752 updateSourceLink: function RuleEditor_updateSourceLink()
michael@0 1753 {
michael@0 1754 let sourceLabel = this.element.querySelector(".source-link-label");
michael@0 1755 sourceLabel.setAttribute("value", this.rule.title);
michael@0 1756
michael@0 1757 let sourceHref = (this.rule.sheet && this.rule.sheet.href) ?
michael@0 1758 this.rule.sheet.href : this.rule.title;
michael@0 1759
michael@0 1760 sourceLabel.setAttribute("tooltiptext", sourceHref);
michael@0 1761
michael@0 1762 let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
michael@0 1763 if (showOrig && this.rule.domRule.type != ELEMENT_STYLE) {
michael@0 1764 this.rule.getOriginalSourceStrings().then((strings) => {
michael@0 1765 sourceLabel.setAttribute("value", strings.short);
michael@0 1766 sourceLabel.setAttribute("tooltiptext", strings.full);
michael@0 1767 })
michael@0 1768 }
michael@0 1769 },
michael@0 1770
michael@0 1771 /**
michael@0 1772 * Update the rule editor with the contents of the rule.
michael@0 1773 */
michael@0 1774 populate: function() {
michael@0 1775 // Clear out existing viewers.
michael@0 1776 while (this.selectorText.hasChildNodes()) {
michael@0 1777 this.selectorText.removeChild(this.selectorText.lastChild);
michael@0 1778 }
michael@0 1779
michael@0 1780 // If selector text comes from a css rule, highlight selectors that
michael@0 1781 // actually match. For custom selector text (such as for the 'element'
michael@0 1782 // style, just show the text directly.
michael@0 1783 if (this.rule.domRule.type === ELEMENT_STYLE) {
michael@0 1784 this.selectorText.textContent = this.rule.selectorText;
michael@0 1785 } else {
michael@0 1786 this.rule.domRule.selectors.forEach((selector, i) => {
michael@0 1787 if (i != 0) {
michael@0 1788 createChild(this.selectorText, "span", {
michael@0 1789 class: "ruleview-selector-separator",
michael@0 1790 textContent: ", "
michael@0 1791 });
michael@0 1792 }
michael@0 1793 let cls;
michael@0 1794 if (this.rule.matchedSelectors.indexOf(selector) > -1) {
michael@0 1795 cls = "ruleview-selector-matched";
michael@0 1796 } else {
michael@0 1797 cls = "ruleview-selector-unmatched";
michael@0 1798 }
michael@0 1799 createChild(this.selectorText, "span", {
michael@0 1800 class: cls,
michael@0 1801 textContent: selector
michael@0 1802 });
michael@0 1803 });
michael@0 1804 }
michael@0 1805
michael@0 1806 for (let prop of this.rule.textProps) {
michael@0 1807 if (!prop.editor) {
michael@0 1808 let editor = new TextPropertyEditor(this, prop);
michael@0 1809 this.propertyList.appendChild(editor.element);
michael@0 1810 }
michael@0 1811 }
michael@0 1812 },
michael@0 1813
michael@0 1814 /**
michael@0 1815 * Programatically add a new property to the rule.
michael@0 1816 *
michael@0 1817 * @param {string} aName
michael@0 1818 * Property name.
michael@0 1819 * @param {string} aValue
michael@0 1820 * Property value.
michael@0 1821 * @param {string} aPriority
michael@0 1822 * Property priority.
michael@0 1823 * @param {TextProperty} aSiblingProp
michael@0 1824 * Optional, property next to which the new property will be added.
michael@0 1825 * @return {TextProperty}
michael@0 1826 * The new property
michael@0 1827 */
michael@0 1828 addProperty: function(aName, aValue, aPriority, aSiblingProp) {
michael@0 1829 let prop = this.rule.createProperty(aName, aValue, aPriority, aSiblingProp);
michael@0 1830 let index = this.rule.textProps.indexOf(prop);
michael@0 1831 let editor = new TextPropertyEditor(this, prop);
michael@0 1832
michael@0 1833 // Insert this node before the DOM node that is currently at its new index
michael@0 1834 // in the property list. There is currently one less node in the DOM than
michael@0 1835 // in the property list, so this causes it to appear after aSiblingProp.
michael@0 1836 // If there is no node at its index, as is the case where this is the last
michael@0 1837 // node being inserted, then this behaves as appendChild.
michael@0 1838 this.propertyList.insertBefore(editor.element,
michael@0 1839 this.propertyList.children[index]);
michael@0 1840
michael@0 1841 return prop;
michael@0 1842 },
michael@0 1843
michael@0 1844 /**
michael@0 1845 * Programatically add a list of new properties to the rule. Focus the UI
michael@0 1846 * to the proper location after adding (either focus the value on the
michael@0 1847 * last property if it is empty, or create a new property and focus it).
michael@0 1848 *
michael@0 1849 * @param {Array} aProperties
michael@0 1850 * Array of properties, which are objects with this signature:
michael@0 1851 * {
michael@0 1852 * name: {string},
michael@0 1853 * value: {string},
michael@0 1854 * priority: {string}
michael@0 1855 * }
michael@0 1856 * @param {TextProperty} aSiblingProp
michael@0 1857 * Optional, the property next to which all new props should be added.
michael@0 1858 */
michael@0 1859 addProperties: function(aProperties, aSiblingProp) {
michael@0 1860 if (!aProperties || !aProperties.length) {
michael@0 1861 return;
michael@0 1862 }
michael@0 1863
michael@0 1864 let lastProp = aSiblingProp;
michael@0 1865 for (let p of aProperties) {
michael@0 1866 lastProp = this.addProperty(p.name, p.value, p.priority, lastProp);
michael@0 1867 }
michael@0 1868
michael@0 1869 // Either focus on the last value if incomplete, or start a new one.
michael@0 1870 if (lastProp && lastProp.value.trim() === "") {
michael@0 1871 lastProp.editor.valueSpan.click();
michael@0 1872 } else {
michael@0 1873 this.newProperty();
michael@0 1874 }
michael@0 1875 },
michael@0 1876
michael@0 1877 /**
michael@0 1878 * Create a text input for a property name. If a non-empty property
michael@0 1879 * name is given, we'll create a real TextProperty and add it to the
michael@0 1880 * rule.
michael@0 1881 */
michael@0 1882 newProperty: function() {
michael@0 1883 // If we're already creating a new property, ignore this.
michael@0 1884 if (!this.closeBrace.hasAttribute("tabindex")) {
michael@0 1885 return;
michael@0 1886 }
michael@0 1887
michael@0 1888 // While we're editing a new property, it doesn't make sense to
michael@0 1889 // start a second new property editor, so disable focusing the
michael@0 1890 // close brace for now.
michael@0 1891 this.closeBrace.removeAttribute("tabindex");
michael@0 1892
michael@0 1893 this.newPropItem = createChild(this.propertyList, "li", {
michael@0 1894 class: "ruleview-property ruleview-newproperty",
michael@0 1895 });
michael@0 1896
michael@0 1897 this.newPropSpan = createChild(this.newPropItem, "span", {
michael@0 1898 class: "ruleview-propertyname",
michael@0 1899 tabindex: "0"
michael@0 1900 });
michael@0 1901
michael@0 1902 this.multipleAddedProperties = null;
michael@0 1903
michael@0 1904 this.editor = new InplaceEditor({
michael@0 1905 element: this.newPropSpan,
michael@0 1906 done: this._onNewProperty,
michael@0 1907 destroy: this._newPropertyDestroy,
michael@0 1908 advanceChars: ":",
michael@0 1909 contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
michael@0 1910 popup: this.ruleView.popup
michael@0 1911 });
michael@0 1912
michael@0 1913 // Auto-close the input if multiple rules get pasted into new property.
michael@0 1914 this.editor.input.addEventListener("paste",
michael@0 1915 blurOnMultipleProperties, false);
michael@0 1916 },
michael@0 1917
michael@0 1918 /**
michael@0 1919 * Called when the new property input has been dismissed.
michael@0 1920 *
michael@0 1921 * @param {string} aValue
michael@0 1922 * The value in the editor.
michael@0 1923 * @param {bool} aCommit
michael@0 1924 * True if the value should be committed.
michael@0 1925 */
michael@0 1926 _onNewProperty: function(aValue, aCommit) {
michael@0 1927 if (!aValue || !aCommit) {
michael@0 1928 return;
michael@0 1929 }
michael@0 1930
michael@0 1931 // parseDeclarations allows for name-less declarations, but in the present
michael@0 1932 // case, we're creating a new declaration, it doesn't make sense to accept
michael@0 1933 // these entries
michael@0 1934 this.multipleAddedProperties = parseDeclarations(aValue).filter(d => d.name);
michael@0 1935
michael@0 1936 // Blur the editor field now and deal with adding declarations later when
michael@0 1937 // the field gets destroyed (see _newPropertyDestroy)
michael@0 1938 this.editor.input.blur();
michael@0 1939 },
michael@0 1940
michael@0 1941 /**
michael@0 1942 * Called when the new property editor is destroyed.
michael@0 1943 * This is where the properties (type TextProperty) are actually being
michael@0 1944 * added, since we want to wait until after the inplace editor `destroy`
michael@0 1945 * event has been fired to keep consistent UI state.
michael@0 1946 */
michael@0 1947 _newPropertyDestroy: function() {
michael@0 1948 // We're done, make the close brace focusable again.
michael@0 1949 this.closeBrace.setAttribute("tabindex", "0");
michael@0 1950
michael@0 1951 this.propertyList.removeChild(this.newPropItem);
michael@0 1952 delete this.newPropItem;
michael@0 1953 delete this.newPropSpan;
michael@0 1954
michael@0 1955 // If properties were added, we want to focus the proper element.
michael@0 1956 // If the last new property has no value, focus the value on it.
michael@0 1957 // Otherwise, start a new property and focus that field.
michael@0 1958 if (this.multipleAddedProperties && this.multipleAddedProperties.length) {
michael@0 1959 this.addProperties(this.multipleAddedProperties);
michael@0 1960 }
michael@0 1961 }
michael@0 1962 };
michael@0 1963
michael@0 1964 /**
michael@0 1965 * Create a TextPropertyEditor.
michael@0 1966 *
michael@0 1967 * @param {RuleEditor} aRuleEditor
michael@0 1968 * The rule editor that owns this TextPropertyEditor.
michael@0 1969 * @param {TextProperty} aProperty
michael@0 1970 * The text property to edit.
michael@0 1971 * @constructor
michael@0 1972 */
michael@0 1973 function TextPropertyEditor(aRuleEditor, aProperty) {
michael@0 1974 this.ruleEditor = aRuleEditor;
michael@0 1975 this.doc = this.ruleEditor.doc;
michael@0 1976 this.popup = this.ruleEditor.ruleView.popup;
michael@0 1977 this.prop = aProperty;
michael@0 1978 this.prop.editor = this;
michael@0 1979 this.browserWindow = this.doc.defaultView.top;
michael@0 1980 this.removeOnRevert = this.prop.value === "";
michael@0 1981
michael@0 1982 this._onEnableClicked = this._onEnableClicked.bind(this);
michael@0 1983 this._onExpandClicked = this._onExpandClicked.bind(this);
michael@0 1984 this._onStartEditing = this._onStartEditing.bind(this);
michael@0 1985 this._onNameDone = this._onNameDone.bind(this);
michael@0 1986 this._onValueDone = this._onValueDone.bind(this);
michael@0 1987 this._onValidate = throttle(this._previewValue, 10, this);
michael@0 1988 this.update = this.update.bind(this);
michael@0 1989
michael@0 1990 this._create();
michael@0 1991 this.update();
michael@0 1992 }
michael@0 1993
michael@0 1994 TextPropertyEditor.prototype = {
michael@0 1995 /**
michael@0 1996 * Boolean indicating if the name or value is being currently edited.
michael@0 1997 */
michael@0 1998 get editing() {
michael@0 1999 return !!(this.nameSpan.inplaceEditor || this.valueSpan.inplaceEditor ||
michael@0 2000 this.ruleEditor.ruleView.colorPicker.tooltip.isShown() ||
michael@0 2001 this.ruleEditor.ruleView.colorPicker.eyedropperOpen) ||
michael@0 2002 this.popup.isOpen;
michael@0 2003 },
michael@0 2004
michael@0 2005 /**
michael@0 2006 * Create the property editor's DOM.
michael@0 2007 */
michael@0 2008 _create: function() {
michael@0 2009 this.element = this.doc.createElementNS(HTML_NS, "li");
michael@0 2010 this.element.classList.add("ruleview-property");
michael@0 2011
michael@0 2012 // The enable checkbox will disable or enable the rule.
michael@0 2013 this.enable = createChild(this.element, "div", {
michael@0 2014 class: "ruleview-enableproperty theme-checkbox",
michael@0 2015 tabindex: "-1"
michael@0 2016 });
michael@0 2017 this.enable.addEventListener("click", this._onEnableClicked, true);
michael@0 2018
michael@0 2019 // Click to expand the computed properties of the text property.
michael@0 2020 this.expander = createChild(this.element, "span", {
michael@0 2021 class: "ruleview-expander theme-twisty"
michael@0 2022 });
michael@0 2023 this.expander.addEventListener("click", this._onExpandClicked, true);
michael@0 2024
michael@0 2025 this.nameContainer = createChild(this.element, "span", {
michael@0 2026 class: "ruleview-namecontainer"
michael@0 2027 });
michael@0 2028 this.nameContainer.addEventListener("click", (aEvent) => {
michael@0 2029 // Clicks within the name shouldn't propagate any further.
michael@0 2030 aEvent.stopPropagation();
michael@0 2031 if (aEvent.target === propertyContainer) {
michael@0 2032 this.nameSpan.click();
michael@0 2033 }
michael@0 2034 }, false);
michael@0 2035
michael@0 2036 // Property name, editable when focused. Property name
michael@0 2037 // is committed when the editor is unfocused.
michael@0 2038 this.nameSpan = createChild(this.nameContainer, "span", {
michael@0 2039 class: "ruleview-propertyname theme-fg-color5",
michael@0 2040 tabindex: "0",
michael@0 2041 });
michael@0 2042
michael@0 2043 editableField({
michael@0 2044 start: this._onStartEditing,
michael@0 2045 element: this.nameSpan,
michael@0 2046 done: this._onNameDone,
michael@0 2047 destroy: this.update,
michael@0 2048 advanceChars: ':',
michael@0 2049 contentType: InplaceEditor.CONTENT_TYPES.CSS_PROPERTY,
michael@0 2050 popup: this.popup
michael@0 2051 });
michael@0 2052
michael@0 2053 // Auto blur name field on multiple CSS rules get pasted in.
michael@0 2054 this.nameContainer.addEventListener("paste",
michael@0 2055 blurOnMultipleProperties, false);
michael@0 2056
michael@0 2057 appendText(this.nameContainer, ": ");
michael@0 2058
michael@0 2059 // Create a span that will hold the property and semicolon.
michael@0 2060 // Use this span to create a slightly larger click target
michael@0 2061 // for the value.
michael@0 2062 let propertyContainer = createChild(this.element, "span", {
michael@0 2063 class: "ruleview-propertycontainer"
michael@0 2064 });
michael@0 2065
michael@0 2066 propertyContainer.addEventListener("click", (aEvent) => {
michael@0 2067 // Clicks within the value shouldn't propagate any further.
michael@0 2068 aEvent.stopPropagation();
michael@0 2069
michael@0 2070 if (aEvent.target === propertyContainer) {
michael@0 2071 this.valueSpan.click();
michael@0 2072 }
michael@0 2073 }, false);
michael@0 2074
michael@0 2075 // Property value, editable when focused. Changes to the
michael@0 2076 // property value are applied as they are typed, and reverted
michael@0 2077 // if the user presses escape.
michael@0 2078 this.valueSpan = createChild(propertyContainer, "span", {
michael@0 2079 class: "ruleview-propertyvalue theme-fg-color1",
michael@0 2080 tabindex: "0",
michael@0 2081 });
michael@0 2082
michael@0 2083 this.valueSpan.addEventListener("click", (event) => {
michael@0 2084 let target = event.target;
michael@0 2085
michael@0 2086 if (target.nodeName === "a") {
michael@0 2087 event.stopPropagation();
michael@0 2088 event.preventDefault();
michael@0 2089 this.browserWindow.openUILinkIn(target.href, "tab");
michael@0 2090 }
michael@0 2091 }, false);
michael@0 2092
michael@0 2093 // Storing the TextProperty on the valuespan for easy access
michael@0 2094 // (for instance by the tooltip)
michael@0 2095 this.valueSpan.textProperty = this.prop;
michael@0 2096
michael@0 2097 // Save the initial value as the last committed value,
michael@0 2098 // for restoring after pressing escape.
michael@0 2099 this.committed = { name: this.prop.name,
michael@0 2100 value: this.prop.value,
michael@0 2101 priority: this.prop.priority };
michael@0 2102
michael@0 2103 appendText(propertyContainer, ";");
michael@0 2104
michael@0 2105 this.warning = createChild(this.element, "div", {
michael@0 2106 class: "ruleview-warning",
michael@0 2107 hidden: "",
michael@0 2108 title: CssLogic.l10n("rule.warning.title"),
michael@0 2109 });
michael@0 2110
michael@0 2111 // Holds the viewers for the computed properties.
michael@0 2112 // will be populated in |_updateComputed|.
michael@0 2113 this.computed = createChild(this.element, "ul", {
michael@0 2114 class: "ruleview-computedlist",
michael@0 2115 });
michael@0 2116
michael@0 2117 editableField({
michael@0 2118 start: this._onStartEditing,
michael@0 2119 element: this.valueSpan,
michael@0 2120 done: this._onValueDone,
michael@0 2121 destroy: this.update,
michael@0 2122 validate: this._onValidate,
michael@0 2123 advanceChars: ';',
michael@0 2124 contentType: InplaceEditor.CONTENT_TYPES.CSS_VALUE,
michael@0 2125 property: this.prop,
michael@0 2126 popup: this.popup
michael@0 2127 });
michael@0 2128 },
michael@0 2129
michael@0 2130 /**
michael@0 2131 * Get the path from which to resolve requests for this
michael@0 2132 * rule's stylesheet.
michael@0 2133 * @return {string} the stylesheet's href.
michael@0 2134 */
michael@0 2135 get sheetHref() {
michael@0 2136 let domRule = this.prop.rule.domRule;
michael@0 2137 if (domRule) {
michael@0 2138 return domRule.href || domRule.nodeHref;
michael@0 2139 }
michael@0 2140 },
michael@0 2141
michael@0 2142 /**
michael@0 2143 * Get the URI from which to resolve relative requests for
michael@0 2144 * this rule's stylesheet.
michael@0 2145 * @return {nsIURI} A URI based on the the stylesheet's href.
michael@0 2146 */
michael@0 2147 get sheetURI() {
michael@0 2148 if (this._sheetURI === undefined) {
michael@0 2149 if (this.sheetHref) {
michael@0 2150 this._sheetURI = IOService.newURI(this.sheetHref, null, null);
michael@0 2151 } else {
michael@0 2152 this._sheetURI = null;
michael@0 2153 }
michael@0 2154 }
michael@0 2155
michael@0 2156 return this._sheetURI;
michael@0 2157 },
michael@0 2158
michael@0 2159 /**
michael@0 2160 * Resolve a URI based on the rule stylesheet
michael@0 2161 * @param {string} relativePath the path to resolve
michael@0 2162 * @return {string} the resolved path.
michael@0 2163 */
michael@0 2164 resolveURI: function(relativePath) {
michael@0 2165 if (this.sheetURI) {
michael@0 2166 relativePath = this.sheetURI.resolve(relativePath);
michael@0 2167 }
michael@0 2168 return relativePath;
michael@0 2169 },
michael@0 2170
michael@0 2171 /**
michael@0 2172 * Check the property value to find an external resource (if any).
michael@0 2173 * @return {string} the URI in the property value, or null if there is no match.
michael@0 2174 */
michael@0 2175 getResourceURI: function() {
michael@0 2176 let val = this.prop.value;
michael@0 2177 let uriMatch = CSS_RESOURCE_RE.exec(val);
michael@0 2178 let uri = null;
michael@0 2179
michael@0 2180 if (uriMatch && uriMatch[1]) {
michael@0 2181 uri = uriMatch[1];
michael@0 2182 }
michael@0 2183
michael@0 2184 return uri;
michael@0 2185 },
michael@0 2186
michael@0 2187 /**
michael@0 2188 * Populate the span based on changes to the TextProperty.
michael@0 2189 */
michael@0 2190 update: function() {
michael@0 2191 if (this.prop.enabled) {
michael@0 2192 this.enable.style.removeProperty("visibility");
michael@0 2193 this.enable.setAttribute("checked", "");
michael@0 2194 } else {
michael@0 2195 this.enable.style.visibility = "visible";
michael@0 2196 this.enable.removeAttribute("checked");
michael@0 2197 }
michael@0 2198
michael@0 2199 this.warning.hidden = this.editing || this.isValid();
michael@0 2200
michael@0 2201 if ((this.prop.overridden || !this.prop.enabled) && !this.editing) {
michael@0 2202 this.element.classList.add("ruleview-overridden");
michael@0 2203 } else {
michael@0 2204 this.element.classList.remove("ruleview-overridden");
michael@0 2205 }
michael@0 2206
michael@0 2207 let name = this.prop.name;
michael@0 2208 this.nameSpan.textContent = name;
michael@0 2209
michael@0 2210 // Combine the property's value and priority into one string for
michael@0 2211 // the value.
michael@0 2212 let val = this.prop.value;
michael@0 2213 if (this.prop.priority) {
michael@0 2214 val += " !" + this.prop.priority;
michael@0 2215 }
michael@0 2216
michael@0 2217 let store = this.prop.rule.elementStyle.store;
michael@0 2218 let propDirty = store.userProperties.contains(this.prop.rule.style, name);
michael@0 2219
michael@0 2220 if (propDirty) {
michael@0 2221 this.element.setAttribute("dirty", "");
michael@0 2222 } else {
michael@0 2223 this.element.removeAttribute("dirty");
michael@0 2224 }
michael@0 2225
michael@0 2226 let swatchClass = "ruleview-colorswatch";
michael@0 2227 let outputParser = this.ruleEditor.ruleView._outputParser;
michael@0 2228 let frag = outputParser.parseCssProperty(name, val, {
michael@0 2229 colorSwatchClass: swatchClass,
michael@0 2230 colorClass: "ruleview-color",
michael@0 2231 defaultColorType: !propDirty,
michael@0 2232 urlClass: "theme-link",
michael@0 2233 baseURI: this.sheetURI
michael@0 2234 });
michael@0 2235 this.valueSpan.innerHTML = "";
michael@0 2236 this.valueSpan.appendChild(frag);
michael@0 2237
michael@0 2238 // Attach the color picker tooltip to the color swatches
michael@0 2239 this._swatchSpans = this.valueSpan.querySelectorAll("." + swatchClass);
michael@0 2240 for (let span of this._swatchSpans) {
michael@0 2241 // Capture the original declaration value to be able to revert later
michael@0 2242 let originalValue = this.valueSpan.textContent;
michael@0 2243 // Adding this swatch to the list of swatches our colorpicker knows about
michael@0 2244 this.ruleEditor.ruleView.colorPicker.addSwatch(span, {
michael@0 2245 onPreview: () => this._previewValue(this.valueSpan.textContent),
michael@0 2246 onCommit: () => this._applyNewValue(this.valueSpan.textContent),
michael@0 2247 onRevert: () => this._applyNewValue(originalValue)
michael@0 2248 });
michael@0 2249 }
michael@0 2250
michael@0 2251 // Populate the computed styles.
michael@0 2252 this._updateComputed();
michael@0 2253 },
michael@0 2254
michael@0 2255 _onStartEditing: function() {
michael@0 2256 this.element.classList.remove("ruleview-overridden");
michael@0 2257 this._previewValue(this.prop.value);
michael@0 2258 },
michael@0 2259
michael@0 2260 /**
michael@0 2261 * Populate the list of computed styles.
michael@0 2262 */
michael@0 2263 _updateComputed: function () {
michael@0 2264 // Clear out existing viewers.
michael@0 2265 while (this.computed.hasChildNodes()) {
michael@0 2266 this.computed.removeChild(this.computed.lastChild);
michael@0 2267 }
michael@0 2268
michael@0 2269 let showExpander = false;
michael@0 2270 for each (let computed in this.prop.computed) {
michael@0 2271 // Don't bother to duplicate information already
michael@0 2272 // shown in the text property.
michael@0 2273 if (computed.name === this.prop.name) {
michael@0 2274 continue;
michael@0 2275 }
michael@0 2276
michael@0 2277 showExpander = true;
michael@0 2278
michael@0 2279 let li = createChild(this.computed, "li", {
michael@0 2280 class: "ruleview-computed"
michael@0 2281 });
michael@0 2282
michael@0 2283 if (computed.overridden) {
michael@0 2284 li.classList.add("ruleview-overridden");
michael@0 2285 }
michael@0 2286
michael@0 2287 createChild(li, "span", {
michael@0 2288 class: "ruleview-propertyname theme-fg-color5",
michael@0 2289 textContent: computed.name
michael@0 2290 });
michael@0 2291 appendText(li, ": ");
michael@0 2292
michael@0 2293 let outputParser = this.ruleEditor.ruleView._outputParser;
michael@0 2294 let frag = outputParser.parseCssProperty(
michael@0 2295 computed.name, computed.value, {
michael@0 2296 colorSwatchClass: "ruleview-colorswatch",
michael@0 2297 urlClass: "theme-link",
michael@0 2298 baseURI: this.sheetURI
michael@0 2299 }
michael@0 2300 );
michael@0 2301
michael@0 2302 createChild(li, "span", {
michael@0 2303 class: "ruleview-propertyvalue theme-fg-color1",
michael@0 2304 child: frag
michael@0 2305 });
michael@0 2306
michael@0 2307 appendText(li, ";");
michael@0 2308 }
michael@0 2309
michael@0 2310 // Show or hide the expander as needed.
michael@0 2311 if (showExpander) {
michael@0 2312 this.expander.style.visibility = "visible";
michael@0 2313 } else {
michael@0 2314 this.expander.style.visibility = "hidden";
michael@0 2315 }
michael@0 2316 },
michael@0 2317
michael@0 2318 /**
michael@0 2319 * Handles clicks on the disabled property.
michael@0 2320 */
michael@0 2321 _onEnableClicked: function(aEvent) {
michael@0 2322 let checked = this.enable.hasAttribute("checked");
michael@0 2323 if (checked) {
michael@0 2324 this.enable.removeAttribute("checked");
michael@0 2325 } else {
michael@0 2326 this.enable.setAttribute("checked", "");
michael@0 2327 }
michael@0 2328 this.prop.setEnabled(!checked);
michael@0 2329 aEvent.stopPropagation();
michael@0 2330 },
michael@0 2331
michael@0 2332 /**
michael@0 2333 * Handles clicks on the computed property expander.
michael@0 2334 */
michael@0 2335 _onExpandClicked: function(aEvent) {
michael@0 2336 this.computed.classList.toggle("styleinspector-open");
michael@0 2337 if (this.computed.classList.contains("styleinspector-open")) {
michael@0 2338 this.expander.setAttribute("open", "true");
michael@0 2339 } else {
michael@0 2340 this.expander.removeAttribute("open");
michael@0 2341 }
michael@0 2342 aEvent.stopPropagation();
michael@0 2343 },
michael@0 2344
michael@0 2345 /**
michael@0 2346 * Called when the property name's inplace editor is closed.
michael@0 2347 * Ignores the change if the user pressed escape, otherwise
michael@0 2348 * commits it.
michael@0 2349 *
michael@0 2350 * @param {string} aValue
michael@0 2351 * The value contained in the editor.
michael@0 2352 * @param {boolean} aCommit
michael@0 2353 * True if the change should be applied.
michael@0 2354 */
michael@0 2355 _onNameDone: function(aValue, aCommit) {
michael@0 2356 if (aCommit) {
michael@0 2357 // Unlike the value editor, if a name is empty the entire property
michael@0 2358 // should always be removed.
michael@0 2359 if (aValue.trim() === "") {
michael@0 2360 this.remove();
michael@0 2361 } else {
michael@0 2362 // Adding multiple rules inside of name field overwrites the current
michael@0 2363 // property with the first, then adds any more onto the property list.
michael@0 2364 let properties = parseDeclarations(aValue);
michael@0 2365
michael@0 2366 if (properties.length) {
michael@0 2367 this.prop.setName(properties[0].name);
michael@0 2368 if (properties.length > 1) {
michael@0 2369 this.prop.setValue(properties[0].value, properties[0].priority);
michael@0 2370 this.ruleEditor.addProperties(properties.slice(1), this.prop);
michael@0 2371 }
michael@0 2372 }
michael@0 2373 }
michael@0 2374 }
michael@0 2375 },
michael@0 2376
michael@0 2377 /**
michael@0 2378 * Remove property from style and the editors from DOM.
michael@0 2379 * Begin editing next available property.
michael@0 2380 */
michael@0 2381 remove: function() {
michael@0 2382 if (this._swatchSpans && this._swatchSpans.length) {
michael@0 2383 for (let span of this._swatchSpans) {
michael@0 2384 this.ruleEditor.ruleView.colorPicker.removeSwatch(span);
michael@0 2385 }
michael@0 2386 }
michael@0 2387
michael@0 2388 this.element.parentNode.removeChild(this.element);
michael@0 2389 this.ruleEditor.rule.editClosestTextProperty(this.prop);
michael@0 2390 this.valueSpan.textProperty = null;
michael@0 2391 this.prop.remove();
michael@0 2392 },
michael@0 2393
michael@0 2394 /**
michael@0 2395 * Called when a value editor closes. If the user pressed escape,
michael@0 2396 * revert to the value this property had before editing.
michael@0 2397 *
michael@0 2398 * @param {string} aValue
michael@0 2399 * The value contained in the editor.
michael@0 2400 * @param {bool} aCommit
michael@0 2401 * True if the change should be applied.
michael@0 2402 */
michael@0 2403 _onValueDone: function(aValue, aCommit) {
michael@0 2404 if (!aCommit) {
michael@0 2405 // A new property should be removed when escape is pressed.
michael@0 2406 if (this.removeOnRevert) {
michael@0 2407 this.remove();
michael@0 2408 } else {
michael@0 2409 this.prop.setValue(this.committed.value, this.committed.priority);
michael@0 2410 }
michael@0 2411 return;
michael@0 2412 }
michael@0 2413
michael@0 2414 let {propertiesToAdd,firstValue} = this._getValueAndExtraProperties(aValue);
michael@0 2415
michael@0 2416 // First, set this property value (common case, only modified a property)
michael@0 2417 let val = parseSingleValue(firstValue);
michael@0 2418 this.prop.setValue(val.value, val.priority);
michael@0 2419 this.removeOnRevert = false;
michael@0 2420 this.committed.value = this.prop.value;
michael@0 2421 this.committed.priority = this.prop.priority;
michael@0 2422
michael@0 2423 // If needed, add any new properties after this.prop.
michael@0 2424 this.ruleEditor.addProperties(propertiesToAdd, this.prop);
michael@0 2425
michael@0 2426 // If the name or value is not actively being edited, and the value is
michael@0 2427 // empty, then remove the whole property.
michael@0 2428 // A timeout is used here to accurately check the state, since the inplace
michael@0 2429 // editor `done` and `destroy` events fire before the next editor
michael@0 2430 // is focused.
michael@0 2431 if (val.value.trim() === "") {
michael@0 2432 setTimeout(() => {
michael@0 2433 if (!this.editing) {
michael@0 2434 this.remove();
michael@0 2435 }
michael@0 2436 }, 0);
michael@0 2437 }
michael@0 2438 },
michael@0 2439
michael@0 2440 /**
michael@0 2441 * Parse a value string and break it into pieces, starting with the
michael@0 2442 * first value, and into an array of additional properties (if any).
michael@0 2443 *
michael@0 2444 * Example: Calling with "red; width: 100px" would return
michael@0 2445 * { firstValue: "red", propertiesToAdd: [{ name: "width", value: "100px" }] }
michael@0 2446 *
michael@0 2447 * @param {string} aValue
michael@0 2448 * The string to parse
michael@0 2449 * @return {object} An object with the following properties:
michael@0 2450 * firstValue: A string containing a simple value, like
michael@0 2451 * "red" or "100px!important"
michael@0 2452 * propertiesToAdd: An array with additional properties, following the
michael@0 2453 * parseDeclarations format of {name,value,priority}
michael@0 2454 */
michael@0 2455 _getValueAndExtraProperties: function(aValue) {
michael@0 2456 // The inplace editor will prevent manual typing of multiple properties,
michael@0 2457 // but we need to deal with the case during a paste event.
michael@0 2458 // Adding multiple properties inside of value editor sets value with the
michael@0 2459 // first, then adds any more onto the property list (below this property).
michael@0 2460 let firstValue = aValue;
michael@0 2461 let propertiesToAdd = [];
michael@0 2462
michael@0 2463 let properties = parseDeclarations(aValue);
michael@0 2464
michael@0 2465 // Check to see if the input string can be parsed as multiple properties
michael@0 2466 if (properties.length) {
michael@0 2467 // Get the first property value (if any), and any remaining properties (if any)
michael@0 2468 if (!properties[0].name && properties[0].value) {
michael@0 2469 firstValue = properties[0].value;
michael@0 2470 propertiesToAdd = properties.slice(1);
michael@0 2471 }
michael@0 2472 // In some cases, the value could be a property:value pair itself.
michael@0 2473 // Join them as one value string and append potentially following properties
michael@0 2474 else if (properties[0].name && properties[0].value) {
michael@0 2475 firstValue = properties[0].name + ": " + properties[0].value;
michael@0 2476 propertiesToAdd = properties.slice(1);
michael@0 2477 }
michael@0 2478 }
michael@0 2479
michael@0 2480 return {
michael@0 2481 propertiesToAdd: propertiesToAdd,
michael@0 2482 firstValue: firstValue
michael@0 2483 };
michael@0 2484 },
michael@0 2485
michael@0 2486 _applyNewValue: function(aValue) {
michael@0 2487 let val = parseSingleValue(aValue);
michael@0 2488
michael@0 2489 this.prop.setValue(val.value, val.priority);
michael@0 2490 this.removeOnRevert = false;
michael@0 2491 this.committed.value = this.prop.value;
michael@0 2492 this.committed.priority = this.prop.priority;
michael@0 2493 },
michael@0 2494
michael@0 2495 /**
michael@0 2496 * Live preview this property, without committing changes.
michael@0 2497 * @param {string} aValue The value to set the current property to.
michael@0 2498 */
michael@0 2499 _previewValue: function(aValue) {
michael@0 2500 // Since function call is throttled, we need to make sure we are still editing
michael@0 2501 if (!this.editing) {
michael@0 2502 return;
michael@0 2503 }
michael@0 2504
michael@0 2505 let val = parseSingleValue(aValue);
michael@0 2506 this.ruleEditor.rule.previewPropertyValue(this.prop, val.value, val.priority);
michael@0 2507 },
michael@0 2508
michael@0 2509 /**
michael@0 2510 * Validate this property. Does it make sense for this value to be assigned
michael@0 2511 * to this property name? This does not apply the property value
michael@0 2512 *
michael@0 2513 * @param {string} [aValue]
michael@0 2514 * The property value used for validation.
michael@0 2515 * Defaults to the current value for this.prop
michael@0 2516 *
michael@0 2517 * @return {bool} true if the property value is valid, false otherwise.
michael@0 2518 */
michael@0 2519 isValid: function(aValue) {
michael@0 2520 let name = this.prop.name;
michael@0 2521 let value = typeof aValue == "undefined" ? this.prop.value : aValue;
michael@0 2522 let val = parseSingleValue(value);
michael@0 2523
michael@0 2524 let style = this.doc.createElementNS(HTML_NS, "div").style;
michael@0 2525 let prefs = Services.prefs;
michael@0 2526
michael@0 2527 // We toggle output of errors whilst the user is typing a property value.
michael@0 2528 let prefVal = prefs.getBoolPref("layout.css.report_errors");
michael@0 2529 prefs.setBoolPref("layout.css.report_errors", false);
michael@0 2530
michael@0 2531 let validValue = false;
michael@0 2532 try {
michael@0 2533 style.setProperty(name, val.value, val.priority);
michael@0 2534 validValue = style.getPropertyValue(name) !== "" || val.value === "";
michael@0 2535 } finally {
michael@0 2536 prefs.setBoolPref("layout.css.report_errors", prefVal);
michael@0 2537 }
michael@0 2538 return validValue;
michael@0 2539 }
michael@0 2540 };
michael@0 2541
michael@0 2542 /**
michael@0 2543 * Store of CSSStyleDeclarations mapped to properties that have been changed by
michael@0 2544 * the user.
michael@0 2545 */
michael@0 2546 function UserProperties() {
michael@0 2547 this.map = new Map();
michael@0 2548 }
michael@0 2549
michael@0 2550 UserProperties.prototype = {
michael@0 2551 /**
michael@0 2552 * Get a named property for a given CSSStyleDeclaration.
michael@0 2553 *
michael@0 2554 * @param {CSSStyleDeclaration} aStyle
michael@0 2555 * The CSSStyleDeclaration against which the property is mapped.
michael@0 2556 * @param {string} aName
michael@0 2557 * The name of the property to get.
michael@0 2558 * @param {string} aDefault
michael@0 2559 * The value to return if the property is has been changed outside of
michael@0 2560 * the rule view.
michael@0 2561 * @return {string}
michael@0 2562 * The property value if it has previously been set by the user, null
michael@0 2563 * otherwise.
michael@0 2564 */
michael@0 2565 getProperty: function(aStyle, aName, aDefault) {
michael@0 2566 let key = this.getKey(aStyle);
michael@0 2567 let entry = this.map.get(key, null);
michael@0 2568
michael@0 2569 if (entry && aName in entry) {
michael@0 2570 let item = entry[aName];
michael@0 2571 if (item != aDefault) {
michael@0 2572 delete entry[aName];
michael@0 2573 return aDefault;
michael@0 2574 }
michael@0 2575 return item;
michael@0 2576 }
michael@0 2577 return aDefault;
michael@0 2578 },
michael@0 2579
michael@0 2580 /**
michael@0 2581 * Set a named property for a given CSSStyleDeclaration.
michael@0 2582 *
michael@0 2583 * @param {CSSStyleDeclaration} aStyle
michael@0 2584 * The CSSStyleDeclaration against which the property is to be mapped.
michael@0 2585 * @param {String} aName
michael@0 2586 * The name of the property to set.
michael@0 2587 * @param {String} aUserValue
michael@0 2588 * The value of the property to set.
michael@0 2589 */
michael@0 2590 setProperty: function(aStyle, aName, aUserValue) {
michael@0 2591 let key = this.getKey(aStyle);
michael@0 2592 let entry = this.map.get(key, null);
michael@0 2593 if (entry) {
michael@0 2594 entry[aName] = aUserValue;
michael@0 2595 } else {
michael@0 2596 let props = {};
michael@0 2597 props[aName] = aUserValue;
michael@0 2598 this.map.set(key, props);
michael@0 2599 }
michael@0 2600 },
michael@0 2601
michael@0 2602 /**
michael@0 2603 * Check whether a named property for a given CSSStyleDeclaration is stored.
michael@0 2604 *
michael@0 2605 * @param {CSSStyleDeclaration} aStyle
michael@0 2606 * The CSSStyleDeclaration against which the property would be mapped.
michael@0 2607 * @param {String} aName
michael@0 2608 * The name of the property to check.
michael@0 2609 */
michael@0 2610 contains: function(aStyle, aName) {
michael@0 2611 let key = this.getKey(aStyle);
michael@0 2612 let entry = this.map.get(key, null);
michael@0 2613 return !!entry && aName in entry;
michael@0 2614 },
michael@0 2615
michael@0 2616 getKey: function(aStyle) {
michael@0 2617 return aStyle.href + ":" + aStyle.line;
michael@0 2618 }
michael@0 2619 };
michael@0 2620
michael@0 2621 /**
michael@0 2622 * Helper functions
michael@0 2623 */
michael@0 2624
michael@0 2625 /**
michael@0 2626 * Create a child element with a set of attributes.
michael@0 2627 *
michael@0 2628 * @param {Element} aParent
michael@0 2629 * The parent node.
michael@0 2630 * @param {string} aTag
michael@0 2631 * The tag name.
michael@0 2632 * @param {object} aAttributes
michael@0 2633 * A set of attributes to set on the node.
michael@0 2634 */
michael@0 2635 function createChild(aParent, aTag, aAttributes) {
michael@0 2636 let elt = aParent.ownerDocument.createElementNS(HTML_NS, aTag);
michael@0 2637 for (let attr in aAttributes) {
michael@0 2638 if (aAttributes.hasOwnProperty(attr)) {
michael@0 2639 if (attr === "textContent") {
michael@0 2640 elt.textContent = aAttributes[attr];
michael@0 2641 } else if(attr === "child") {
michael@0 2642 elt.appendChild(aAttributes[attr]);
michael@0 2643 } else {
michael@0 2644 elt.setAttribute(attr, aAttributes[attr]);
michael@0 2645 }
michael@0 2646 }
michael@0 2647 }
michael@0 2648 aParent.appendChild(elt);
michael@0 2649 return elt;
michael@0 2650 }
michael@0 2651
michael@0 2652 function createMenuItem(aMenu, aAttributes) {
michael@0 2653 let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
michael@0 2654
michael@0 2655 item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
michael@0 2656 item.setAttribute("accesskey", _strings.GetStringFromName(aAttributes.accesskey));
michael@0 2657 item.addEventListener("command", aAttributes.command);
michael@0 2658
michael@0 2659 aMenu.appendChild(item);
michael@0 2660
michael@0 2661 return item;
michael@0 2662 }
michael@0 2663
michael@0 2664 function setTimeout() {
michael@0 2665 let window = Services.appShell.hiddenDOMWindow;
michael@0 2666 return window.setTimeout.apply(window, arguments);
michael@0 2667 }
michael@0 2668
michael@0 2669 function clearTimeout() {
michael@0 2670 let window = Services.appShell.hiddenDOMWindow;
michael@0 2671 return window.clearTimeout.apply(window, arguments);
michael@0 2672 }
michael@0 2673
michael@0 2674 function throttle(func, wait, scope) {
michael@0 2675 var timer = null;
michael@0 2676 return function() {
michael@0 2677 if(timer) {
michael@0 2678 clearTimeout(timer);
michael@0 2679 }
michael@0 2680 var args = arguments;
michael@0 2681 timer = setTimeout(function() {
michael@0 2682 timer = null;
michael@0 2683 func.apply(scope, args);
michael@0 2684 }, wait);
michael@0 2685 };
michael@0 2686 }
michael@0 2687
michael@0 2688 /**
michael@0 2689 * Event handler that causes a blur on the target if the input has
michael@0 2690 * multiple CSS properties as the value.
michael@0 2691 */
michael@0 2692 function blurOnMultipleProperties(e) {
michael@0 2693 setTimeout(() => {
michael@0 2694 let props = parseDeclarations(e.target.value);
michael@0 2695 if (props.length > 1) {
michael@0 2696 e.target.blur();
michael@0 2697 }
michael@0 2698 }, 0);
michael@0 2699 }
michael@0 2700
michael@0 2701 /**
michael@0 2702 * Append a text node to an element.
michael@0 2703 */
michael@0 2704 function appendText(aParent, aText) {
michael@0 2705 aParent.appendChild(aParent.ownerDocument.createTextNode(aText));
michael@0 2706 }
michael@0 2707
michael@0 2708 XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
michael@0 2709 return Cc["@mozilla.org/widget/clipboardhelper;1"].
michael@0 2710 getService(Ci.nsIClipboardHelper);
michael@0 2711 });
michael@0 2712
michael@0 2713 XPCOMUtils.defineLazyGetter(this, "_strings", function() {
michael@0 2714 return Services.strings.createBundle(
michael@0 2715 "chrome://global/locale/devtools/styleinspector.properties");
michael@0 2716 });
michael@0 2717
michael@0 2718 XPCOMUtils.defineLazyGetter(this, "domUtils", function() {
michael@0 2719 return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
michael@0 2720 });
michael@0 2721
michael@0 2722 loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);

mercurial