single-file-hooks-frames.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. (function () {
  2. 'use strict';
  3. /*
  4. * Copyright 2010-2022 Gildas Lormeau
  5. * contact : gildas.lormeau <at> gmail.com
  6. *
  7. * This file is part of SingleFile.
  8. *
  9. * The code in this file is free software: you can redistribute it and/or
  10. * modify it under the terms of the GNU Affero General Public License
  11. * (GNU AGPL) as published by the Free Software Foundation, either version 3
  12. * of the License, or (at your option) any later version.
  13. *
  14. * The code in this file is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  17. * General Public License for more details.
  18. *
  19. * As additional permission under GNU AGPL version 3 section 7, you may
  20. * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
  21. * AGPL normally required by section 4, provided you include this license
  22. * notice and a URL through which recipients can access the Corresponding
  23. * Source.
  24. */
  25. /* global globalThis, window */
  26. (globalThis => {
  27. const LOAD_DEFERRED_IMAGES_START_EVENT = "single-file-load-deferred-images-start";
  28. const LOAD_DEFERRED_IMAGES_END_EVENT = "single-file-load-deferred-images-end";
  29. const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT = "single-file-load-deferred-images-keep-zoom-level-start";
  30. const LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT = "single-file-load-deferred-images-keep-zoom-level-end";
  31. const LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_EVENT = "single-file-load-deferred-images-keep-zoom-level-reset";
  32. const LOAD_DEFERRED_IMAGES_RESET_EVENT = "single-file-load-deferred-images-reset";
  33. const BLOCK_COOKIES_START_EVENT = "single-file-block-cookies-start";
  34. const BLOCK_COOKIES_END_EVENT = "single-file-block-cookies-end";
  35. const BLOCK_STORAGE_START_EVENT = "single-file-block-storage-start";
  36. const BLOCK_STORAGE_END_EVENT = "single-file-block-storage-end";
  37. const DISPATCH_SCROLL_START_EVENT = "single-file-dispatch-scroll-event-start";
  38. const DISPATCH_SCROLL_END_EVENT = "single-file-dispatch-scroll-event-end";
  39. const LAZY_LOAD_ATTRIBUTE = "single-file-lazy-load";
  40. const LOAD_IMAGE_EVENT = "single-file-load-image";
  41. const IMAGE_LOADED_EVENT = "single-file-image-loaded";
  42. const FETCH_REQUEST_EVENT = "single-file-request-fetch";
  43. const FETCH_ACK_EVENT = "single-file-ack-fetch";
  44. const FETCH_RESPONSE_EVENT = "single-file-response-fetch";
  45. const GET_ADOPTED_STYLESHEETS_REQUEST_EVENT = "single-file-request-get-adopted-stylesheets";
  46. const UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT = "single-file-unregister-request-get-adopted-stylesheets";
  47. const GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT = "single-file-response-get-adopted-stylesheets";
  48. const NEW_FONT_FACE_EVENT = "single-file-new-font-face";
  49. const DELETE_FONT_EVENT = "single-file-delete-font";
  50. const CLEAR_FONTS_EVENT = "single-file-clear-fonts";
  51. const FONT_STYLE_PROPERTIES = {
  52. family: "font-family",
  53. style: "font-style",
  54. weight: "font-weight",
  55. stretch: "font-stretch",
  56. unicodeRange: "unicode-range",
  57. variant: "font-variant",
  58. featureSettings: "font-feature-settings"
  59. };
  60. const fetch = (url, options) => globalThis.fetch(url, options);
  61. const CustomEvent = globalThis.CustomEvent;
  62. const document = globalThis.document;
  63. const screen = globalThis.screen;
  64. const Element = globalThis.Element;
  65. const UIEvent = globalThis.UIEvent;
  66. const Event = globalThis.Event;
  67. const FileReader = globalThis.FileReader;
  68. const Blob = globalThis.Blob;
  69. const JSON = globalThis.JSON;
  70. const MutationObserver = globalThis.MutationObserver;
  71. const observers = new Map();
  72. const observedElements = new Map();
  73. let dispatchScrollEvent;
  74. init();
  75. new MutationObserver(init).observe(document, { childList: true });
  76. function init() {
  77. document.addEventListener(LOAD_DEFERRED_IMAGES_START_EVENT, () => loadDeferredImagesStart());
  78. document.addEventListener(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT, () => loadDeferredImagesStart(true));
  79. document.addEventListener(LOAD_DEFERRED_IMAGES_END_EVENT, () => loadDeferredImagesEnd());
  80. document.addEventListener(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT, () => loadDeferredImagesEnd(true));
  81. document.addEventListener(LOAD_DEFERRED_IMAGES_RESET_EVENT, resetScreenSize);
  82. document.addEventListener(LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_EVENT, () => {
  83. const transform = document.documentElement.style.getPropertyValue("-sf-transform");
  84. const transformPriority = document.documentElement.style.getPropertyPriority("-sf-transform");
  85. const transformOrigin = document.documentElement.style.getPropertyValue("-sf-transform-origin");
  86. const transformOriginPriority = document.documentElement.style.getPropertyPriority("-sf-transform-origin");
  87. const minHeight = document.documentElement.style.getPropertyValue("-sf-min-height");
  88. const minHeightPriority = document.documentElement.style.getPropertyPriority("-sf-min-height");
  89. document.documentElement.style.setProperty("transform", transform, transformPriority);
  90. document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
  91. document.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
  92. document.documentElement.style.removeProperty("-sf-transform");
  93. document.documentElement.style.removeProperty("-sf-transform-origin");
  94. document.documentElement.style.removeProperty("-sf-min-height");
  95. resetScreenSize();
  96. });
  97. document.addEventListener(DISPATCH_SCROLL_START_EVENT, () => { dispatchScrollEvent = true; });
  98. document.addEventListener(DISPATCH_SCROLL_END_EVENT, () => { dispatchScrollEvent = false; });
  99. document.addEventListener(BLOCK_COOKIES_START_EVENT, () => {
  100. try {
  101. document.__defineGetter__("cookie", () => { throw new Error("document.cookie temporary blocked by SingleFile"); });
  102. } catch (error) {
  103. // ignored
  104. }
  105. });
  106. document.addEventListener(BLOCK_COOKIES_END_EVENT, () => { delete document.cookie; });
  107. document.addEventListener(BLOCK_STORAGE_START_EVENT, () => {
  108. if (!globalThis._singleFile_localStorage) {
  109. globalThis._singleFile_localStorage = globalThis.localStorage;
  110. globalThis.__defineGetter__("localStorage", () => { throw new Error("localStorage temporary blocked by SingleFile"); });
  111. }
  112. if (!globalThis._singleFile_indexedDB) {
  113. globalThis._singleFile_indexedDB = globalThis.indexedDB;
  114. globalThis.__defineGetter__("indexedDB", () => { throw new Error("indexedDB temporary blocked by SingleFile"); });
  115. }
  116. });
  117. document.addEventListener(BLOCK_STORAGE_END_EVENT, () => {
  118. if (globalThis._singleFile_localStorage) {
  119. delete globalThis.localStorage;
  120. globalThis.localStorage = globalThis._singleFile_localStorage;
  121. delete globalThis._singleFile_localStorage;
  122. }
  123. if (!globalThis._singleFile_indexedDB) {
  124. delete globalThis.indexedDB;
  125. globalThis.indexedDB = globalThis._singleFile_indexedDB;
  126. delete globalThis._singleFile_indexedDB;
  127. }
  128. });
  129. document.addEventListener(FETCH_REQUEST_EVENT, async event => {
  130. document.dispatchEvent(new CustomEvent(FETCH_ACK_EVENT));
  131. const { url, options } = JSON.parse(event.detail);
  132. let detail;
  133. try {
  134. const response = await fetch(url, options);
  135. detail = { url, response: await response.arrayBuffer(), headers: [...response.headers], status: response.status };
  136. } catch (error) {
  137. detail = { url, error: error && error.toString() };
  138. }
  139. document.dispatchEvent(new CustomEvent(FETCH_RESPONSE_EVENT, { detail }));
  140. });
  141. document.addEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener);
  142. }
  143. function loadDeferredImagesStart(keepZoomLevel) {
  144. const scrollingElement = document.scrollingElement || document.documentElement;
  145. const clientHeight = scrollingElement.clientHeight;
  146. const clientWidth = scrollingElement.clientWidth;
  147. const scrollHeight = Math.max(scrollingElement.scrollHeight - clientHeight, clientHeight);
  148. const scrollWidth = Math.max(scrollingElement.scrollWidth - clientWidth, clientWidth);
  149. document.querySelectorAll("[loading=lazy]").forEach(element => {
  150. element.loading = "eager";
  151. element.setAttribute(LAZY_LOAD_ATTRIBUTE, "");
  152. });
  153. scrollingElement.__defineGetter__("clientHeight", () => scrollHeight);
  154. scrollingElement.__defineGetter__("clientWidth", () => scrollWidth);
  155. screen.__defineGetter__("height", () => scrollHeight);
  156. screen.__defineGetter__("width", () => scrollWidth);
  157. globalThis._singleFile_innerHeight = globalThis.innerHeight;
  158. globalThis._singleFile_innerWidth = globalThis.innerWidth;
  159. globalThis.__defineGetter__("innerHeight", () => scrollHeight);
  160. globalThis.__defineGetter__("innerWidth", () => scrollWidth);
  161. if (!keepZoomLevel) {
  162. if (!globalThis._singleFile_getBoundingClientRect) {
  163. globalThis._singleFile_getBoundingClientRect = Element.prototype.getBoundingClientRect;
  164. Element.prototype.getBoundingClientRect = function () {
  165. const boundingRect = globalThis._singleFile_getBoundingClientRect.call(this);
  166. if (this == scrollingElement) {
  167. boundingRect.__defineGetter__("height", () => scrollHeight);
  168. boundingRect.__defineGetter__("bottom", () => scrollHeight + boundingRect.top);
  169. boundingRect.__defineGetter__("width", () => scrollWidth);
  170. boundingRect.__defineGetter__("right", () => scrollWidth + boundingRect.left);
  171. }
  172. return boundingRect;
  173. };
  174. }
  175. }
  176. if (!globalThis._singleFileImage) {
  177. const Image = globalThis.Image;
  178. globalThis._singleFileImage = globalThis.Image;
  179. globalThis.__defineGetter__("Image", function () {
  180. return function () {
  181. const image = new Image(...arguments);
  182. const result = new Image(...arguments);
  183. result.__defineSetter__("src", value => {
  184. image.src = value;
  185. document.dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT, { detail: image.src }));
  186. });
  187. result.__defineGetter__("src", () => image.src);
  188. result.__defineSetter__("srcset", value => {
  189. document.dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT));
  190. image.srcset = value;
  191. });
  192. result.__defineGetter__("srcset", () => image.srcset);
  193. result.__defineGetter__("height", () => image.height);
  194. result.__defineGetter__("width", () => image.width);
  195. result.__defineGetter__("naturalHeight", () => image.naturalHeight);
  196. result.__defineGetter__("naturalWidth", () => image.naturalWidth);
  197. if (image.decode) {
  198. result.__defineGetter__("decode", () => () => image.decode());
  199. }
  200. image.onload = image.onloadend = image.onerror = event => {
  201. document.dispatchEvent(new CustomEvent(IMAGE_LOADED_EVENT, { detail: image.src }));
  202. result.dispatchEvent(new Event(event.type, event));
  203. };
  204. return result;
  205. };
  206. });
  207. }
  208. let zoomFactorX, zoomFactorY;
  209. if (keepZoomLevel) {
  210. zoomFactorX = clientHeight / scrollHeight;
  211. zoomFactorY = clientWidth / scrollWidth;
  212. } else {
  213. zoomFactorX = (clientHeight + globalThis.scrollY) / scrollHeight;
  214. zoomFactorY = (clientWidth + globalThis.scrollX) / scrollWidth;
  215. }
  216. const zoomFactor = Math.min(zoomFactorX, zoomFactorY);
  217. if (zoomFactor < 1) {
  218. const transform = document.documentElement.style.getPropertyValue("transform");
  219. const transformPriority = document.documentElement.style.getPropertyPriority("transform");
  220. const transformOrigin = document.documentElement.style.getPropertyValue("transform-origin");
  221. const transformOriginPriority = document.documentElement.style.getPropertyPriority("transform-origin");
  222. const minHeight = document.documentElement.style.getPropertyValue("min-height");
  223. const minHeightPriority = document.documentElement.style.getPropertyPriority("min-height");
  224. document.documentElement.style.setProperty("transform-origin", (zoomFactorX < 1 ? "50%" : "0") + " " + (zoomFactorY < 1 ? "50%" : "0") + " 0", "important");
  225. document.documentElement.style.setProperty("transform", "scale3d(" + zoomFactor + ", " + zoomFactor + ", 1)", "important");
  226. document.documentElement.style.setProperty("min-height", (100 / zoomFactor) + "vh", "important");
  227. dispatchResizeEvent();
  228. if (keepZoomLevel) {
  229. document.documentElement.style.setProperty("-sf-transform", transform, transformPriority);
  230. document.documentElement.style.setProperty("-sf-transform-origin", transformOrigin, transformOriginPriority);
  231. document.documentElement.style.setProperty("-sf-min-height", minHeight, minHeightPriority);
  232. } else {
  233. document.documentElement.style.setProperty("transform", transform, transformPriority);
  234. document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
  235. document.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
  236. }
  237. }
  238. if (!keepZoomLevel) {
  239. dispatchResizeEvent();
  240. const docBoundingRect = scrollingElement.getBoundingClientRect();
  241. if (window == window.top) {
  242. [...observers].forEach(([intersectionObserver, observer]) => {
  243. const getBoundingClientRectDefined = observer.options && observer.options.root && observer.options.root.getBoundingClientRect;
  244. const rootBoundingRect = getBoundingClientRectDefined && observer.options.root.getBoundingClientRect();
  245. const targetElements = observedElements.get(intersectionObserver);
  246. if (targetElements) {
  247. const params = targetElements.map(target => {
  248. const boundingClientRect = target.getBoundingClientRect();
  249. const isIntersecting = true;
  250. const intersectionRatio = 1;
  251. const rootBounds = getBoundingClientRectDefined ? rootBoundingRect : docBoundingRect;
  252. const time = 0;
  253. return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
  254. });
  255. observer.callback.call(intersectionObserver, params, intersectionObserver);
  256. }
  257. });
  258. }
  259. }
  260. }
  261. function loadDeferredImagesEnd(keepZoomLevel) {
  262. document.querySelectorAll("[" + LAZY_LOAD_ATTRIBUTE + "]").forEach(element => {
  263. element.loading = "lazy";
  264. element.removeAttribute(LAZY_LOAD_ATTRIBUTE);
  265. });
  266. if (!keepZoomLevel) {
  267. if (globalThis._singleFile_getBoundingClientRect) {
  268. Element.prototype.getBoundingClientRect = globalThis._singleFile_getBoundingClientRect;
  269. delete globalThis._singleFile_getBoundingClientRect;
  270. }
  271. }
  272. if (globalThis._singleFileImage) {
  273. delete globalThis.Image;
  274. globalThis.Image = globalThis._singleFileImage;
  275. delete globalThis._singleFileImage;
  276. }
  277. if (!keepZoomLevel) {
  278. dispatchResizeEvent();
  279. }
  280. }
  281. function resetScreenSize() {
  282. const scrollingElement = document.scrollingElement || document.documentElement;
  283. if (globalThis._singleFile_innerHeight != null) {
  284. delete globalThis.innerHeight;
  285. globalThis.innerHeight = globalThis._singleFile_innerHeight;
  286. delete globalThis._singleFile_innerHeight;
  287. }
  288. if (globalThis._singleFile_innerWidth != null) {
  289. delete globalThis.innerWidth;
  290. globalThis.innerWidth = globalThis._singleFile_innerWidth;
  291. delete globalThis._singleFile_innerWidth;
  292. }
  293. delete scrollingElement.clientHeight;
  294. delete scrollingElement.clientWidth;
  295. delete screen.height;
  296. delete screen.width;
  297. }
  298. if (globalThis.FontFace) {
  299. const FontFace = globalThis.FontFace;
  300. globalThis.FontFace = function () {
  301. getDetailObject(...arguments).then(detail => document.dispatchEvent(new CustomEvent(NEW_FONT_FACE_EVENT, { detail })));
  302. return new FontFace(...arguments);
  303. };
  304. globalThis.FontFace.prototype = FontFace.prototype;
  305. globalThis.FontFace.toString = function () { return "function FontFace() { [native code] }"; };
  306. const deleteFont = document.fonts.delete;
  307. document.fonts.delete = function (fontFace) {
  308. getDetailObject(fontFace.family).then(detail => document.dispatchEvent(new CustomEvent(DELETE_FONT_EVENT, { detail })));
  309. return deleteFont.call(document.fonts, fontFace);
  310. };
  311. document.fonts.delete.toString = function () { return "function delete() { [native code] }"; };
  312. const clearFonts = document.fonts.clear;
  313. document.fonts.clear = function () {
  314. document.dispatchEvent(new CustomEvent(CLEAR_FONTS_EVENT));
  315. return clearFonts.call(document.fonts);
  316. };
  317. document.fonts.clear.toString = function () { return "function clear() { [native code] }"; };
  318. }
  319. if (globalThis.IntersectionObserver) {
  320. const IntersectionObserver = globalThis.IntersectionObserver;
  321. globalThis.IntersectionObserver = function () {
  322. const intersectionObserver = new IntersectionObserver(...arguments);
  323. const observeIntersection = IntersectionObserver.prototype.observe || intersectionObserver.observe;
  324. const unobserveIntersection = IntersectionObserver.prototype.unobserve || intersectionObserver.unobserve;
  325. const callback = arguments[0];
  326. const options = arguments[1];
  327. if (observeIntersection) {
  328. intersectionObserver.observe = function (targetElement) {
  329. let targetElements = observedElements.get(intersectionObserver);
  330. if (!targetElements) {
  331. targetElements = [];
  332. observedElements.set(intersectionObserver, targetElements);
  333. }
  334. targetElements.push(targetElement);
  335. return observeIntersection.call(intersectionObserver, targetElement);
  336. };
  337. }
  338. if (unobserveIntersection) {
  339. intersectionObserver.unobserve = function (targetElement) {
  340. let targetElements = observedElements.get(intersectionObserver);
  341. if (targetElements) {
  342. targetElements = targetElements.filter(element => element != targetElement);
  343. if (targetElements.length) {
  344. observedElements.set(intersectionObserver, targetElements);
  345. } else {
  346. observedElements.delete(intersectionObserver);
  347. observers.delete(intersectionObserver);
  348. }
  349. }
  350. return unobserveIntersection.call(intersectionObserver, targetElement);
  351. };
  352. }
  353. observers.set(intersectionObserver, { callback, options });
  354. return intersectionObserver;
  355. };
  356. globalThis.IntersectionObserver.prototype = IntersectionObserver.prototype;
  357. globalThis.IntersectionObserver.toString = function () { return "function IntersectionObserver() { [native code] }"; };
  358. }
  359. function getAdoptedStylesheetsListener(event) {
  360. const shadowRoot = event.target.shadowRoot;
  361. event.stopPropagation();
  362. if (shadowRoot) {
  363. shadowRoot.addEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener, { capture: true });
  364. shadowRoot.addEventListener(UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, () => shadowRoot.removeEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener), { once: true });
  365. const adoptedStyleSheets = Array.from(shadowRoot.adoptedStyleSheets).map(stylesheet => Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n"));
  366. if (adoptedStyleSheets.length) {
  367. shadowRoot.dispatchEvent(new CustomEvent(GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT, { detail: { adoptedStyleSheets } }));
  368. }
  369. }
  370. }
  371. async function getDetailObject(fontFamily, src, descriptors) {
  372. const detail = {};
  373. detail["font-family"] = fontFamily;
  374. detail.src = src;
  375. if (descriptors) {
  376. Object.keys(descriptors).forEach(descriptor => {
  377. if (FONT_STYLE_PROPERTIES[descriptor]) {
  378. detail[FONT_STYLE_PROPERTIES[descriptor]] = descriptors[descriptor];
  379. }
  380. });
  381. }
  382. return new Promise(resolve => {
  383. if (detail.src instanceof ArrayBuffer) {
  384. const reader = new FileReader();
  385. reader.readAsDataURL(new Blob([detail.src]));
  386. reader.addEventListener("load", () => {
  387. detail.src = "url(" + reader.result + ")";
  388. resolve(detail);
  389. });
  390. } else {
  391. resolve(detail);
  392. }
  393. });
  394. }
  395. function dispatchResizeEvent() {
  396. try {
  397. globalThis.dispatchEvent(new UIEvent("resize"));
  398. if (dispatchScrollEvent) {
  399. globalThis.dispatchEvent(new UIEvent("scroll"));
  400. }
  401. } catch (error) {
  402. // ignored
  403. }
  404. }
  405. })(typeof globalThis == "object" ? globalThis : window);
  406. })();