PageStyleChild.sys.mjs (6008B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 export class PageStyleChild extends JSWindowActorChild { 6 actorCreated() { 7 // C++ can create the actor and call us here once an "interesting" link 8 // element gets added to the DOM. If pageload hasn't finished yet, just 9 // wait for that by doing nothing; the actor registration event 10 // listeners will ensure we get the pageshow event. 11 // It is also possible we get created in response to the parent 12 // sending us a message - in that case, it's still worth doing the 13 // same things here: 14 if (!this.browsingContext || !this.browsingContext.associatedWindow) { 15 return; 16 } 17 let { document } = this.browsingContext.associatedWindow; 18 if (document.readyState != "complete") { 19 return; 20 } 21 // If we've already seen a pageshow, send stylesheets now: 22 this.#collectAndSendSheets(); 23 } 24 25 handleEvent(event) { 26 if (event?.type != "pageshow") { 27 throw new Error("Unexpected event!"); 28 } 29 30 // On page show, tell the parent all of the stylesheets this document 31 // has. If we are in the topmost browsing context, delete the stylesheets 32 // from the previous page. 33 if (this.browsingContext.top === this.browsingContext) { 34 this.sendAsyncMessage("PageStyle:Clear"); 35 } 36 37 this.#collectAndSendSheets(); 38 } 39 40 receiveMessage(msg) { 41 switch (msg.name) { 42 // Sent when the page's enabled style sheet is changed. 43 case "PageStyle:Switch": 44 if (this.browsingContext.top == this.browsingContext) { 45 this.browsingContext.authorStyleDisabledDefault = false; 46 } 47 this.docShell.docViewer.authorStyleDisabled = false; 48 this._switchStylesheet(msg.data.title); 49 break; 50 // Sent when "No Style" is chosen. 51 case "PageStyle:Disable": 52 if (this.browsingContext.top == this.browsingContext) { 53 this.browsingContext.authorStyleDisabledDefault = true; 54 } 55 this.docShell.docViewer.authorStyleDisabled = true; 56 break; 57 } 58 } 59 60 /** 61 * Returns links that would represent stylesheets once loaded. 62 */ 63 _collectLinks(document) { 64 let result = []; 65 for (let link of document.querySelectorAll("link")) { 66 if (link.namespaceURI !== "http://www.w3.org/1999/xhtml") { 67 continue; 68 } 69 let isStyleSheet = Array.from(link.relList).some( 70 r => r.toLowerCase() == "stylesheet" 71 ); 72 if (!isStyleSheet) { 73 continue; 74 } 75 if (!link.href) { 76 continue; 77 } 78 result.push(link); 79 } 80 return result; 81 } 82 83 /** 84 * Switch the stylesheet so that only the sheet with the given title is enabled. 85 */ 86 _switchStylesheet(title) { 87 let document = this.document; 88 let docStyleSheets = Array.from(document.styleSheets); 89 let links; 90 91 // Does this doc contain a stylesheet with this title? 92 // If not, it's a subframe's stylesheet that's being changed, 93 // so no need to disable stylesheets here. 94 let docContainsStyleSheet = !title; 95 if (title) { 96 links = this._collectLinks(document); 97 docContainsStyleSheet = 98 docStyleSheets.some(sheet => sheet.title == title) || 99 links.some(link => link.title == title); 100 } 101 102 for (let sheet of docStyleSheets) { 103 if (sheet.title) { 104 if (docContainsStyleSheet) { 105 sheet.disabled = sheet.title !== title; 106 } 107 } else if (sheet.disabled) { 108 sheet.disabled = false; 109 } 110 } 111 112 // If there's no title, we just need to disable potentially-enabled 113 // stylesheets via document.styleSheets, so no need to deal with links 114 // there. 115 // 116 // We don't want to enable <link rel="stylesheet" disabled> without title 117 // that were not enabled before. 118 if (title) { 119 for (let link of links) { 120 if (link.title == title && link.disabled) { 121 link.disabled = false; 122 } 123 } 124 } 125 } 126 127 #collectAndSendSheets() { 128 let window = this.browsingContext.associatedWindow; 129 window.requestIdleCallback(() => { 130 if (!window || window.closed) { 131 return; 132 } 133 let filteredStyleSheets = this.#collectStyleSheets(window); 134 this.sendAsyncMessage("PageStyle:Add", { 135 filteredStyleSheets, 136 preferredStyleSheetSet: this.document.preferredStyleSheetSet, 137 }); 138 }); 139 } 140 141 /** 142 * Get the stylesheets that have a title (and thus can be switched) in this 143 * webpage. 144 * 145 * @param content The window object for the page. 146 */ 147 #collectStyleSheets(content) { 148 let result = []; 149 let document = content.document; 150 151 for (let sheet of document.styleSheets) { 152 let title = sheet.title; 153 if (!title) { 154 // Sheets without a title are not alternates. 155 continue; 156 } 157 158 // Skip any stylesheets that don't match the screen media type. 159 let media = sheet.media.mediaText; 160 if (media && !content.matchMedia(media).matches) { 161 continue; 162 } 163 164 // We skip links here, see below. 165 if ( 166 sheet.href && 167 sheet.ownerNode && 168 sheet.ownerNode.nodeName.toLowerCase() == "link" 169 ) { 170 continue; 171 } 172 173 let disabled = sheet.disabled; 174 result.push({ title, disabled }); 175 } 176 177 // This is tricky, because we can't just rely on document.styleSheets, as 178 // `<link disabled>` makes the sheet don't appear there at all. 179 for (let link of this._collectLinks(document)) { 180 let title = link.title; 181 if (!title) { 182 continue; 183 } 184 185 let media = link.media; 186 if (media && !content.matchMedia(media).matches) { 187 continue; 188 } 189 190 let disabled = 191 link.disabled || 192 !!link.sheet?.disabled || 193 document.preferredStyleSheetSet != title; 194 result.push({ title, disabled }); 195 } 196 197 return result; 198 } 199 }