-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathindex.js
167 lines (157 loc) · 6.57 KB
/
index.js
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
158
159
160
161
162
163
164
165
166
167
// @ts-check
/**
* Aynchronous initialization of the runtime DOM.
*
* @param {Options} options
* @return {Promise<ElementProcessor>}
*
* @typedef {{
* minify?: boolean // minify the output (default: true)
* parseCss: boolean // parse CSS (default: true)
* preflight?: boolean // enables CSS reset for descendants of the root element (default: true)
* root?: Element // the DOM element that will be scanned for windi classes (default: document.body)
* watch?: boolean // enable/disable watch mode, only applies to the root element (default: true)
* windiCssVersion?: string, // Windi CSS version (default: latest)
* config?: any // optional windicss config
* }} Options
*
* @typedef {(element: Element | DocumentFragment) => Promise<void>} ElementProcessor
*/
export default async (options) => {
/**
* The runtime configuration.
*/
const { minify, parseCss, preflight, root, watch, windiCssVersion, config } = Object.assign({
minify: false,
parseCss: true,
preflight: true,
root: document.body,
watch: true,
windiCssVersion: 'latest'
}, options);
/**
* Initialization of Windi CSS Processor and style element containing generated styles.
*/
const [{ default: Processor }, { CSSParser }] = await Promise.all([
import(/* @vite-ignore */ `https://esm.run/windicss@${windiCssVersion}`),
import(/* @vite-ignore */ `https://esm.run/windicss@${windiCssVersion}/utils/parser`)
]);
const processor = new Processor(config);
const styleSheet = processor.interpret().styleSheet; // workaround for new StyleSheet()
const styleElement = document.head.appendChild(document.createElement('style'));
const classes = new Set(), queuedClasses = new Set();
const tags = new Set(), queuedTags = new Set();
/**
* Processing elements and storing css classes and tag names for updating the generated styles.
* @type {(element: Element | DocumentFragment, recurse?: boolean) => void}
*/
const process = (element, recurse = true) => {
const descendants = recurse ? element.querySelectorAll('*') : [];
[element, ...descendants].forEach(elem => {
elem.classList.length > 0 && elem.classList.forEach(className => {
!classes.has(className) && queuedClasses.add(className);
});
if (preflight) {
const tagName = elem.tagName.toLowerCase();
!tags.has(tagName) && queuedTags.add(tagName);
}
});
};
/**
* A scheduler that requests an animation frame (fallback: setTimeout) to update styles.
*/
const scheduleUpdate = (() => {
const request = window.requestAnimationFrame || (callback => setTimeout(callback, 16.66));
let id = 0;
return () => !!(queuedClasses.size + queuedTags.size) && (id == 0) && (id = request(() => { update(); id = 0 }));
})();
/**
* Computes the style element content and updates the style element.
*/
const update = () => {
if (queuedClasses.size) {
const classString = [...queuedClasses].join(' ');
const classStyleSheet = processor.interpret(classString).styleSheet;
styleSheet.extend(classStyleSheet);
queuedClasses.forEach(c => classes.add(c));
queuedClasses.clear();
}
if (preflight && queuedTags.size) {
const html = [...queuedTags].map(t => `<${t}`).join(' ');
const preflightStyleSheet = processor.preflight(html);
styleSheet.extend(preflightStyleSheet);
queuedTags.forEach(t => tags.add(t));
queuedTags.clear();
}
styleSheet.sort();
styleElement.innerHTML = styleSheet.build(minify); // use .innerHTML because .textContent does not trigger a style update
};
/**
* Parses the CSS of all style elements and transforms directives.
*
* @param {Element | DocumentFragment} element
*/
const parseStyles = (element) => {
// querySelectorAll does not include template element contents
// because they are not visible in the DOM
element.querySelectorAll("style[lang='windify']").forEach(style => {
const cssParser = new CSSParser(style.textContent, processor);
style.innerHTML = cssParser.parse().build();
style.removeAttribute('lang');
});
};
/**
* Initially parse Windi CSS directives, compute the styles and install a mutation overserver.
*/
const init = () => {
// parse all directives like @apply
if (parseCss) {
parseStyles(document.documentElement);
}
// collect all classes and tags
process(root);
// update styles immediately in order to have correct styling when showing the root element
update();
// remove hidden attribute from html, body and root elements
[document.documentElement, document.body, root].forEach(elem => {
if (elem.hidden) {
elem.removeAttribute('hidden');
}
});
// install mutation observer
if (watch) {
// note: a mutation observer does not recognize changes to template content!
new MutationObserver(mutations => {
mutations.forEach(({ type, target, attributeName, addedNodes }) => {
// @ts-ignore (target is always an Element)
type === 'attributes' && attributeName === 'class' && process(target, false);
// @ts-ignore (node is always an Element)
type === 'childList' && addedNodes.forEach(node => node.nodeType === Node.ELEMENT_NODE && process(node));
});
scheduleUpdate();
}).observe(root, {
childList: true,
subtree: true,
attributeFilter: ['class'] // implicitely sets attributes to true
});
}
};
/**
* Entry point
*/
if (typeof window === 'undefined') {
console.warn('Windify cannot be used outside of a browser.');
return;
}
init();
return async (...elements) => {
elements.forEach(elem => {
parseCss && parseStyles(elem);
process(elem);
});
// We knowingly don't await the update of the styles. Subsequent calls to process()
// will not trigger an update if the styles are already up to date or if the styles
// are still being processed.
scheduleUpdate();
};
};