style-injector.js 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. 'use strict';
  2. /** @type {function(opts):StyleInjector} */
  3. window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
  4. compare,
  5. onUpdate = () => {},
  6. }) => {
  7. const PREFIX = 'stylus-';
  8. const PATCH_ID = 'transition-patch';
  9. // styles are out of order if any of these elements is injected between them
  10. const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
  11. const docRewriteObserver = RewriteObserver(_sort);
  12. const docRootObserver = RootObserver(_sortIfNeeded);
  13. const list = [];
  14. const table = new Map();
  15. let isEnabled = true;
  16. let isTransitionPatched;
  17. // will store the original method refs because the page can override them
  18. let creationDoc, createElement, createElementNS;
  19. return /** @namespace StyleInjector */ {
  20. list,
  21. async apply(styleMap) {
  22. const styles = _styleMapToArray(styleMap);
  23. const value = !styles.length
  24. ? []
  25. : await docRootObserver.evade(() => {
  26. if (!isTransitionPatched && isEnabled) {
  27. _applyTransitionPatch(styles);
  28. }
  29. return styles.map(_addUpdate);
  30. });
  31. _emitUpdate();
  32. return value;
  33. },
  34. clear() {
  35. _addRemoveElements(false);
  36. list.length = 0;
  37. table.clear();
  38. _emitUpdate();
  39. },
  40. clearOrphans() {
  41. for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) {
  42. const id = el.id.slice(PREFIX.length);
  43. if (/^\d+$/.test(id) || id === PATCH_ID) {
  44. el.remove();
  45. }
  46. }
  47. },
  48. remove(id) {
  49. _remove(id);
  50. _emitUpdate();
  51. },
  52. replace(styleMap) {
  53. const styles = _styleMapToArray(styleMap);
  54. const added = new Set(styles.map(s => s.id));
  55. const removed = [];
  56. for (const style of list) {
  57. if (!added.has(style.id)) {
  58. removed.push(style.id);
  59. }
  60. }
  61. styles.forEach(_addUpdate);
  62. removed.forEach(_remove);
  63. _emitUpdate();
  64. },
  65. toggle(enable) {
  66. if (isEnabled === enable) return;
  67. isEnabled = enable;
  68. if (!enable) _toggleObservers(false);
  69. _addRemoveElements(enable);
  70. if (enable) _toggleObservers(true);
  71. },
  72. };
  73. function _add(style) {
  74. const el = style.el = _createStyle(style.id, style.code);
  75. const i = list.findIndex(item => compare(item, style) > 0);
  76. table.set(style.id, style);
  77. if (isEnabled) {
  78. document.documentElement.insertBefore(el, i < 0 ? null : list[i].el);
  79. }
  80. list.splice(i < 0 ? list.length : i, 0, style);
  81. return el;
  82. }
  83. function _addRemoveElements(add) {
  84. for (const {el} of list) {
  85. if (add) {
  86. document.documentElement.appendChild(el);
  87. } else {
  88. el.remove();
  89. }
  90. }
  91. }
  92. function _addUpdate(style) {
  93. return table.has(style.id) ? _update(style) : _add(style);
  94. }
  95. function _applyTransitionPatch(styles) {
  96. isTransitionPatched = true;
  97. // CSS transition bug workaround: since we insert styles asynchronously,
  98. // the browsers, especially Firefox, may apply all transitions on page load
  99. if (document.readyState === 'complete' ||
  100. document.visibilityState === 'hidden' ||
  101. !styles.some(s => s.code.includes('transition'))) {
  102. return;
  103. }
  104. const el = _createStyle(PATCH_ID, `
  105. :root:not(#\\0):not(#\\0) * {
  106. transition: none !important;
  107. }
  108. `);
  109. document.documentElement.appendChild(el);
  110. // wait for the next paint to complete
  111. // note: requestAnimationFrame won't fire in inactive tabs
  112. requestAnimationFrame(() => setTimeout(() => el.remove()));
  113. }
  114. function _createStyle(id, code = '') {
  115. if (!creationDoc) _initCreationDoc();
  116. let el;
  117. if (document.documentElement instanceof SVGSVGElement) {
  118. // SVG document style
  119. el = createElementNS.call(creationDoc, 'http://www.w3.org/2000/svg', 'style');
  120. } else if (document instanceof XMLDocument) {
  121. // XML document style
  122. el = createElementNS.call(creationDoc, 'http://www.w3.org/1999/xhtml', 'style');
  123. } else {
  124. // HTML document style; also works on HTML-embedded SVG
  125. el = createElement.call(creationDoc, 'style');
  126. }
  127. if (id) {
  128. el.id = `${PREFIX}${id}`;
  129. const oldEl = document.getElementById(el.id);
  130. if (oldEl) oldEl.id += '-superseded-by-Stylus';
  131. }
  132. el.type = 'text/css';
  133. // SVG className is not a string, but an instance of SVGAnimatedString
  134. el.classList.add('stylus');
  135. el.textContent = code;
  136. return el;
  137. }
  138. function _toggleObservers(shouldStart) {
  139. const onOff = shouldStart && isEnabled ? 'start' : 'stop';
  140. docRewriteObserver[onOff]();
  141. docRootObserver[onOff]();
  142. }
  143. function _emitUpdate() {
  144. _toggleObservers(list.length);
  145. onUpdate();
  146. }
  147. /*
  148. FF59+ workaround: allow the page to read our sheets, https://github.com/openstyles/stylus/issues/461
  149. First we're trying the page context document where inline styles may be forbidden by CSP
  150. https://bugzilla.mozilla.org/show_bug.cgi?id=1579345#c3
  151. and since userAgent.navigator can be spoofed via about:config or devtools,
  152. we're checking for getPreventDefault that was removed in FF59
  153. */
  154. function _initCreationDoc() {
  155. creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
  156. if (creationDoc) {
  157. ({createElement, createElementNS} = creationDoc);
  158. const el = document.documentElement.appendChild(_createStyle());
  159. const isApplied = el.sheet;
  160. el.remove();
  161. if (isApplied) return;
  162. }
  163. creationDoc = document;
  164. ({createElement, createElementNS} = document);
  165. }
  166. function _remove(id) {
  167. const style = table.get(id);
  168. if (!style) return;
  169. table.delete(id);
  170. list.splice(list.indexOf(style), 1);
  171. style.el.remove();
  172. }
  173. function _sort() {
  174. docRootObserver.evade(() => {
  175. list.sort(compare);
  176. _addRemoveElements(true);
  177. });
  178. }
  179. function _sortIfNeeded() {
  180. let needsSort;
  181. let el = list.length && list[0].el;
  182. if (!el) {
  183. needsSort = false;
  184. } else if (el.parentNode !== creationDoc.documentElement) {
  185. needsSort = true;
  186. } else {
  187. let i = 0;
  188. while (el) {
  189. if (i < list.length && el === list[i].el) {
  190. i++;
  191. } else if (ORDERED_TAGS.has(el.localName)) {
  192. needsSort = true;
  193. break;
  194. }
  195. el = el.nextElementSibling;
  196. }
  197. // some styles are not injected to the document
  198. if (i < list.length) needsSort = true;
  199. }
  200. if (needsSort) _sort();
  201. return needsSort;
  202. }
  203. function _styleMapToArray(styleMap) {
  204. return Object.values(styleMap).map(s => ({
  205. id: s.id,
  206. code: s.code.join(''),
  207. }));
  208. }
  209. function _update({id, code}) {
  210. const style = table.get(id);
  211. if (style.code !== code) {
  212. style.code = code;
  213. style.el.textContent = code;
  214. }
  215. }
  216. function RewriteObserver(onChange) {
  217. // detect documentElement being rewritten from inside the script
  218. let root;
  219. let observing = false;
  220. let timer;
  221. const observer = new MutationObserver(_check);
  222. return {start, stop};
  223. function start() {
  224. if (observing) return;
  225. // detect dynamic iframes rewritten after creation by the embedder i.e. externally
  226. root = document.documentElement;
  227. timer = setTimeout(_check);
  228. observer.observe(document, {childList: true});
  229. observing = true;
  230. }
  231. function stop() {
  232. if (!observing) return;
  233. clearTimeout(timer);
  234. observer.disconnect();
  235. observing = false;
  236. }
  237. function _check() {
  238. if (root !== document.documentElement) {
  239. root = document.documentElement;
  240. onChange();
  241. }
  242. }
  243. }
  244. function RootObserver(onChange) {
  245. let digest = 0;
  246. let lastCalledTime = NaN;
  247. let observing = false;
  248. const observer = new MutationObserver(() => {
  249. if (digest) {
  250. if (performance.now() - lastCalledTime > 1000) {
  251. digest = 0;
  252. } else if (digest > 5) {
  253. throw new Error('The page keeps generating mutations. Skip the event.');
  254. }
  255. }
  256. if (onChange()) {
  257. digest++;
  258. lastCalledTime = performance.now();
  259. }
  260. });
  261. return {evade, start, stop};
  262. function evade(fn) {
  263. const restore = observing && start;
  264. stop();
  265. return new Promise(resolve => _run(fn, resolve, _waitForRoot))
  266. .then(restore);
  267. }
  268. function start() {
  269. if (observing) return;
  270. observer.observe(document.documentElement, {childList: true});
  271. observing = true;
  272. }
  273. function stop() {
  274. if (!observing) return;
  275. // FIXME: do we need this?
  276. observer.takeRecords();
  277. observer.disconnect();
  278. observing = false;
  279. }
  280. function _run(fn, resolve, wait) {
  281. if (document.documentElement) {
  282. resolve(fn());
  283. return true;
  284. }
  285. if (wait) wait(fn, resolve);
  286. }
  287. function _waitForRoot(...args) {
  288. new MutationObserver((_, observer) => _run(...args) && observer.disconnect())
  289. .observe(document, {childList: true});
  290. }
  291. }
  292. };