-
Notifications
You must be signed in to change notification settings - Fork 0
/
createCustomStyleSheet.ts
157 lines (134 loc) · 4.15 KB
/
createCustomStyleSheet.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
import { Events, App as ObsidianApp, Plugin } from 'obsidian';
import getFloatingWindows from './getFloatingWindows';
const Counter = Symbol('CustomStylesheet count');
export type CustomStyleSheet = {
(): void;
get css(): string;
set css(text: string);
setAttribute(attr: string, value: string): void;
removeAttribute(attr: string): void;
is(el: HTMLElement): boolean;
};
function foreachWindow(app: ObsidianApp, fn: (document: Document, isFloating: boolean) => void) {
fn(app.workspace.containerEl.doc, true);
for (const float of getFloatingWindows(app)) {
fn(float.document, false);
}
}
/**
* Creates a custom stylesheet that is applied at runtime.
* It will apply to all floating windows and is automatically removed when your plugin is unloaded.
*
* @param app The app instance.
* @param plugin Your plugin instance.
*
* @returns A controller to manipulate the style sheet and its associated `<style>` element.
*/
export default function createCustomStyleSheet(app: ObsidianApp, plugin: Plugin) {
let result: CustomStyleSheet;
const pl = plugin as Plugin & { [Counter]?: number };
const plId = plugin.manifest.id;
const ssId = (pl[Counter] ?? (pl[Counter] = 0)).toString();
pl[Counter]++;
const styleEl: HTMLStyleElement = app.workspace.containerEl.doc.createElement('style');
const styleElInFloats: HTMLStyleElement[] = [];
// Set attributes for the style element.
styleEl.setAttribute('data-source-plugin', plId);
styleEl.setAttribute('data-source-id', ssId);
// Functions.
function unapply() {
styleElInFloats.splice(0, styleElInFloats.length).forEach((el) => el.remove());
styleEl.detach();
foreachWindow(app, (doc) => {
for (const styleEl of Array.from(doc.head.querySelectorAll('style'))) {
if (result.is(styleEl)) {
styleEl.remove();
}
}
});
}
function reapply() {
unapply();
foreachWindow(app, (doc, isFloating) => {
// Get the last style element.
let lastEl = doc.head.lastElementChild;
for (let el = lastEl; el != null; el = el.previousElementSibling) {
lastEl = el;
if (lastEl.tagName === 'STYLE') {
break;
}
}
// Insert the stylesheet after it.
if (!isFloating) {
lastEl?.insertAdjacentElement('afterend', styleEl);
return;
}
// If it's a floating window, insert a clone.
const styleElClone = styleEl.cloneNode(true) as HTMLStyleElement;
styleElInFloats.push(styleElClone);
lastEl?.insertAdjacentElement('afterend', styleElClone);
});
}
// Start listening for changes that might affect the `<style>` elements in the document head.
app.workspace.on('css-change', reapply);
app.workspace.on('layout-change', reapply);
// Create the custom stylesheet object.
result = Object.freeze(Object.defineProperties(
() => {
unapply();
app.workspace.off('css-change', reapply);
app.workspace.off('layout-change', reapply);
},
{
css: {
enumerable: true,
configurable: false,
get() {
return styleEl.textContent;
},
set(v) {
styleEl.textContent = v;
for (const styleEl of styleElInFloats) {
styleEl.textContent = v;
}
},
},
is: {
enumerable: false,
configurable: false,
value: (el: HTMLElement) => {
return el.getAttribute('data-source-plugin') === plId && el.getAttribute('data-source-id') === ssId;
},
},
setAttribute: {
enumerable: false,
configurable: false,
value: (attr: string, value: string) => {
if (attr === 'data-source-id' || attr === 'data-source-plugin') {
throw new Error(`Cannot change attribute '${attr}' on custom style sheet.`);
}
styleEl.setAttribute(attr, value);
for (const styleEl of styleElInFloats) {
styleEl.setAttribute(attr, value);
}
},
},
removeAttribute: {
enumerable: false,
configurable: false,
value: (attr: string) => {
if (attr === 'data-source-id' || attr === 'data-source-plugin') {
throw new Error(`Cannot remove attribute '${attr}' from custom style sheet.`);
}
styleEl.removeAttribute(attr);
for (const styleEl of styleElInFloats) {
styleEl.removeAttribute(attr);
}
},
},
},
) as CustomStyleSheet);
// Apply.
reapply();
return result;
}