1(function () { 2 // Prevent multiple initializations if the script is accidentally loaded more than once 3 if (window.SfStyleWatcher) return; 4 5 const DATA_STYLE_ATTR = 'data-sf-style'; 6 const LAYOUT_CRITICAL_PROPS = { 7 width: true, 8 height: true, 9 'min-width': true, 10 'max-width': true, 11 'min-height': true, 12 'max-height': true, 13 top: true, 14 left: true, 15 right: true, 16 bottom: true, 17 display: true, 18 position: true, 19 transform: true 20 }; 21 22 /** 23 * Minimal microtask scheduler to batch multiple style writes into a single batch. 24 * queueMicrotask finishes before the browser does things like rendering the page Ensures no flicker. 25 */ 26 const queue = []; 27 let queued = false; // flag 28 /** 29 * Schedule a function to run in the next microtask batch. 30 * Multiple calls before the batch executes will queue multiple functions. 31 */ 32 function schedule(fn) { 33 queue.push(fn); 34 if (!queued) { 35 queued = true; 36 queueMicrotask(() => { 37 try { 38 // Execute all queued style updates in order 39 for (let i = 0; i < queue.length; i++) { 40 queue[i]() 41 } 42 } finally { 43 // Always reset the queue and flag 44 queue.length = 0; 45 queued = false; 46 } 47 }); 48 } 49 } 50 51 /** 52 * Flush queued style operations immediately. 53 * Useful when deterministic style reads are required. 54 */ 55 function flushScheduled() { 56 if (!queue.length) { 57 queued = false; 58 return; 59 } 60 61 const pending = queue.splice(0, queue.length); 62 queued = false; 63 for (let i = 0; i < pending.length; i++) { 64 pending[i](); 65 } 66 } 67 68 /** 69 * Parse a CSS style text into a property map. 70 * Handles !important priority and trims whitespace. 71 * 72 * @param {string} styleText - Raw style text 73 * @returns {Object<string, { value: string, priority: string }>} - Parsed styles 74 */ 75 function parseStyleText(styleText) { 76 const map = {}; 77 if (!styleText) return map; 78 const parts = styleText.split(';'); 79 for (let i = 0; i < parts.length; i++) { 80 const part = parts[i].trim(); 81 if (!part) continue; 82 const colonIndex = part.indexOf(':'); 83 if (colonIndex === -1) continue; 84 const prop = part.slice(0, colonIndex).trim(); 85 let value = part.slice(colonIndex + 1).trim(); 86 let priority = ''; 87 if (/!important$/i.test(value)) { 88 value = value.replace(/!important$/i, '').trim(); 89 priority = 'important'; 90 } 91 if (prop) { 92 map[prop] = { value, priority }; 93 } 94 } 95 return map; 96 } 97 98 /** 99 * Returns true when style map includes layout-critical properties. 100 * @param {Object<string, { value: string, priority: string }>} map - Parsed style map. 101 * @returns {boolean} - Whether immediate application is required. 102 */ 103 function hasLayoutCriticalProps(map) { 104 const keys = Object.keys(map); 105 for (let i = 0; i < keys.length; i++) { 106 if (LAYOUT_CRITICAL_PROPS[keys[i].toLowerCase()]) { 107 return true; 108 } 109 } 110 return false; 111 } 112 113 /** 114 * Apply inline styles from the data-sf-style attribute without overriding 115 * styles set by component scripts. Only properties present in data-sf-style 116 * are updated, and previously applied properties are removed when absent. 117 * 118 * Uses cached values (sfAppliedStyleText/sfAppliedStyleMap) to skip redundant writes. 119 * All actual DOM writes are deferred to the microtask scheduler to avoid flicker. 120 * 121 * JS-override detection: if component JS has changed a property's value since 122 * sf-utils last applied it (i.e. current inline value differs from cached value), 123 * that property is considered "JS-owned" and will not be overwritten or removed. 124 * This prevents sf-utils from fighting against component scripts (e.g. sf-dialog.js 125 * setting display:block on the overlay after sf-utils set display:none from 126 * data-sf-style). 127 * 128 * @param {Element} element - The DOM element to style 129 * @param {string|null} styleValue - The raw value of the data-sf-style attribute 130 */ 131 function applySfStyle(element, styleValue, forceSync) { 132 const newCss = styleValue || ''; 133 const prevCss = element.sfAppliedStyleText || ''; 134 135 // Skip if the style string hasn't changed 136 if (newCss === prevCss) return; 137 138 const newMap = parseStyleText(newCss); 139 const prevMap = element.sfAppliedStyleMap || {}; 140 const shouldApplySync = !!forceSync || hasLayoutCriticalProps(newMap) || hasLayoutCriticalProps(prevMap); 141 142 // Snapshot the current inline style map at the moment data-sf-style is processed 143 // (synchronous, before the microtask runs). This is the baseline for first-time 144 // properties (prevEntry === undefined): if a property's value changes between 145 // this snapshot and when the microtask executes, component JS set it in the 146 // interim and must not be overwritten. 147 const scheduleTimeMap = parseStyleText(element.style.cssText); 148 149 const applyStyles = () => { 150 // Remove properties that were previously applied but no longer present,
151 // but only if component JS has not overridden the value since we last set it. 152 Object.keys(prevMap).forEach(prop => { 153 if (!newMap.hasOwnProperty(prop)) { 154 const currentValue = element.style.getPropertyValue(prop); 155 // Safe to remove only when the value still matches what sf-utils last wrote. 156 // If it differs, JS has changed it â leave it alone. 157 if (currentValue === prevMap[prop].value) { 158 element.style.removeProperty(prop); 159 } 160 } 161 }); 162 163 // Set/update current properties from data-sf-style, 164 // but only if component JS has not overridden the value since data-sf-style was stamped. 165 Object.keys(newMap).forEach(prop => { 166 const entry = newMap[prop]; 167 const currentValue = element.style.getPropertyValue(prop); 168 const prevEntry = prevMap[prop]; 169 170 let jsOverrode; 171 if (prevEntry !== undefined) { 172 // sf-utils previously managed this property. 173 // JS-overridden if the current value is non-empty and differs from what sf-utils last wrote. 174 jsOverrode = currentValue !== '' && currentValue !== prevEntry.value; 175 } else if (element.sfAppliedStyleMap !== undefined) { 176 // sf-utils manages this element but has never seen this specific property before. 177 // Use the schedule-time snapshot as the baseline: if the value changed between 178 // snapshot time and microtask execution, component JS set it â respect it. 179 const scheduleTimeValue = scheduleTimeMap[prop] ? scheduleTimeMap[prop].value : ''; 180 jsOverrode = currentValue !== '' && currentValue !== scheduleTimeValue; 181 } else { 182 // sf-utils has NEVER managed this element at all (sfAppliedStyleMap is absent). 183 // Any pre-existing inline value must be treated as JS-owned unconditionally. 184 // The schedule-time snapshot cannot be used here: if JS set the value before 185 // applySfStyle was ever called (e.g. Gantt JS sets splitter height before 186 // Blazor re-stamps data-sf-style after a cache-clearing re-mount in WASM), 187 // scheduleTimeValue === currentValue, so the snapshot check would incorrectly 188 // report jsOverrode=false and overwrite the JS value with the Blazor default. 189 jsOverrode = currentValue !== ''; 190 } 191 192 if (!jsOverrode) { 193 element.style.setProperty(prop, entry.value, entry.priority); 194 } 195 }); 196 197 // Update the cache to reflect what is actually in the element now. 198 // For JS-overridden properties, record the live JS value so that future 199 // data-sf-style re-stamps can correctly detect further JS changes. 200 const updatedMap = {}; 201 Object.keys(newMap).forEach(prop => { 202 const currentValue = element.style.getPropertyValue(prop); 203 updatedMap[prop] = { 204 value: currentValue, 205 priority: element.style.getPropertyPriority(prop) 206 }; 207 }); 208 element.sfAppliedStyleText = newCss; 209 element.sfAppliedStyleMap = updatedMap; 210 211 if (element.hasAttribute('data-sf-style')) { 212 // Mark intentional removal so the observer does not call clearSfStyle 213 element.isDataAttrRemoved = true; 214 element.removeAttribute(DATA_STYLE_ATTR); 215 queueMicrotask(() => { delete element.isDataAttrRemoved; }); 216 } 217 }; 218 219 // Apply immediately for layout-critical updates to avoid stale reads (WASM/server parity). 220 // Keep batching for non-critical properties to reduce style churn and flicker. 221 if (shouldApplySync) { 222 applyStyles(); 223 } else {
224 schedule(applyStyles); 225 } 226 } 227 228 /** 229 * Clear inline styles that were previously applied by this script. 230 * Important: Only clears if we previously set something (checks cache). 231 * This prevents accidentally wiping user- or framework-defined inline styles. 232 * 233 * @param {Element} element - The DOM element to potentially clear 234 */ 235 function clearSfStyle(element, forceSync) { 236 // Only act if this script previously applied a style to this element 237 if (element.sfAppliedStyleMap !== undefined || element.sfAppliedStyleText !== undefined) { 238 const clearStyles = () => { 239 const prevMap = element.sfAppliedStyleMap || {}; 240 Object.keys(prevMap).forEach(prop => { 241 const currentValue = element.style.getPropertyValue(prop); 242 // Only remove if the value still matches what sf-utils last wrote. 243 // If JS changed it, leave it alone. 244 if (currentValue === prevMap[prop].value) { 245 element.style.removeProperty(prop); 246 } 247 }); 248 // Remove cache entries to indicate we no longer manage this element's styles 249 delete element.sfAppliedStyleText; 250 delete element.sfAppliedStyleMap; 251 }; 252 253 if (forceSync) { 254 clearStyles(); 255 } else { 256 schedule(clearStyles); 257 } 258 } 259 } 260 261 /** 262 * Patch native attribute APIs so data-sf-style writes become immediately observable. 263 * This removes timing differences between render modes when components read dimensions 264 * right after render. 265 */ 266 const nativeSetAttribute = Element.prototype.setAttribute; 267 const nativeRemoveAttribute = Element.prototype.removeAttribute; 268 Element.prototype.setAttribute = function (name, value) { 269 nativeSetAttribute.call(this, name, value); 270 if (name === DATA_STYLE_ATTR && this.nodeType === 1) { 271 applySfStyle(this, value, true); 272 } 273 }; 274 275 Element.prototype.removeAttribute = function (name) { 276 const hadDataStyle = name === DATA_STYLE_ATTR && this.hasAttribute(DATA_STYLE_ATTR); 277 nativeRemoveAttribute.call(this, name); 278 if (hadDataStyle && this.nodeType === 1 && !this.isDataAttrRemoved) { 279 clearSfStyle(this, true); 280 } 281 }; 282 283 /** 284 * Process a single element if it has data-sf-style. 285 */ 286 function processElement(element) { 287 if (!element || element.nodeType !== 1) return; 288 if (element.hasAttribute(DATA_STYLE_ATTR)) { 289 applySfStyle(element, element.getAttribute(DATA_STYLE_ATTR), true); 290 } 291 } 292 293 /** 294 * Recursively process a subtree for elements with data-sf-style attributes. 295 * 296 * Used both on initial load and when new nodes are added to the DOM. 297 * 298 * @param {Node} root - The root node of the subtree (usually an Element) 299 */ 300 function processTree(root) { 301 if (!root || root.nodeType !== 1) return; 302 // Process root itself 303 processElement(root); 304 // Process descendants 305 if (root.querySelectorAll) { 306 root.querySelectorAll('[data-sf-style]').forEach(processElement); 307 } 308 } 309 310 /** 311 * MutationObserver callback - reacts to DOM changes. 312 * 313 * Handles: 314 * New nodes being added (childList mutations) 315 * Changes to data-sf-style attribute 316 */ 317 const observer = new MutationObserver(mutationsList => { 318 for (const mutation of mutationsList) { 319 if (mutation.type === 'childList') { 320 // New nodes added 321 mutation.addedNodes.forEach(node => processTree(node)); 322 } else if (mutation.type === 'attributes') { 323 const element = mutation.target; 324 if (mutation.attributeName === DATA_STYLE_ATTR) { 325 const styleValue = element.getAttribute(DATA_STYLE_ATTR); 326 // Attribute removed 327 if (styleValue === null) { 328 // To ensure if its not called from removal of data attr we intentionally did 329 if (!element.isDataAttrRemoved) 330 clearSfStyle(element, true); 331 } else { 332 // Attribute changed/added 333 applySfStyle(element, styleValue, true); 334 } 335 } else if (mutation.attributeName === 'style') { 336 if (element.hasAttribute(DATA_STYLE_ATTR)) { 337 // data-sf-style still present: re-apply our styles to prevent visible flicker. 338 // JS-override detection inside applySfStyle guards individual properties. 339 applySfStyle(element, element.getAttribute(DATA_STYLE_ATTR), true); 340 } else if (element.sfAppliedStyleMap !== undefined) { 341 // data-sf-style already removed by sf-utils, but component JS just changed 342 // the inline style. Sync the cache to the current live values so that 343 // when Blazor re-renders and re-adds data-sf-style, applySfStyle correctly 344 // identifies JS-overridden properties and leaves them untouched. 345 const liveMap = {}; 346 Object.keys(element.sfAppliedStyleMap).forEac
346h(prop => { 347 liveMap[prop] = { 348 value: element.style.getPropertyValue(prop), 349 priority: element.style.getPropertyPriority(prop) 350 }; 351 }); 352 element.sfAppliedStyleMap = liveMap; 353 // Keep sfAppliedStyleText unchanged â it still reflects the last 354 // data-sf-style string so applySfStyle can detect a re-stamp. 355 } 356 } 357 } 358 } 359 }); 360 361 // Observe the entire document for subtree changes, new nodes, and specific attributes 362 observer.observe(document.documentElement, { 363 subtree: true, 364 childList: true, 365 attributes: true, 366 // Limited attribute monitoring to only what we need 367 attributeFilter: [DATA_STYLE_ATTR, 'style'] // narrow filter for performance 368 }); 369 370 // Process the existing DOM once at startup 371 processTree(document.documentElement); 372 373 /** 374 * Public API exposed on window.SfStyleWatcher 375 * 376 * Useful for manual refresh 377 */ 378 window.SfStyleWatcher = { 379 refresh(root = document.documentElement) { 380 processTree(root); 381 }, 382 flush() { 383 flushScheduled(); 384 }, 385 // Stop observing mutations 386 disconnect() { 387 observer.disconnect(); 388 } 389 }; 390})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.