Quellcodebibliothek Statistik Leitseite products/Sources/formale Sprachen/C/Firefox/devtools/server/actors/utils/   (Browser von der Mozilla Stiftung Version 136.0.1©)  Datei vom 10.2.2025 mit Größe 32 kB image not shown  

Quelle  stylesheets-manager.js   Sprache: JAVA

 
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */


"use strict";

const EventEmitter = require("resource://devtools/shared/event-emitter.js");
const {
  getSourcemapBaseURL,
} = require("resource://devtools/server/actors/utils/source-map-utils.js");

loader.lazyRequireGetter(
  this,
  ["addPseudoClassLock""removePseudoClassLock"],
  "resource://devtools/server/actors/highlighters/utils/markup.js",
  true
);
loader.lazyRequireGetter(
  this,
  "loadSheet",
  "resource://devtools/shared/layout/utils.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["getStyleSheetOwnerNode""getStyleSheetText"],
  "resource://devtools/server/actors/utils/stylesheet-utils.js",
  true
);

const TRANSITION_PSEUDO_CLASS = ":-moz-styleeditor-transitioning";
const TRANSITION_DURATION_MS = 500;
const TRANSITION_BUFFER_MS = 1000;
const TRANSITION_RULE_SELECTOR = `:root${TRANSITION_PSEUDO_CLASS}, :root${TRANSITION_PSEUDO_CLASS} *:not(:-moz-native-anonymous)`;
const TRANSITION_SHEET =
  "data:text/css;charset=utf-8," +
  encodeURIComponent(`
  ${TRANSITION_RULE_SELECTOR} {
    transition-duration: ${TRANSITION_DURATION_MS}ms !important;
    transition-delay: 0ms !important;
    transition-timing-function: ease-out !important;
    transition-property: all !important;
  }
`);

// The possible kinds of style-applied events.
// UPDATE_PRESERVING_RULES means that the update is guaranteed to
// preserve the number and order of rules on the style sheet.
// UPDATE_GENERAL covers any other kind of change to the style sheet.
const UPDATE_PRESERVING_RULES = 0;
const UPDATE_GENERAL = 1;

// If the user edits a stylesheet, we stash a copy of the edited text
// here, keyed by the stylesheet.  This way, if the tools are closed
// and then reopened, the edited text will be available. A weak map
// is used so that navigation by the user will eventually cause the
// edited text to be collected.
const modifiedStyleSheets = new WeakMap();

/**
 * Manage stylesheets related to a given Target Actor.
 * @emits stylesheet-updated: emitted when there was changes in a stylesheet
 *        First arg is an object with the following properties:
 *        - resourceId {String}: The id that was assigned to the stylesheet
 *        - updateKind {String}: Which kind of update it is ("style-applied",
 *          "at-rules-changed", "matches-change", "property-change")
 *        - updates {Object}: The update data
 */

