import { DisposableStore, toDisposable, IDisposable } from '../common/lifecycle.js';
import { autorun, IObservable } from '../common/observable.js';
import { getWindows, sharedMutationObserver } from './dom.js';
import { mainWindow } from './window.js';
const globalStylesheets = new Map<HTMLStyleElement , Set<HTMLStyleElement >>();
export function isGlobalStylesheet(node: Node): boolean {
return globalStylesheets.has(node as HTMLStyleElement);
}
export function createStyleSheet2(): WrappedStyleElement {
return new WrappedStyleElement();
}
class WrappedStyleElement {
private _currentCssStyle = '';
private _styleSheet: HTMLStyleElement | undefined = undefined;
public setStyle(cssStyle: string): void {
if (cssStyle === this._currentCssStyle) {
return;
}
this._currentCssStyle = cssStyle;
if (!this._styleSheet) {
this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.textContent = cssStyle);
} else {
this._styleSheet.textContent = cssStyle;
}
}
public dispose(): void {
if (this._styleSheet) {
this._styleSheet.remove();
this._styleSheet = undefined;
}
}
}
export function createStyleSheet(container: HTMLElement = mainWindow.document.head, beforeAppend?: (style: HTMLStyleElement) => void, disposableStore?: DisposableStore): HTMLStyleElement {
const style = document.createElement('style');
style.type = 'text/css';
style.media = 'screen';
beforeAppend?.(style);
container.appendChild(style);
if (disposableStore) {
disposableStore.add(toDisposable(() => style.remove()));
}
if (container === mainWindow.document.head) {
const globalStylesheetClones = new Set<HTMLStyleElement>();
globalStylesheets.set(style, globalStylesheetClones);
if (disposableStore) {
disposableStore.add(toDisposable(() => globalStylesheets.delete(style)));
}
for (const { window: targetWindow, disposables } of getWindows()) {
if (targetWindow === mainWindow) {
continue;
}
const cloneDisposable = disposables.add(cloneGlobalStyleSheet(style, globalStylesheetClones, targetWindow));
disposableStore?.add(cloneDisposable);
}
}
return style;
}
export function cloneGlobalStylesheets(targetWindow: Window): IDisposable {
const disposables = new DisposableStore();
for (const [globalStylesheet, clonedGlobalStylesheets] of globalStylesheets) {
disposables.add(cloneGlobalStyleSheet(globalStylesheet, clonedGlobalStylesheets, targetWindow));
}
return disposables;
}
function cloneGlobalStyleSheet(globalStylesheet: HTMLStyleElement, globalStylesheetClones: Set<HTMLStyleElement>, targetWindow: Window): IDisposable {
const disposables = new DisposableStore();
const clone = globalStylesheet.cloneNode(true) as HTMLStyleElement;
targetWindow.document.head.appendChild(clone);
disposables.add(toDisposable(() => clone.remove()));
for (const rule of getDynamicStyleSheetRules(globalStylesheet)) {
clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length);
}
disposables.add(sharedMutationObserver.observe(globalStylesheet, disposables, { childList: true })(() => {
clone.textContent = globalStylesheet.textContent;
}));
globalStylesheetClones.add(clone);
disposables.add(toDisposable(() => globalStylesheetClones.delete(clone)));
return disposables;
}
let _sharedStyleSheet: HTMLStyleElement | null = null;
function getSharedStyleSheet(): HTMLStyleElement {
if (!_sharedStyleSheet) {
_sharedStyleSheet = createStyleSheet();
}
return _sharedStyleSheet;
}
function getDynamicStyleSheetRules(style: HTMLStyleElement) {
if (style?.sheet?.rules) {
return style.sheet.rules;
}
if (style?.sheet?.cssRules) {
return style.sheet.cssRules;
}
return [];
}
export function createCSSRule(selector: string, cssText: string, style = getSharedStyleSheet()): void {
if (!style || !cssText) {
return;
}
style.sheet?.insertRule(`${selector} {${cssText}}`, 0);
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
createCSSRule(selector, cssText, clonedGlobalStylesheet);
}
}
export function removeCSSRulesContainingSelector(ruleName: string, style = getSharedStyleSheet()): void {
if (!style) {
return;
}
const rules = getDynamicStyleSheetRules(style);
const toDelete: number[] = [];
for (let i = 0; i < rules.length; i++) {
const rule = rules[i];
if (isCSSStyleRule(rule) && rule.selectorText.indexOf(ruleName) !== -1) {
toDelete.push(i);
}
}
for (let i = toDelete.length - 1; i >= 0; i--) {
style.sheet?.deleteRule(toDelete[i]);
}
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet);
}
}
function isCSSStyleRule(rule: CSSRule): rule is CSSStyleRule {
return typeof (rule as CSSStyleRule).selectorText === 'string';
}
export function createStyleSheetFromObservable(css: IObservable<string>): IDisposable {
const store = new DisposableStore();
const w = store.add(createStyleSheet2());
store.add(autorun(reader => {
w.setStyle(css.read(reader));
}));
return store;
}