summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/css-reload.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/shared/css-reload.js')
-rw-r--r--devtools/client/shared/css-reload.js142
1 files changed, 142 insertions, 0 deletions
diff --git a/devtools/client/shared/css-reload.js b/devtools/client/shared/css-reload.js
new file mode 100644
index 000000000..de82c6c5f
--- /dev/null
+++ b/devtools/client/shared/css-reload.js
@@ -0,0 +1,142 @@
+/* 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 { Services } = require("resource://gre/modules/Services.jsm");
+const { getTheme } = require("devtools/client/shared/theme");
+
+function iterStyleNodes(window, func) {
+ for (let node of window.document.childNodes) {
+ // Look for ProcessingInstruction nodes.
+ if (node.nodeType === 7) {
+ func(node);
+ }
+ }
+
+ const links = window.document.getElementsByTagNameNS(
+ "http://www.w3.org/1999/xhtml", "link"
+ );
+ for (let node of links) {
+ func(node);
+ }
+}
+
+function replaceCSS(window, fileURI) {
+ const document = window.document;
+ const randomKey = Math.random();
+ Services.obs.notifyObservers(null, "startupcache-invalidate", null);
+
+ // Scan every CSS tag and reload ones that match the file we are
+ // looking for.
+ iterStyleNodes(window, node => {
+ if (node.nodeType === 7) {
+ // xml-stylesheet declaration
+ if (node.data.includes(fileURI)) {
+ const newNode = window.document.createProcessingInstruction(
+ "xml-stylesheet",
+ `href="${fileURI}?s=${randomKey}" type="text/css"`
+ );
+ document.insertBefore(newNode, node);
+ document.removeChild(node);
+ }
+ } else if (node.href.includes(fileURI)) {
+ const parentNode = node.parentNode;
+ const newNode = window.document.createElementNS(
+ "http://www.w3.org/1999/xhtml",
+ "link"
+ );
+ newNode.rel = "stylesheet";
+ newNode.type = "text/css";
+ newNode.href = fileURI + "?s=" + randomKey;
+
+ parentNode.insertBefore(newNode, node);
+ parentNode.removeChild(node);
+ }
+ });
+}
+
+function _replaceResourceInSheet(sheet, filename, randomKey) {
+ for (let i = 0; i < sheet.cssRules.length; i++) {
+ const rule = sheet.cssRules[i];
+ if (rule.type === rule.IMPORT_RULE) {
+ _replaceResourceInSheet(rule.styleSheet, filename);
+ } else if (rule.cssText.includes(filename)) {
+ // Strip off any existing query strings. This might lose
+ // updates for files if there are multiple resources
+ // referenced in the same rule, but the chances of someone hot
+ // reloading multiple resources in the same rule is very low.
+ const text = rule.cssText.replace(/\?s=0.\d+/g, "");
+ const newRule = (
+ text.replace(filename, filename + "?s=" + randomKey)
+ );
+
+ sheet.deleteRule(i);
+ sheet.insertRule(newRule, i);
+ }
+ }
+}
+
+function replaceCSSResource(window, fileURI) {
+ const document = window.document;
+ const randomKey = Math.random();
+
+ // Only match the filename. False positives are much better than
+ // missing updates, as all that would happen is we reload more
+ // resources than we need. We do this because many resources only
+ // use relative paths.
+ const parts = fileURI.split("/");
+ const file = parts[parts.length - 1];
+
+ // Scan every single rule in the entire page for any reference to
+ // this resource, and re-insert the rule to force it to update.
+ for (let sheet of document.styleSheets) {
+ _replaceResourceInSheet(sheet, file, randomKey);
+ }
+
+ for (let node of document.querySelectorAll("img,image")) {
+ if (node.src.startsWith(fileURI)) {
+ node.src = fileURI + "?s=" + randomKey;
+ }
+ }
+}
+
+function watchCSS(window) {
+ if (Services.prefs.getBoolPref("devtools.loader.hotreload")) {
+ const watcher = require("devtools/client/shared/devtools-file-watcher");
+
+ function onFileChanged(_, relativePath) {
+ if (relativePath.match(/\.css$/)) {
+ if (relativePath.startsWith("client/themes")) {
+ let path = relativePath.replace(/^client\/themes\//, "");
+
+ // Special-case a few files that get imported from other CSS
+ // files. We just manually hot reload the parent CSS file.
+ if (path === "variables.css" || path === "toolbars.css" ||
+ path === "common.css" || path === "splitters.css") {
+ replaceCSS(window, "chrome://devtools/skin/" + getTheme() + "-theme.css");
+ } else {
+ replaceCSS(window, "chrome://devtools/skin/" + path);
+ }
+ return;
+ }
+
+ replaceCSS(
+ window,
+ "chrome://devtools/content/" + relativePath.replace(/^client\//, "")
+ );
+ replaceCSS(window, "resource://devtools/" + relativePath);
+ } else if (relativePath.match(/\.(svg|png)$/)) {
+ relativePath = relativePath.replace(/^client\/themes\//, "");
+ replaceCSSResource(window, "chrome://devtools/skin/" + relativePath);
+ }
+ }
+ watcher.on("file-changed", onFileChanged);
+
+ window.addEventListener("unload", () => {
+ watcher.off("file-changed", onFileChanged);
+ });
+ }
+}
+
+module.exports = { watchCSS };