single-file-hooks-frames.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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 window, globalThis */
  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 addEventListener = (type, listener, options) => globalThis.addEventListener(type, listener, options);
  61. const dispatchEvent = event => { try { globalThis.dispatchEvent(event); } catch (error) { /* ignored */ } };
  62. const fetch = (url, options) => globalThis.fetch(url, options);
  63. const CustomEvent = globalThis.CustomEvent;
  64. const document = globalThis.document;
  65. const screen = globalThis.screen;
  66. const Element = globalThis.Element;
  67. const UIEvent = globalThis.UIEvent;
  68. const Event = globalThis.Event;
  69. const FileReader = globalThis.FileReader;
  70. const Blob = globalThis.Blob;
  71. const JSON = globalThis.JSON;
  72. const observers = new Map();
  73. const observedElements = new Map();
  74. let dispatchScrollEvent;
  75. addEventListener(LOAD_DEFERRED_IMAGES_START_EVENT, () => loadDeferredImagesStart());
  76. addEventListener(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_START_EVENT, () => loadDeferredImagesStart(true));
  77. function loadDeferredImagesStart(keepZoomLevel) {
  78. const scrollingElement = document.scrollingElement || document.documentElement;
  79. const clientHeight = scrollingElement.clientHeight;
  80. const clientWidth = scrollingElement.clientWidth;
  81. const scrollHeight = Math.max(scrollingElement.scrollHeight - clientHeight, clientHeight);
  82. const scrollWidth = Math.max(scrollingElement.scrollWidth - clientWidth, clientWidth);
  83. document.querySelectorAll("[loading=lazy]").forEach(element => {
  84. element.loading = "eager";
  85. element.setAttribute(LAZY_LOAD_ATTRIBUTE, "");
  86. });
  87. scrollingElement.__defineGetter__("clientHeight", () => scrollHeight);
  88. scrollingElement.__defineGetter__("clientWidth", () => scrollWidth);
  89. screen.__defineGetter__("height", () => scrollHeight);
  90. screen.__defineGetter__("width", () => scrollWidth);
  91. globalThis._singleFile_innerHeight = globalThis.innerHeight;
  92. globalThis._singleFile_innerWidth = globalThis.innerWidth;
  93. globalThis.__defineGetter__("innerHeight", () => scrollHeight);
  94. globalThis.__defineGetter__("innerWidth", () => scrollWidth);
  95. if (!keepZoomLevel) {
  96. if (!globalThis._singleFile_getBoundingClientRect) {
  97. globalThis._singleFile_getBoundingClientRect = Element.prototype.getBoundingClientRect;
  98. Element.prototype.getBoundingClientRect = function () {
  99. const boundingRect = globalThis._singleFile_getBoundingClientRect.call(this);
  100. if (this == scrollingElement) {
  101. boundingRect.__defineGetter__("height", () => scrollHeight);
  102. boundingRect.__defineGetter__("bottom", () => scrollHeight + boundingRect.top);
  103. boundingRect.__defineGetter__("width", () => scrollWidth);
  104. boundingRect.__defineGetter__("right", () => scrollWidth + boundingRect.left);
  105. }
  106. return boundingRect;
  107. };
  108. }
  109. }
  110. if (!globalThis._singleFileImage) {
  111. const Image = globalThis.Image;
  112. globalThis._singleFileImage = globalThis.Image;
  113. globalThis.__defineGetter__("Image", function () {
  114. return function () {
  115. const image = new Image(...arguments);
  116. const result = new Image(...arguments);
  117. result.__defineSetter__("src", value => {
  118. image.src = value;
  119. dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT, { detail: image.src }));
  120. });
  121. result.__defineGetter__("src", () => image.src);
  122. result.__defineSetter__("srcset", value => {
  123. dispatchEvent(new CustomEvent(LOAD_IMAGE_EVENT));
  124. image.srcset = value;
  125. });
  126. result.__defineGetter__("srcset", () => image.srcset);
  127. result.__defineGetter__("height", () => image.height);
  128. result.__defineGetter__("width", () => image.width);
  129. result.__defineGetter__("naturalHeight", () => image.naturalHeight);
  130. result.__defineGetter__("naturalWidth", () => image.naturalWidth);
  131. if (image.decode) {
  132. result.__defineGetter__("decode", () => () => image.decode());
  133. }
  134. image.onload = image.onloadend = image.onerror = event => {
  135. dispatchEvent(new CustomEvent(IMAGE_LOADED_EVENT, { detail: image.src }));
  136. result.dispatchEvent(new Event(event.type, event));
  137. };
  138. return result;
  139. };
  140. });
  141. }
  142. let zoomFactorX, zoomFactorY;
  143. if (keepZoomLevel) {
  144. zoomFactorX = clientHeight / scrollHeight;
  145. zoomFactorY = clientWidth / scrollWidth;
  146. } else {
  147. zoomFactorX = (clientHeight + globalThis.scrollY) / scrollHeight;
  148. zoomFactorY = (clientWidth + globalThis.scrollX) / scrollWidth;
  149. }
  150. const zoomFactor = Math.min(zoomFactorX, zoomFactorY);
  151. if (zoomFactor < 1) {
  152. const transform = document.documentElement.style.getPropertyValue("transform");
  153. const transformPriority = document.documentElement.style.getPropertyPriority("transform");
  154. const transformOrigin = document.documentElement.style.getPropertyValue("transform-origin");
  155. const transformOriginPriority = document.documentElement.style.getPropertyPriority("transform-origin");
  156. const minHeight = document.documentElement.style.getPropertyValue("min-height");
  157. const minHeightPriority = document.documentElement.style.getPropertyPriority("min-height");
  158. document.documentElement.style.setProperty("transform-origin", (zoomFactorX < 1 ? "50%" : "0") + " " + (zoomFactorY < 1 ? "50%" : "0") + " 0", "important");
  159. document.documentElement.style.setProperty("transform", "scale3d(" + zoomFactor + ", " + zoomFactor + ", 1)", "important");
  160. document.documentElement.style.setProperty("min-height", (100 / zoomFactor) + "vh", "important");
  161. dispatchResizeEvent();
  162. if (keepZoomLevel) {
  163. document.documentElement.style.setProperty("-sf-transform", transform, transformPriority);
  164. document.documentElement.style.setProperty("-sf-transform-origin", transformOrigin, transformOriginPriority);
  165. document.documentElement.style.setProperty("-sf-min-height", minHeight, minHeightPriority);
  166. } else {
  167. document.documentElement.style.setProperty("transform", transform, transformPriority);
  168. document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
  169. document.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
  170. }
  171. }
  172. if (!keepZoomLevel) {
  173. dispatchResizeEvent();
  174. const docBoundingRect = scrollingElement.getBoundingClientRect();
  175. if (window == window.top) {
  176. [...observers].forEach(([intersectionObserver, observer]) => {
  177. const getBoundingClientRectDefined = observer.options && observer.options.root && observer.options.root.getBoundingClientRect;
  178. const rootBoundingRect = getBoundingClientRectDefined && observer.options.root.getBoundingClientRect();
  179. const targetElements = observedElements.get(intersectionObserver);
  180. if (targetElements) {
  181. const params = targetElements.map(target => {
  182. const boundingClientRect = target.getBoundingClientRect();
  183. const isIntersecting = true;
  184. const intersectionRatio = 1;
  185. const rootBounds = getBoundingClientRectDefined ? rootBoundingRect : docBoundingRect;
  186. const time = 0;
  187. return { target, intersectionRatio, boundingClientRect, intersectionRect: boundingClientRect, isIntersecting, rootBounds, time };
  188. });
  189. observer.callback(params, intersectionObserver);
  190. }
  191. });
  192. }
  193. }
  194. }
  195. addEventListener(LOAD_DEFERRED_IMAGES_END_EVENT, () => loadDeferredImagesEnd());
  196. addEventListener(LOAD_DEFERRED_IMAGES_KEEP_ZOOM_LEVEL_END_EVENT, () => loadDeferredImagesEnd(true));
  197. addEventListener(LOAD_DEFERRED_IMAGES_RESET_EVENT, resetScreenSize);
  198. addEventListener(LOAD_DEFERRED_IMAGES_RESET_ZOOM_LEVEL_EVENT, () => {
  199. const transform = document.documentElement.style.getPropertyValue("-sf-transform");
  200. const transformPriority = document.documentElement.style.getPropertyPriority("-sf-transform");
  201. const transformOrigin = document.documentElement.style.getPropertyValue("-sf-transform-origin");
  202. const transformOriginPriority = document.documentElement.style.getPropertyPriority("-sf-transform-origin");
  203. const minHeight = document.documentElement.style.getPropertyValue("-sf-min-height");
  204. const minHeightPriority = document.documentElement.style.getPropertyPriority("-sf-min-height");
  205. document.documentElement.style.setProperty("transform", transform, transformPriority);
  206. document.documentElement.style.setProperty("transform-origin", transformOrigin, transformOriginPriority);
  207. document.documentElement.style.setProperty("min-height", minHeight, minHeightPriority);
  208. document.documentElement.style.removeProperty("-sf-transform");
  209. document.documentElement.style.removeProperty("-sf-transform-origin");
  210. document.documentElement.style.removeProperty("-sf-min-height");
  211. resetScreenSize();
  212. });
  213. function loadDeferredImagesEnd(keepZoomLevel) {
  214. document.querySelectorAll("[" + LAZY_LOAD_ATTRIBUTE + "]").forEach(element => {
  215. element.loading = "lazy";
  216. element.removeAttribute(LAZY_LOAD_ATTRIBUTE);
  217. });
  218. if (!keepZoomLevel) {
  219. if (globalThis._singleFile_getBoundingClientRect) {
  220. Element.prototype.getBoundingClientRect = globalThis._singleFile_getBoundingClientRect;
  221. delete globalThis._singleFile_getBoundingClientRect;
  222. }
  223. }
  224. if (globalThis._singleFileImage) {
  225. delete globalThis.Image;
  226. globalThis.Image = globalThis._singleFileImage;
  227. delete globalThis._singleFileImage;
  228. }
  229. if (!keepZoomLevel) {
  230. dispatchResizeEvent();
  231. }
  232. }
  233. function resetScreenSize() {
  234. const scrollingElement = document.scrollingElement || document.documentElement;
  235. if (globalThis._singleFile_innerHeight != null) {
  236. delete globalThis.innerHeight;
  237. globalThis.innerHeight = globalThis._singleFile_innerHeight;
  238. delete globalThis._singleFile_innerHeight;
  239. }
  240. if (globalThis._singleFile_innerWidth != null) {
  241. delete globalThis.innerWidth;
  242. globalThis.innerWidth = globalThis._singleFile_innerWidth;
  243. delete globalThis._singleFile_innerWidth;
  244. }
  245. delete scrollingElement.clientHeight;
  246. delete scrollingElement.clientWidth;
  247. delete screen.height;
  248. delete screen.width;
  249. }
  250. addEventListener(DISPATCH_SCROLL_START_EVENT, () => {
  251. dispatchScrollEvent = true;
  252. });
  253. addEventListener(DISPATCH_SCROLL_END_EVENT, () => {
  254. dispatchScrollEvent = false;
  255. });
  256. addEventListener(BLOCK_COOKIES_START_EVENT, () => {
  257. try {
  258. document.__defineGetter__("cookie", () => { throw new Error("document.cookie temporary blocked by SingleFile"); });
  259. } catch (error) {
  260. // ignored
  261. }
  262. });
  263. addEventListener(BLOCK_COOKIES_END_EVENT, () => {
  264. delete document.cookie;
  265. });
  266. addEventListener(BLOCK_STORAGE_START_EVENT, () => {
  267. if (!globalThis._singleFile_localStorage) {
  268. globalThis._singleFile_localStorage = globalThis.localStorage;
  269. globalThis.__defineGetter__("localStorage", () => { throw new Error("localStorage temporary blocked by SingleFile"); });
  270. }
  271. if (!globalThis._singleFile_indexedDB) {
  272. globalThis._singleFile_indexedDB = globalThis.indexedDB;
  273. globalThis.__defineGetter__("indexedDB", () => { throw new Error("indexedDB temporary blocked by SingleFile"); });
  274. }
  275. });
  276. addEventListener(BLOCK_STORAGE_END_EVENT, () => {
  277. if (globalThis._singleFile_localStorage) {
  278. delete globalThis.localStorage;
  279. globalThis.localStorage = globalThis._singleFile_localStorage;
  280. delete globalThis._singleFile_localStorage;
  281. }
  282. if (!globalThis._singleFile_indexedDB) {
  283. delete globalThis.indexedDB;
  284. globalThis.indexedDB = globalThis._singleFile_indexedDB;
  285. delete globalThis._singleFile_indexedDB;
  286. }
  287. });
  288. addEventListener(FETCH_REQUEST_EVENT, async event => {
  289. dispatchEvent(new CustomEvent(FETCH_ACK_EVENT));
  290. const { url, options } = JSON.parse(event.detail);
  291. let detail;
  292. try {
  293. const response = await fetch(url, options);
  294. detail = { url, response: await response.arrayBuffer(), headers: [...response.headers], status: response.status };
  295. } catch (error) {
  296. detail = { url, error: error && error.toString() };
  297. }
  298. dispatchEvent(new CustomEvent(FETCH_RESPONSE_EVENT, { detail }));
  299. });
  300. addEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener);
  301. if (globalThis.FontFace) {
  302. const FontFace = globalThis.FontFace;
  303. globalThis.FontFace = function () {
  304. getDetailObject(...arguments).then(detail => dispatchEvent(new CustomEvent(NEW_FONT_FACE_EVENT, { detail })));
  305. return new FontFace(...arguments);
  306. };
  307. globalThis.FontFace.prototype = FontFace.prototype;
  308. globalThis.FontFace.toString = function () { return "function FontFace() { [native code] }"; };
  309. const deleteFont = document.fonts.delete;
  310. document.fonts.delete = function (fontFace) {
  311. getDetailObject(fontFace.family).then(detail => dispatchEvent(new CustomEvent(DELETE_FONT_EVENT, { detail })));
  312. return deleteFont.call(document.fonts, fontFace);
  313. };
  314. document.fonts.delete.toString = function () { return "function delete() { [native code] }"; };
  315. const clearFonts = document.fonts.clear;
  316. document.fonts.clear = function () {
  317. dispatchEvent(new CustomEvent(CLEAR_FONTS_EVENT));
  318. return clearFonts.call(document.fonts);
  319. };
  320. document.fonts.clear.toString = function () { return "function clear() { [native code] }"; };
  321. }
  322. if (globalThis.IntersectionObserver) {
  323. const IntersectionObserver = globalThis.IntersectionObserver;
  324. globalThis.IntersectionObserver = function () {
  325. const intersectionObserver = new IntersectionObserver(...arguments);
  326. const observeIntersection = IntersectionObserver.prototype.observe || intersectionObserver.observe;
  327. const unobserveIntersection = IntersectionObserver.prototype.unobserve || intersectionObserver.unobserve;
  328. const callback = arguments[0];
  329. const options = arguments[1];
  330. if (observeIntersection) {
  331. intersectionObserver.observe = function (targetElement) {
  332. let targetElements = observedElements.get(intersectionObserver);
  333. if (!targetElements) {
  334. targetElements = [];
  335. observedElements.set(intersectionObserver, targetElements);
  336. }
  337. targetElements.push(targetElement);
  338. return observeIntersection.call(intersectionObserver, targetElement);
  339. };
  340. }
  341. if (unobserveIntersection) {
  342. intersectionObserver.unobserve = function (targetElement) {
  343. let targetElements = observedElements.get(intersectionObserver);
  344. if (targetElements) {
  345. targetElements = targetElements.filter(element => element != targetElement);
  346. if (targetElements.length) {
  347. observedElements.set(intersectionObserver, targetElements);
  348. } else {
  349. observedElements.delete(intersectionObserver);
  350. observers.delete(intersectionObserver);
  351. }
  352. }
  353. return unobserveIntersection.call(intersectionObserver, targetElement);
  354. };
  355. }
  356. observers.set(intersectionObserver, { callback, options });
  357. return intersectionObserver;
  358. };
  359. globalThis.IntersectionObserver.prototype = IntersectionObserver.prototype;
  360. globalThis.IntersectionObserver.toString = function () { return "function IntersectionObserver() { [native code] }"; };
  361. }
  362. function getAdoptedStylesheetsListener(event) {
  363. const shadowRoot = event.target.shadowRoot;
  364. event.stopPropagation();
  365. if (shadowRoot) {
  366. shadowRoot.addEventListener(GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, getAdoptedStylesheetsListener, {});
  367. shadowRoot.addEventListener(UNREGISTER_GET_ADOPTED_STYLESHEETS_REQUEST_EVENT, () => shadowRoot.removeEventListener(getAdoptedStylesheetsListener), { once: true });
  368. const adoptedStyleSheets = Array.from(shadowRoot.adoptedStyleSheets).map(stylesheet => Array.from(stylesheet.cssRules).map(cssRule => cssRule.cssText).join("\n"));
  369. if (adoptedStyleSheets.length) {
  370. event.target.dispatchEvent(new CustomEvent(GET_ADOPTED_STYLESHEETS_RESPONSE_EVENT, { detail: { adoptedStyleSheets } }));
  371. }
  372. }
  373. }
  374. async function getDetailObject(fontFamily, src, descriptors) {
  375. const detail = {};
  376. detail["font-family"] = fontFamily;
  377. detail.src = src;
  378. if (descriptors) {
  379. Object.keys(descriptors).forEach(descriptor => {
  380. if (FONT_STYLE_PROPERTIES[descriptor]) {
  381. detail[FONT_STYLE_PROPERTIES[descriptor]] = descriptors[descriptor];
  382. }
  383. });
  384. }
  385. return new Promise(resolve => {
  386. if (detail.src instanceof ArrayBuffer) {
  387. const reader = new FileReader();
  388. reader.readAsDataURL(new Blob([detail.src]));
  389. reader.addEventListener("load", () => {
  390. detail.src = "url(" + reader.result + ")";
  391. resolve(detail);
  392. });
  393. } else {
  394. resolve(detail);
  395. }
  396. });
  397. }
  398. function dispatchResizeEvent() {
  399. try {
  400. dispatchEvent(new UIEvent("resize"));
  401. if (dispatchScrollEvent) {
  402. dispatchEvent(new UIEvent("scroll"));
  403. }
  404. } catch (error) {
  405. // ignored
  406. }
  407. }
  408. })(typeof globalThis == "object" ? globalThis : window);
  409. })();