class StyleSheetsManager extends EventEmitter {
  #abortController;
  // Map<resourceId, AbortController>
  #mqlChangeAbortControllerMap = new Map();
  #styleSheetCount = 0;
  #styleSheetMap = new Map();
  #styleSheetCreationData;
  #targetActor;
  #transitionSheetLoaded;
  #transitionTimeout;
  #watchListeners = {
    onAvailable: [],
    onUpdated: [],
    onDestroyed: [],
  };

  /**
   * @param TargetActor targetActor
   *        The target actor from which we should observe stylesheet changes.
   */

  constructor(targetActor) {
    super();

    this.#targetActor = targetActor;
  }

  #setEventListenersIfNeeded() {
    if (this.#abortController) {
      return;
    }

    this.#abortController = new AbortController();
    const { signal } = this.#abortController;

    // Listen for new stylesheet being added via StyleSheetApplicableStateChanged
    this.#targetActor.chromeEventHandler.addEventListener(
      "StyleSheetApplicableStateChanged",
      this.#onApplicableStateChanged,
      { capture: true, signal }
    );
    this.#targetActor.chromeEventHandler.addEventListener(
      "StyleSheetRemoved",
      this.#onStylesheetRemoved,
      { capture: true, signal }
    );

    this.#watchStyleSheetChangeEvents();
    this.#targetActor.on("window-ready"this.#onTargetActorWindowReady, {
      signal,
    });
  }

  /**
   * Calling this function will make the StyleSheetsManager start the event listeners needed
   * to watch for stylesheet additions and modifications.
   * This resolves once it notified about existing stylesheets.
   * @param {Object} options
   * @param {Function} onAvailable: Function that will be called when a stylesheet is
   *                   registered, but also with already registered stylesheets
   *                   if ignoreExisting is not set to true.
   *                   This is called with a single object parameter with the following properties:
   *                   - {String} resourceId: The id that was assigned to the stylesheet
   *                   - {StyleSheet} styleSheet: The actual stylesheet object
   *                   - {Object} creationData: An object with:
   *                              - {Boolean} isCreatedByDevTools: Was the stylesheet created
   *                                by DevTools (e.g. by the user clicking the new stylesheet
   *                                button in the styleeditor)
   *                              - {String} fileName
   * @param {Function} onUpdated: Function that will be called when a stylesheet is updated
   *                   This is called with a single object parameter with the following properties:
   *                   - {String} resourceId: The id that was assigned to the stylesheet
   *                   - {String} updateKind: Which kind of update it is ("style-applied",
   *                     "at-rules-changed", "matches-change", "property-change")
   *                   - {Object} updates : The update data
   * @param {Function} onDestroyed: Function that will be called when a stylesheet is removed
   *                   This is called with a single object parameter with the following properties:
   *                   - {String} resourceId: The id that was assigned to the stylesheet
   * @param {Boolean} ignoreExisting: Pass to true to avoid onAvailable to be called with
   *                  already registered stylesheets.
   */

  async watch({ onAvailable, onUpdated, onDestroyed, ignoreExisting = false }) {
    if (!onAvailable && !onUpdated && !onDestroyed) {
      throw new Error("Expect onAvailable, onUpdated or onDestroyed");
    }

    if (onAvailable) {
      if (typeof onAvailable !== "function") {
        throw new Error("onAvailable should be a function");
      }

      // Don't register the listener yet if we're ignoring existing stylesheets, we'll do
      // that at the end of the function, after we processed existing stylesheets.
    }

    if (onUpdated) {
      if (typeof onUpdated !== "function") {
        throw new Error("onUpdated should be a function");
      }
      this.#watchListeners.onUpdated.push(onUpdated);
    }

    if (onDestroyed) {
      if (typeof onDestroyed !== "function") {
        throw new Error("onDestroyed should be a function");
      }
      this.#watchListeners.onDestroyed.push(onDestroyed);
    }

    // Process existing stylesheets
    const promises = [];
    for (const window of this.#targetActor.windows) {
      promises.push(this.#getStyleSheetsForWindow(window));
    }

    this.#setEventListenersIfNeeded();

    // Finally, notify about existing stylesheets
    const styleSheets = await Promise.all(promises);
    const styleSheetsData = styleSheets.flat().map(styleSheet => ({
      styleSheet,
      resourceId: this.#registerStyleSheet(styleSheet),
    }));

    let registeredStyleSheetsPromises;
    if (onAvailable && ignoreExisting !== true) {
      registeredStyleSheetsPromises = styleSheetsData.map(
        ({ resourceId, styleSheet }) => onAvailable({ resourceId, styleSheet })
      );
    }

    // Only register the listener after we went over the list of existing stylesheets
    // so the listener is not triggered by possible calls to #registerStyleSheet earlier.
    if (onAvailable) {
      this.#watchListeners.onAvailable.push(onAvailable);
    }

    if (registeredStyleSheetsPromises) {
      await Promise.all(registeredStyleSheetsPromises);
    }
  }

  /**
   * Remove the passed listeners
   *
   * @param {Object} options: See this.watch
   */

  unwatch({ onAvailable, onUpdated, onDestroyed }) {
    if (!this.#watchListeners) {
      return;
    }

    if (onAvailable) {
      const index = this.#watchListeners.onAvailable.indexOf(onAvailable);
      if (index !== -1) {
        this.#watchListeners.onAvailable.splice(index, 1);
      }
    }

    if (onUpdated) {
      const index = this.#watchListeners.onUpdated.indexOf(onUpdated);
      if (index !== -1) {
        this.#watchListeners.onUpdated.splice(index, 1);
      }
    }

    if (onDestroyed) {
      const index = this.#watchListeners.onDestroyed.indexOf(onDestroyed);
      if (index !== -1) {
        this.#watchListeners.onDestroyed.splice(index, 1);
      }
    }
  }

  #watchStyleSheetChangeEvents() {
    for (const window of this.#targetActor.windows) {
      this.#watchStyleSheetChangeEventsForWindow(window);
    }
  }

  #onTargetActorWindowReady = ({ window }) => {
    this.#watchStyleSheetChangeEventsForWindow(window);
  };

  #watchStyleSheetChangeEventsForWindow(window) {
    // We have to set this flag in order to get the
    // StyleSheetApplicableStateChanged and StyleSheetRemoved events. See Document.webidl.
    window.document.styleSheetChangeEventsEnabled = true;
  }

  #unwatchStyleSheetChangeEvents() {
    for (const window of this.#targetActor.windows) {
      window.document.styleSheetChangeEventsEnabled = false;
    }
  }

  /**
   * Create a new style sheet in the document with the given text.
   *
   * @param  {Document} document
   *         Document that the new style sheet belong to.
   * @param  {string} text
   *         Content of style sheet.
   * @param  {string} fileName
   *         If the stylesheet adding is from file, `fileName` indicates the path.
   */

  async addStyleSheet(document, text, fileName) {
    const parent = document.documentElement;
    const style = document.createElementNS(
      "http://www.w3.org/1999/xhtml",
      "style"
    );
    style.setAttribute("type""text/css");
    style.setDevtoolsAsTriggeringPrincipal();

    if (text) {
      style.appendChild(document.createTextNode(text));
    }

    // This triggers StyleSheetApplicableStateChanged event.
    parent.appendChild(style);

    // This promise will be resolved when the resource for this stylesheet is available.
    let resolve = null;
    const promise = new Promise(r => {
      resolve = r;
    });

    if (!this.#styleSheetCreationData) {
      this.#styleSheetCreationData = new WeakMap();
    }
    this.#styleSheetCreationData.set(style.sheet, {
      isCreatedByDevTools: true,
      fileName,
      resolve,
    });

    await promise;

    return style.sheet;
  }

  /**
   * Return resourceId of the given style sheet or create one if the stylesheet wasn't
   * registered yet.
   *
   * @params {StyleSheet} styleSheet
   * @returns {String} resourceId
   */

  getStyleSheetResourceId(styleSheet) {
    const existingResourceId = this.#findStyleSheetResourceId(styleSheet);
    if (existingResourceId) {
      return existingResourceId;
    }

    // If we couldn't find an associated resourceId, that means the stylesheet isn't
    // registered yet. Calling #registerStyleSheet will register it and return the
    // associated resourceId it computed for it.
    return this.#registerStyleSheet(styleSheet);
  }

  /**
   * Return the associated resourceId of the given registered style sheet, or null if the
   * stylesheet wasn't registered yet.
   *
   * @params {StyleSheet} styleSheet
   * @returns {String} resourceId
   */

  #findStyleSheetResourceId(styleSheet) {
    for (const [
      resourceId,
      existingStyleSheet,
    ] of this.#styleSheetMap.entries()) {
      if (styleSheet === existingStyleSheet) {
        return resourceId;
      }
    }

    return null;
  }

  /**
   * Return owner node of the style sheet of the given resource id.
   *
   * @params {String} resourceId
   *                  The id associated with the stylesheet
   * @returns {Element|null}
   */

  getOwnerNode(resourceId) {
    const styleSheet = this.#styleSheetMap.get(resourceId);
    return styleSheet.ownerNode;
  }

  /**
   * Return the index of given stylesheet of the given resource id.
   *
   * @params {String} resourceId
   *                  The id associated with the stylesheet
   * @returns {Number}
   */

  getStyleSheetIndex(resourceId) {
    const styleSheet = this.#styleSheetMap.get(resourceId);

    const styleSheets = InspectorUtils.getAllStyleSheets(
      this.#targetActor.window.document,
      true
    );
    let i = 0;
    for (const sheet of styleSheets) {
      if (!this.#shouldListSheet(sheet)) {
        continue;
      }
      if (sheet == styleSheet) {
        return i;
      }
      i++;
    }
    return -1;
  }

  /**
   * Get the text of a stylesheet given its resourceId.
   *
   * @params {String} resourceId
   *                  The id associated with the stylesheet
   * @returns {String}
   */

  async getText(resourceId) {
    const styleSheet = this.#styleSheetMap.get(resourceId);

    const modifiedText = modifiedStyleSheets.get(styleSheet);

    // modifiedText is the content of the stylesheet updated by update function.
    // In case not updating, this is undefined.
    if (modifiedText !== undefined) {
      return modifiedText;
    }

    return getStyleSheetText(styleSheet);
  }

  /**
   * Toggle the disabled property of the stylesheet
   *
   * @params {String} resourceId
   *                  The id associated with the stylesheet
   * @return {Boolean} the disabled state after toggling.
   */

  toggleDisabled(resourceId) {
    const styleSheet = this.#styleSheetMap.get(resourceId);
    styleSheet.disabled = !styleSheet.disabled;

    this.#notifyPropertyChanged(resourceId, "disabled", styleSheet.disabled);

    return styleSheet.disabled;
  }

  /**
   * Update the style sheet in place with new text.
   *
   * @param  {String} resourceId
   * @param  {String} text
   *         New text.
   * @param  {Object} options
   * @param  {Boolean} options.transition
   *         Whether to do CSS transition for change. Defaults to false.
   * @param  {Number} options.kind
   *         Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL. Defaults to UPDATE_GENERAL.
   * @param {String} options.cause
   *         Indicates the cause of this update (e.g. "styleeditor") if this was called
   *         from the stylesheet to be edited by the user from the StyleEditor.
   */

  async setStyleSheetText(
    resourceId,
    text,
    { transition = false, kind = UPDATE_GENERAL, cause = "" } = {}
  ) {
    const styleSheet = this.#styleSheetMap.get(resourceId);
    InspectorUtils.parseStyleSheet(styleSheet, text);
    modifiedStyleSheets.set(styleSheet, text);

    // getStyleSheetRuleCountAndAtRules can be costly, so only call it when needed,
    // i.e. when the whole stylesheet is modified, not when a rule body is.
    let atRules, ruleCount;
    if (kind !== UPDATE_PRESERVING_RULES) {
      ({ atRules, ruleCount } =
        this.getStyleSheetRuleCountAndAtRules(styleSheet));
      this.#notifyPropertyChanged(resourceId, "ruleCount", ruleCount);
    }

    if (transition) {
      this.#startTransition(resourceId, kind, cause);
    } else {
      this.#onStyleSheetUpdated({
        resourceId,
        updateKind: "style-applied",
        updates: {
          event: { kind, cause },
        },
      });
    }

    if (kind !== UPDATE_PRESERVING_RULES) {
      this.#onStyleSheetUpdated({
        resourceId,
        updateKind: "at-rules-changed",
        updates: {
          resourceUpdates: { atRules },
        },
      });
    }
  }

  /**
   * Applies a transition to the stylesheet document so any change made by the user in the
   * client will be animated so it's more visible.
   *
   * @param {String} resourceId
   *        The id associated with the stylesheet
   * @param {Number} kind
   *        Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
   * @param {String} cause
   *         Indicates the cause of this update (e.g. "styleeditor") if this was called
   *         from the stylesheet to be edited by the user from the StyleEditor.
   */

  #startTransition(resourceId, kind, cause) {
    const styleSheet = this.#styleSheetMap.get(resourceId);
    const document = styleSheet.associatedDocument;
    const window = document.ownerGlobal;

    if (!this.#transitionSheetLoaded) {
      this.#transitionSheetLoaded = true;
      // We don't remove this sheet. It uses an internal selector that
      // we only apply via locks, so there's no need to load and unload
      // it all the time.
      loadSheet(window, TRANSITION_SHEET);
    }

    addPseudoClassLock(document.documentElement, TRANSITION_PSEUDO_CLASS);

    // Set up clean up and commit after transition duration (+buffer)
    // @see #onTransitionEnd
    window.clearTimeout(this.#transitionTimeout);
    this.#transitionTimeout = window.setTimeout(
      this.#onTransitionEnd.bind(this, resourceId, kind, cause),
      TRANSITION_DURATION_MS + TRANSITION_BUFFER_MS
    );
  }

  /**
   * @param {String} resourceId
   *        The id associated with the stylesheet
   * @param {Number} kind
   *        Either UPDATE_PRESERVING_RULES or UPDATE_GENERAL
   * @param {String} cause
   *         Indicates the cause of this update (e.g. "styleeditor") if this was called
   *         from the stylesheet to be edited by the user from the StyleEditor.
   */

  #onTransitionEnd(resourceId, kind, cause) {
    const styleSheet = this.#styleSheetMap.get(resourceId);
    const document = styleSheet.associatedDocument;

    this.#transitionTimeout = null;
    removePseudoClassLock(document.documentElement, TRANSITION_PSEUDO_CLASS);

    this.#onStyleSheetUpdated({
      resourceId,
      updateKind: "style-applied",
      updates: {
        event: { kind, cause },
      },
    });
  }

  /**
   * Retrieve the CSSRuleList of a given stylesheet
   *
   * @param {StyleSheet} styleSheet
   * @returns {CSSRuleList}
   */

  #getCSSRules(styleSheet) {
    try {
      return styleSheet.cssRules;
    } catch (e) {
      // sheet isn't loaded yet
    }

    if (!styleSheet.ownerNode) {
      return Promise.resolve([]);
    }

    return new Promise(resolve => {
      styleSheet.ownerNode.addEventListener(
        "load",
        () => resolve(styleSheet.cssRules),
        { once: true }
      );
    });
  }

  /**
   * Get the stylesheets imported by a given stylesheet (via @import)
   *
   * @param {Document} document
   * @param {StyleSheet} styleSheet
   * @returns Array<StyleSheet>
   */

  async #getImportedStyleSheets(document, styleSheet) {
    const importedStyleSheets = [];

    for (const rule of await this.#getCSSRules(styleSheet)) {
      const ruleClassName = ChromeUtils.getClassName(rule);
      if (ruleClassName == "CSSImportRule") {
        // With the Gecko style system, the associated styleSheet may be null
        // if it has already been seen because an import cycle for the same
        // URL.  With Stylo, the styleSheet will exist (which is correct per
        // the latest CSSOM spec), so we also need to check ancestors for the
        // same URL to avoid cycles.
        if (
          !rule.styleSheet ||
          this.#haveAncestorWithSameURL(rule.styleSheet) ||
          !this.#shouldListSheet(rule.styleSheet)
        ) {
          continue;
        }

        importedStyleSheets.push(rule.styleSheet);

        // recurse imports in this stylesheet as well
        const children = await this.#getImportedStyleSheets(
          document,
          rule.styleSheet
        );
        importedStyleSheets.push(...children);
      } else if (ruleClassName != "CSSCharsetRule") {
        // @import rules must precede all others except @charset
        break;
      }
    }

    return importedStyleSheets;
  }

  /**
   * Retrieve the total number of rules (including nested ones) and
   * all the at-rules of a given stylesheet.
   *
   * @param {StyleSheet} styleSheet
   * @returns {Object} An object of the following shape:
   *          - {Integer} ruleCount: The total number of rules in the stylesheet
   *          - {Array<Object>} atRules: An array of object of the following shape:
   *            - type {String}
   *            - conditionText {String}
   *            - matches {Boolean}: true if the media rule matches the current state of the document
   *            - layerName {String}
   *            - line {Number}
   *            - column {Number}
   */

  getStyleSheetRuleCountAndAtRules(styleSheet) {
    const resourceId = this.#findStyleSheetResourceId(styleSheet);
    if (!resourceId) {
      return [];
    }

    if (this.#mqlChangeAbortControllerMap.has(resourceId)) {
      this.#mqlChangeAbortControllerMap.get(resourceId).abort();
      this.#mqlChangeAbortControllerMap.delete(resourceId);
    }

    // Accessing the stylesheet associated window might be slow due to cross compartment
    // wrappers, so only retrieve it if it's needed.
    let win;
    const getStyleSheetAssociatedWindow = () => {
      if (!win) {
        win = styleSheet.associatedDocument?.ownerGlobal;
      }
      return win;
    };

    // This returns the following type of at-rules:
    // - CSSMediaRule
    // - CSSContainerRule
    // - CSSSupportsRule
    // - CSSLayerBlockRule
    // New types can be added from InpsectorUtils.cpp `CollectAtRules`
    const { atRules: styleSheetRules, ruleCount } =
      InspectorUtils.getStyleSheetRuleCountAndAtRules(styleSheet);
    const atRules = [];
    for (const rule of styleSheetRules) {
      const className = ChromeUtils.getClassName(rule);
      if (className === "CSSMediaRule") {
        let matches = false;

        try {
          const associatedWin = getStyleSheetAssociatedWindow();
          const mql = associatedWin.matchMedia(rule.media.mediaText);
          matches = mql.matches;

          let ac = this.#mqlChangeAbortControllerMap.get(resourceId);
          if (!ac) {
            ac = new associatedWin.AbortController();
            this.#mqlChangeAbortControllerMap.set(resourceId, ac);
          }

          const index = atRules.length;
          mql.addEventListener(
            "change",
            () => this.#onMatchesChange(resourceId, index, mql),
            {
              signal: ac.signal,
            }
          );
        } catch (e) {
          // Ignored
        }

        atRules.push({
          type: "media",
          conditionText: rule.conditionText,
          matches,
          line: InspectorUtils.getRelativeRuleLine(rule),
          column: InspectorUtils.getRuleColumn(rule),
        });
      } else if (className === "CSSContainerRule") {
        atRules.push({
          type: "container",
          conditionText: rule.conditionText,
          line: InspectorUtils.getRelativeRuleLine(rule),
          column: InspectorUtils.getRuleColumn(rule),
        });
      } else if (className === "CSSSupportsRule") {
        atRules.push({
          type: "support",
          conditionText: rule.conditionText,
          line: InspectorUtils.getRelativeRuleLine(rule),
          column: InspectorUtils.getRuleColumn(rule),
        });
      } else if (className === "CSSLayerBlockRule") {
        atRules.push({
          type: "layer",
          layerName: rule.name,
          line: InspectorUtils.getRelativeRuleLine(rule),
          column: InspectorUtils.getRuleColumn(rule),
        });
      } else if (className === "CSSPropertyRule") {
        atRules.push({
          type: "property",
          propertyName: rule.name,
          line: InspectorUtils.getRelativeRuleLine(rule),
          column: InspectorUtils.getRuleColumn(rule),
        });
      }
    }
    return {
      ruleCount,
      atRules,
    };
  }

  /**
   * Called when the status of a media query support changes (i.e. it now matches, or it
   * was matching but isn't anymore)
   *
   * @param {String} resourceId
   *        The id associated with the stylesheet
   * @param {Number} index
   *        The index of the media rule relatively to all the other at-rules of the stylesheet
   * @param {MediaQueryList} mql
   *        The result of matchMedia for the given media rule
   */

  #onMatchesChange(resourceId, index, mql) {
    this.#onStyleSheetUpdated({
      resourceId,
      updateKind: "matches-change",
      updates: {
        nestedResourceUpdates: [
          {
            path: ["atRules", index, "matches"],
            value: mql.matches,
          },
        ],
      },
    });
  }

  /**
   * Get the node href of a given stylesheet
   *
   * @param {StyleSheet} styleSheet
   * @returns {String}
   */

  getNodeHref(styleSheet) {
    const { ownerNode } = styleSheet;
    if (!ownerNode) {
      return null;
    }

    if (ownerNode.nodeType == ownerNode.DOCUMENT_NODE) {
      return ownerNode.location.href;
    }

    if (ownerNode.ownerDocument?.location) {
      return ownerNode.ownerDocument.location.href;
    }

    return null;
  }

  /**
   * Get the sourcemap base url of a given stylesheet
   *
   * @param {StyleSheet} styleSheet
   * @returns {String}
   */

  getSourcemapBaseURL(styleSheet) {
    // When the style is injected via nsIDOMWindowUtils.loadSheet, even
    // the parent style sheet has no owner, so default back to target actor
    // document
    const ownerNode = getStyleSheetOwnerNode(styleSheet);
    const ownerDocument = ownerNode
      ? ownerNode.ownerDocument
      : this.#targetActor.window;

    return getSourcemapBaseURL(
      // Technically resolveSourceURL should be used here alongside
      // "this.rawSheet.sourceURL", but the style inspector does not support
      // /*# sourceURL=*/ in CSS, so we're omitting it here (bug 880831).
      styleSheet.href || this.getNodeHref(styleSheet),
      ownerDocument
    );
  }

  /**
   * Get all the stylesheets for a given window
   *
   * @param {Window} window
   * @returns {Array<StyleSheet>}
   */

  async #getStyleSheetsForWindow(window) {
    const { document } = window;
    const documentOnly = !document.nodePrincipal.isSystemPrincipal;

    const styleSheets = [];

    for (const styleSheet of InspectorUtils.getAllStyleSheets(
      document,
      documentOnly
    )) {
      if (!this.#shouldListSheet(styleSheet)) {
        continue;
      }

      styleSheets.push(styleSheet);

      // Get all sheets, including imported ones
      const importedStyleSheets = await this.#getImportedStyleSheets(
        document,
        styleSheet
      );
      styleSheets.push(...importedStyleSheets);
    }

    return styleSheets;
  }

  /**
   * Returns true if a given stylesheet has an ancestor with the same url it has
   *
   * @param {StyleSheet} styleSheet
   * @returns {Boolean}
   */

  #haveAncestorWithSameURL(styleSheet) {
    const href = styleSheet.href;
    while (styleSheet.parentStyleSheet) {
      if (styleSheet.parentStyleSheet.href == href) {
        return true;
      }
      styleSheet = styleSheet.parentStyleSheet;
    }
    return false;
  }

  /**
   * Helper function called when a property changed in a given stylesheet
   *
   * @param {String} resourceId
   *        The id of the stylesheet the change occured in
   * @param {String} property
   *        The property that was changed
   * @param {String} value
   *        The value of the property
   */

  #notifyPropertyChanged(resourceId, property, value) {
    this.#onStyleSheetUpdated({
      resourceId,
      updateKind: "property-change",
      updates: { resourceUpdates: { [property]: value } },
    });
  }

  /**
   * Event handler that is called when the state of applicable of style sheet is changed.
   *
   * For now, StyleSheetApplicableStateChanged event will be called at following timings.
   * - Append <link> of stylesheet to document
   * - Append <style> to document
   * - Change disable attribute of stylesheet object
   * - Change disable attribute of <link> to false
   * - Stylesheet is constructed.
   * When appending <link>, <style> or changing `disabled` attribute to false,
   * `applicable` is passed as true. The other hand, when changing `disabled`
   * to true, this will be false.
   *
   * NOTE: StyleSheetApplicableStateChanged is _not_ called when removing the <link>/<style>,
   *       but a StyleSheetRemovedEvent is emitted in such case (see #onStyleSheetRemoved)
   *
   * @param {StyleSheetApplicableStateChangedEvent}
   *        The triggering event.
   */

  #onApplicableStateChanged = ({ applicable, stylesheet: styleSheet }) => {
    if (
      // Have interest in applicable stylesheet only.
      applicable &&
      styleSheet.associatedDocument &&
      (!this.#targetActor.ignoreSubFrames ||
        styleSheet.associatedDocument.ownerGlobal ===
          this.#targetActor.window) &&
      this.#shouldListSheet(styleSheet) &&
      !this.#haveAncestorWithSameURL(styleSheet)
    ) {
      this.#registerStyleSheet(styleSheet);
    }
  };

  /**
   * Event handler that is called when a style sheet is removed.
   *
   * @param {StyleSheetRemovedEvent}
   *        The triggering event.
   */

  #onStylesheetRemoved = event => {
    this.#unregisterStyleSheet(event.stylesheet);
  };

  /**
   * If the stylesheet isn't registered yet, this function will generate an associated
   * resourceId and call registered `onAvailable` listeners.
   *
   * @param {StyleSheet} styleSheet
   * @returns {String} the associated resourceId
   */

  #registerStyleSheet(styleSheet) {
    const existingResourceId = this.#findStyleSheetResourceId(styleSheet);
    // If the stylesheet is already registered, there's no need to notify about it again.
    if (existingResourceId) {
      return existingResourceId;
    }

    // It's important to prefix the resourceId with the target actorID so we can't have
    // duplicated resource ids when the client connects to multiple targets.
    const resourceId = `${this.#targetActor.actorID}:stylesheet:${this
      .#styleSheetCount++}`;
    this.#styleSheetMap.set(resourceId, styleSheet);

    const creationData = this.#styleSheetCreationData?.get(styleSheet);
    this.#styleSheetCreationData?.delete(styleSheet);

    const onAvailablePromises = [];
    for (const onAvailable of this.#watchListeners.onAvailable) {
      onAvailablePromises.push(
        onAvailable({
          resourceId,
          styleSheet,
          creationData,
        })
      );
    }

    // creationData exists if this stylesheet was created via `addStyleSheet`.
    if (creationData) {
      //  We resolve the promise once the watcher sent the resources to the client,
      // so `addStyleSheet` calls can be fullfilled.
      Promise.all(onAvailablePromises).then(() => creationData?.resolve());
    }
    return resourceId;
  }

  /**
   * If the stylesheet is registered, this function will call registered `onDestroyed`
   * listeners with the stylesheet resourceId.
   *
   * @param {StyleSheet} styleSheet
   */

  #unregisterStyleSheet(styleSheet) {
    const existingResourceId = this.#findStyleSheetResourceId(styleSheet);
    if (!existingResourceId) {
      return;
    }

    this.#styleSheetMap.delete(existingResourceId);
    this.#styleSheetCreationData?.delete(styleSheet);
    if (this.#mqlChangeAbortControllerMap.has(existingResourceId)) {
      this.#mqlChangeAbortControllerMap.get(existingResourceId).abort();
      this.#mqlChangeAbortControllerMap.delete(existingResourceId);
    }

    for (const onDestroyed of this.#watchListeners.onDestroyed) {
      onDestroyed({
        resourceId: existingResourceId,
      });
    }
  }

  #onStyleSheetUpdated(data) {
    this.emit("stylesheet-updated", data);

    for (const onUpdated of this.#watchListeners.onUpdated) {
      onUpdated(data);
    }
  }

  /**
   * Returns true if the passed styleSheet should be handled.
   *
   * @param {StyleSheet} styleSheet
   * @returns {Boolean}
   */

  #shouldListSheet(styleSheet) {
    const href = styleSheet.href?.toLowerCase();
    // FIXME(bug 1826538): Make accessiblecaret.css and similar UA-widget
    // sheets system sheets, then remove this special-case.
    if (
      href === "resource://content-accessible/accessiblecaret.css" ||
      (href === "resource://devtools-highlighter-styles/highlighters.css" &&
        this.#targetActor.sessionContext.type !== "all")
    ) {
      return false;
    }
    return true;
  }

  /**
   * The StyleSheetsManager instance is managed by the target, so this will be called when
   * the target gets destroyed.
   */

  destroy() {
    // Cleanup
    if (this.#abortController) {
      this.#abortController.abort();
    }
    if (this.#mqlChangeAbortControllerMap) {
      for (const ac of this.#mqlChangeAbortControllerMap.values()) {
        ac.abort();
      }
    }

    try {
      this.#unwatchStyleSheetChangeEvents();
    } catch (e) {
      console.error(
        "Error when destroying StyleSheet manager for",
        this.#targetActor,
        ": ",
        e
      );
    }

    this.#styleSheetMap.clear();
    this.#abortController = null;
    this.#mqlChangeAbortControllerMap = null;
    this.#styleSheetCreationData = null;
    this.#styleSheetMap = null;
    this.#targetActor = null;
    this.#watchListeners = null;
  }
}

module.exports = {
  StyleSheetsManager,
  UPDATE_GENERAL,
  UPDATE_PRESERVING_RULES,
};

Messung V0.5
C=93 H=98 G=95

¤ Dauer der Verarbeitung: 0.9 Sekunden  ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

Beweissystem der NASA

Beweissystem Isabelle

NIST Cobol Testsuite

Cephes Mathematical Library

Wiener Entwicklungsmethode

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.