css-registered-properties.js (8509B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /** 8 * @typedef InspectorCSSPropertyDefinition (see InspectorUtils.webidl) 9 * @type {object} 10 * @property {string} name 11 * @property {string} syntax 12 * @property {boolean} inherits 13 * @property {string} initialValue 14 * @property {boolean} fromJS - true if property was registered via CSS.registerProperty 15 */ 16 17 class CSSRegisteredPropertiesWatcher { 18 #abortController; 19 #onAvailable; 20 #onUpdated; 21 #onDestroyed; 22 #registeredPropertiesCache = new Map(); 23 #styleSheetsManager; 24 #targetActor; 25 26 /** 27 * Start watching for all registered CSS properties (@property/CSS.registerProperty) 28 * related to a given Target Actor. 29 * 30 * @param TargetActor targetActor 31 * The target actor from which we should observe css changes. 32 * @param Object options 33 * Dictionary object with following attributes: 34 * - onAvailable: mandatory function 35 * - onUpdated: mandatory function 36 * - onDestroyed: mandatory function 37 * This will be called for each resource. 38 */ 39 async watch(targetActor, { onAvailable, onUpdated, onDestroyed }) { 40 this.#targetActor = targetActor; 41 this.#onAvailable = onAvailable; 42 this.#onUpdated = onUpdated; 43 this.#onDestroyed = onDestroyed; 44 45 // Notify about existing properties 46 const registeredProperties = this.#getRegisteredProperties(); 47 for (const registeredProperty of registeredProperties) { 48 this.#registeredPropertiesCache.set( 49 registeredProperty.name, 50 registeredProperty 51 ); 52 } 53 54 this.#notifyResourcesAvailable(registeredProperties); 55 56 // Listen for new properties being registered via CSS.registerProperty 57 this.#abortController = new AbortController(); 58 const { signal } = this.#abortController; 59 this.#targetActor.chromeEventHandler.addEventListener( 60 "csscustompropertyregistered", 61 this.#onCssCustomPropertyRegistered, 62 { capture: true, signal } 63 ); 64 65 // Watch for stylesheets being added/modified or destroyed, but don't handle existing 66 // stylesheets, as we already have the existing properties from this.#getRegisteredProperties. 67 this.#styleSheetsManager = targetActor.getStyleSheetsManager(); 68 await this.#styleSheetsManager.watch({ 69 onAvailable: this.#refreshCacheAndNotify, 70 onUpdated: this.#refreshCacheAndNotify, 71 onDestroyed: this.#refreshCacheAndNotify, 72 ignoreExisting: true, 73 }); 74 } 75 76 /** 77 * Get all the registered properties for the target actor document. 78 * 79 * @returns Array<InspectorCSSPropertyDefinition> 80 */ 81 #getRegisteredProperties() { 82 return InspectorUtils.getCSSRegisteredProperties( 83 this.#targetActor.window.document 84 ); 85 } 86 87 /** 88 * Compute a resourceId from a given property definition 89 * 90 * @param {InspectorCSSPropertyDefinition} propertyDefinition 91 * @returns string 92 */ 93 #getRegisteredPropertyResourceId(propertyDefinition) { 94 return `${this.#targetActor.actorID}:css-registered-property:${ 95 propertyDefinition.name 96 }`; 97 } 98 99 /** 100 * Called when a stylesheet is added, removed or modified. 101 * This will retrieve the registered properties at this very moment, and notify 102 * about new, updated and removed registered properties. 103 */ 104 #refreshCacheAndNotify = async () => { 105 const registeredProperties = this.#getRegisteredProperties(); 106 const existingPropertiesNames = new Set( 107 this.#registeredPropertiesCache.keys() 108 ); 109 110 const added = []; 111 const updated = []; 112 const removed = []; 113 114 for (const registeredProperty of registeredProperties) { 115 // If the property isn't in the cache already, this is a new one. 116 if (!this.#registeredPropertiesCache.has(registeredProperty.name)) { 117 added.push(registeredProperty); 118 this.#registeredPropertiesCache.set( 119 registeredProperty.name, 120 registeredProperty 121 ); 122 continue; 123 } 124 125 // Removing existing property from the Set so we can then later get the properties 126 // that don't exist anymore. 127 existingPropertiesNames.delete(registeredProperty.name); 128 129 // The property already existed, so we need to check if its definition was modified 130 const cachedRegisteredProperty = this.#registeredPropertiesCache.get( 131 registeredProperty.name 132 ); 133 134 const resourceUpdates = {}; 135 let wasUpdated = false; 136 if (registeredProperty.syntax !== cachedRegisteredProperty.syntax) { 137 resourceUpdates.syntax = registeredProperty.syntax; 138 wasUpdated = true; 139 } 140 if (registeredProperty.inherits !== cachedRegisteredProperty.inherits) { 141 resourceUpdates.inherits = registeredProperty.inherits; 142 wasUpdated = true; 143 } 144 if ( 145 registeredProperty.initialValue !== 146 cachedRegisteredProperty.initialValue 147 ) { 148 resourceUpdates.initialValue = registeredProperty.initialValue; 149 wasUpdated = true; 150 } 151 152 if (wasUpdated === true) { 153 updated.push({ 154 registeredProperty, 155 resourceUpdates, 156 }); 157 this.#registeredPropertiesCache.set( 158 registeredProperty.name, 159 registeredProperty 160 ); 161 } 162 } 163 164 // If there are items left in the Set, it means they weren't processed in the for loop 165 // before, meaning they don't exist anymore. 166 for (const registeredPropertyName of existingPropertiesNames) { 167 removed.push(this.#registeredPropertiesCache.get(registeredPropertyName)); 168 this.#registeredPropertiesCache.delete(registeredPropertyName); 169 } 170 171 this.#notifyResourcesAvailable(added); 172 this.#notifyResourcesUpdated(updated); 173 this.#notifyResourcesDestroyed(removed); 174 }; 175 176 /** 177 * csscustompropertyregistered event listener callback (fired when a property 178 * is registered via CSS.registerProperty). 179 * 180 * @param {CSSCustomPropertyRegisteredEvent} event 181 */ 182 #onCssCustomPropertyRegistered = event => { 183 // Ignore event if property was registered from a global different from the target global. 184 if ( 185 this.#targetActor.ignoreSubFrames && 186 event.target.ownerGlobal !== this.#targetActor.window 187 ) { 188 return; 189 } 190 191 const registeredProperty = event.propertyDefinition; 192 this.#registeredPropertiesCache.set( 193 registeredProperty.name, 194 registeredProperty 195 ); 196 this.#notifyResourcesAvailable([registeredProperty]); 197 }; 198 199 /** 200 * @param {Array<InspectorCSSPropertyDefinition>} registeredProperties 201 */ 202 #notifyResourcesAvailable = registeredProperties => { 203 if (!registeredProperties.length) { 204 return; 205 } 206 207 for (const registeredProperty of registeredProperties) { 208 registeredProperty.resourceId = 209 this.#getRegisteredPropertyResourceId(registeredProperty); 210 } 211 this.#onAvailable(registeredProperties); 212 }; 213 214 /** 215 * @param {Array<object>} updates: Array of update object, which have the following properties: 216 * - {InspectorCSSPropertyDefinition} registeredProperty: The property definition 217 * of the updated property 218 * - {Object} resourceUpdates: An object containing all the fields that are 219 * modified for the registered property. 220 */ 221 #notifyResourcesUpdated = updates => { 222 if (!updates.length) { 223 return; 224 } 225 226 for (const update of updates) { 227 update.resourceId = this.#getRegisteredPropertyResourceId( 228 update.registeredProperty 229 ); 230 // We don't need to send the property definition 231 delete update.registeredProperty; 232 } 233 234 this.#onUpdated(updates); 235 }; 236 237 /** 238 * @param {Array<InspectorCSSPropertyDefinition>} registeredProperties 239 */ 240 #notifyResourcesDestroyed = registeredProperties => { 241 if (!registeredProperties.length) { 242 return; 243 } 244 245 this.#onDestroyed( 246 registeredProperties.map(registeredProperty => 247 this.#getRegisteredPropertyResourceId(registeredProperty) 248 ) 249 ); 250 }; 251 252 destroy() { 253 this.#styleSheetsManager.unwatch({ 254 onAvailable: this.#refreshCacheAndNotify, 255 onUpdated: this.#refreshCacheAndNotify, 256 onDestroyed: this.#refreshCacheAndNotify, 257 }); 258 259 this.#abortController.abort(); 260 } 261 } 262 263 module.exports = CSSRegisteredPropertiesWatcher;