content-infobar-web.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. /*
  2. * Copyright 2010-2020 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * The code in this file is free software: you can redistribute it and/or
  8. * modify it under the terms of the GNU Affero General Public License
  9. * (GNU AGPL) as published by the Free Software Foundation, either version 3
  10. * of the License, or (at your option) any later version.
  11. *
  12. * The code in this file is distributed in the hope that it will be useful,
  13. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero
  15. * General Public License for more details.
  16. *
  17. * As additional permission under GNU AGPL version 3 section 7, you may
  18. * distribute UNMODIFIED VERSIONS OF THIS file without the copy of the GNU
  19. * AGPL normally required by section 4, provided you include this license
  20. * notice and a URL through which recipients can access the Corresponding
  21. * Source.
  22. */
  23. /* global document, Node, window, top, getComputedStyle, XPathResult */
  24. (() => {
  25. const INFOBAR_TAGNAME = "singlefile-infobar";
  26. const LINK_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kj1Iw0AYht+mSkUrDnYQcchQnSyIijqWKhbBQmkrtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfEydFJ0UVK/C4ptIjx4LiH9+59+e67A4RGhalm1wSgapaRisfEbG5VDLyiDwEAvZiVmKkn0osZeI6ve/j4ehfhWd7n/hz9St5kgE8kjjLdsIg3iGc2LZ3zPnGIlSSF+Jx43KACiR+5Lrv8xrnosMAzQ0YmNU8cIhaLHSx3MCsZKvE0cVhRNcoXsi4rnLc4q5Uaa9XJbxjMaytprtMcQRxLSCAJETJqKKMCCxFaNVJMpGg/5uEfdvxJcsnkKoORYwFVqJAcP/gb/O6tWZiadJOCMaD7xbY/RoHALtCs2/b3sW03TwD/M3Cltf3VBjD3SXq9rYWPgIFt4OK6rcl7wOUOMPSkS4bkSH6aQqEAvJ/RM+WAwVv6EGtu31r7OH0AMtSr5Rvg4BAYK1L2use9ezr79u+ZVv9+AFlNcp0UUpiqAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AsHAB8H+DhhoQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAJUExURQAAAICHi4qKioTuJAkAAAABdFJOUwBA5thmAAAAAWJLR0QCZgt8ZAAAAJJJREFUOI3t070NRCEMA2CnYAOyDyPwpHj/Va7hJ3FzV7zy3ET5JIwoAF6Jk4wzAJAkzxAYG9YRTgB+24wBgKmfrGAKTcEfAY4KRlRoIeBTgKOCERVaCPgU4Khge2GqKOBTgKOCERVaAEC/4PNcnyoSWHpjqkhwKxbcig0Q6AorXYF/+A6eIYD1lVbwG/jdA6/kA2THRAURVubcAAAAAElFTkSuQmCC";
  27. const CLOSE_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABAAgMAAADXB5lNAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kj1Iw0AYht+mSkUrDnYQcchQnSyIijqWKhbBQmkrtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfEydFJ0UVK/C4ptIjx4LiH9+59+e67A4RGhalm1wSgapaRisfEbG5VDLyiDwEAvZiVmKkn0osZeI6ve/j4ehfhWd7n/hz9St5kgE8kjjLdsIg3iGc2LZ3zPnGIlSSF+Jx43KACiR+5Lrv8xrnosMAzQ0YmNU8cIhaLHSx3MCsZKvE0cVhRNcoXsi4rnLc4q5Uaa9XJbxjMaytprtMcQRxLSCAJETJqKKMCCxFaNVJMpGg/5uEfdvxJcsnkKoORYwFVqJAcP/gb/O6tWZiadJOCMaD7xbY/RoHALtCs2/b3sW03TwD/M3Cltf3VBjD3SXq9rYWPgIFt4OK6rcl7wOUOMPSkS4bkSH6aQqEAvJ/RM+WAwVv6EGtu31r7OH0AMtSr5Rvg4BAYK1L2use9ezr79u+ZVv9+AFlNcp0UUpiqAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AsHAB8VC4EQ6QAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAJUExURQAAAICHi4qKioTuJAkAAAABdFJOUwBA5thmAAAAAWJLR0QCZgt8ZAAAAJtJREFUOI3NkrsBgCAMRLFwBPdxBArcfxXFkO8rbKWAAJfHJ9faf9vuYX/749T5NmShm3bEwbe2SxeuM4+2oxDL1cDoKtVUjRy+tH78Cv2CS+wIiQNC1AEhk4AQeUTMWUJMfUJMSEJMSEY8kIx4IONroaYAimNxsXp1PA7PxwfVL8QnowwoVC0lig07wDDVUjAdbAnjwtow/z/bDW7eI4M2KruJAAAAAElFTkSuQmCC";
  28. const IMAGE_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABABAMAAABYR2ztAAABhmlDQ1BJQ0MgcHJvZmlsZQAAKJF9kj1Iw0AYht+mSkUrDnYQcchQnSyIijqWKhbBQmkrtOpgcukfNGlIUlwcBdeCgz+LVQcXZ10dXAVB8AfEydFJ0UVK/C4ptIjx4LiH9+59+e67A4RGhalm1wSgapaRisfEbG5VDLyiDwEAvZiVmKkn0osZeI6ve/j4ehfhWd7n/hz9St5kgE8kjjLdsIg3iGc2LZ3zPnGIlSSF+Jx43KACiR+5Lrv8xrnosMAzQ0YmNU8cIhaLHSx3MCsZKvE0cVhRNcoXsi4rnLc4q5Uaa9XJbxjMaytprtMcQRxLSCAJETJqKKMCCxFaNVJMpGg/5uEfdvxJcsnkKoORYwFVqJAcP/gb/O6tWZiadJOCMaD7xbY/RoHALtCs2/b3sW03TwD/M3Cltf3VBjD3SXq9rYWPgIFt4OK6rcl7wOUOMPSkS4bkSH6aQqEAvJ/RM+WAwVv6EGtu31r7OH0AMtSr5Rvg4BAYK1L2use9ezr79u+ZVv9+AFlNcp0UUpiqAAAACXBIWXMAAC4jAAAuIwF4pT92AAAAB3RJTUUH5AsHADIRLMaOHwAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAAAPUExURQAAAIqKioyNjY2OjvDw8L2y1DEAAAABdFJOUwBA5thmAAAAAWJLR0QB/wIt3gAAAGNJREFUSMdjYCAJsLi4OBCQx6/CBQwIGIDPCBcXAkYQUsACU+AwlBVQHg6Eg5pgZBGOboIJZugDFwRwoJECJCUOhJI1wZwzqmBUwagCuipgIqTABG9h7YIKaKGAURAFEF/6AQAO4HqSoDP8bgAAAABJRU5ErkJggg==";
  29. const SINGLEFILE_COMMENT = "SingleFile";
  30. const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
  31. const INFOBAR_STYLES = `
  32. .infobar {
  33. background-color: #737373;
  34. color: white;
  35. display: flex;
  36. position: fixed;
  37. top: 16px;
  38. right: 16px;
  39. height: auto;
  40. width: auto;
  41. min-height: 24px;
  42. min-width: 24px;
  43. background-position: center;
  44. background-repeat: no-repeat;
  45. z-index: 2147483647;
  46. margin: 0 0 0 16px;
  47. background-image: url(${IMAGE_ICON});
  48. border-radius: 16px;
  49. user-select: none;
  50. -moz-user-select: none;
  51. opacity: .7;
  52. cursor: pointer;
  53. padding-left: 0;
  54. padding-right: 0;
  55. padding-top: 0;
  56. padding-bottom: 0;
  57. border: 2px solid #eee;
  58. background-size: 70% 70%;
  59. transition: all 250ms;
  60. font-size: 13px;
  61. }
  62. .infobar:hover {
  63. opacity: 1;
  64. }
  65. .infobar-open {
  66. opacity: 1;
  67. background-color: #f9f9f9;
  68. cursor: auto;
  69. color: #2d2d2d;
  70. padding-top: 2px;
  71. padding-bottom: 2px;
  72. border: 2px solid #878787;
  73. background-image: none;
  74. border-radius: 8px;
  75. user-select: initial;
  76. -moz-user-select: initial;
  77. }
  78. .infobar-close-button {
  79. display: none;
  80. opacity: .7;
  81. padding-top: 4px;
  82. padding-left: 8px;
  83. padding-right: 8px;
  84. cursor: pointer;
  85. transition: opacity 250ms;
  86. height: 16px;
  87. }
  88. .infobar-close-button:hover {
  89. opacity: 1;
  90. }
  91. .infobar-content {
  92. display: none;
  93. font-family: Arial;
  94. font-size: 14px;
  95. line-height: 22px;
  96. word-break: break-word;
  97. white-space: pre-wrap;
  98. position: relative;
  99. top: 1px;
  100. text-align: left;
  101. }
  102. .infobar-link {
  103. display: none;
  104. padding-left: 8px;
  105. padding-right: 8px;
  106. line-height: 11px;
  107. cursor: pointer;
  108. user-select: none;
  109. }
  110. .infobar-link-icon {
  111. padding-top: 4px;
  112. padding-left: 2px;
  113. cursor: pointer;
  114. opacity: .7;
  115. transition: opacity 250ms;
  116. height: 16px;
  117. }
  118. .infobar-link-icon:hover {
  119. opacity: 1;
  120. }
  121. .infobar-open .infobar-close-button, .infobar-open .infobar-content, .infobar-open .infobar-link {
  122. display: inline-block;
  123. }`;
  124. let SHADOW_DOM_SUPPORTED = true;
  125. const browser = this.browser;
  126. if (window == top) {
  127. if (document.readyState == "loading") {
  128. document.addEventListener("DOMContentLoaded", displayIcon, false);
  129. } else {
  130. displayIcon();
  131. }
  132. }
  133. async function displayIcon() {
  134. const result = document.evaluate("//comment()", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  135. let singleFileComment = result && result.singleNodeValue;
  136. if (singleFileComment && isSingleFileComment(singleFileComment)) {
  137. const info = singleFileComment.textContent.split("\n");
  138. const [, , url, saveDate, ...infoData] = info;
  139. if (url && saveDate) {
  140. let options;
  141. if (browser && browser.runtime && browser.runtime.sendMessage) {
  142. options = await browser.runtime.sendMessage({ method: "tabs.getOptions", url });
  143. } else {
  144. options = { displayInfobar: true };
  145. }
  146. if (options.displayInfobar) {
  147. await initInfobar(url, saveDate, infoData);
  148. }
  149. }
  150. }
  151. }
  152. function isSingleFileComment(node) {
  153. return node.nodeType == Node.COMMENT_NODE && node.textContent.includes(SINGLEFILE_COMMENT);
  154. }
  155. async function initInfobar(url, saveDate, infoData) {
  156. let infobarElement = document.querySelector(INFOBAR_TAGNAME);
  157. if (!infobarElement) {
  158. url = url.split("url: ")[1];
  159. saveDate = saveDate.split("saved date: ")[1];
  160. if (infoData && infoData.length > 1) {
  161. let content = infoData[0].split("info: ")[1].trim();
  162. for (let indexLine = 1; indexLine < infoData.length - 1; indexLine++) {
  163. content += "\n" + infoData[indexLine].trim();
  164. }
  165. infoData = content.trim();
  166. } else {
  167. infoData = saveDate;
  168. }
  169. infobarElement = createElement(INFOBAR_TAGNAME, document.body);
  170. infobarElement.className = SINGLE_FILE_UI_ELEMENT_CLASS;
  171. const shadowRoot = await getShadowRoot(infobarElement);
  172. const styleElement = document.createElement("style");
  173. styleElement.textContent = INFOBAR_STYLES;
  174. shadowRoot.appendChild(styleElement);
  175. const infobarContent = document.createElement("div");
  176. infobarContent.classList.add("infobar");
  177. shadowRoot.appendChild(infobarContent);
  178. const closeElement = document.createElement("img");
  179. closeElement.classList.add("infobar-close-button");
  180. infobarContent.appendChild(closeElement);
  181. closeElement.src = CLOSE_ICON;
  182. closeElement.onclick = event => {
  183. if (event.button === 0) {
  184. infobarElement.remove();
  185. }
  186. };
  187. const infoElement = document.createElement("span");
  188. infobarContent.appendChild(infoElement);
  189. infoElement.classList.add("infobar-content");
  190. infoElement.textContent = infoData;
  191. const linkElement = document.createElement("a");
  192. linkElement.classList.add("infobar-link");
  193. infobarContent.appendChild(linkElement);
  194. linkElement.target = "_blank";
  195. linkElement.rel = "noopener noreferrer";
  196. linkElement.title = "Open source URL: " + url;
  197. linkElement.href = url;
  198. const imgElement = document.createElement("img");
  199. imgElement.classList.add("infobar-link-icon");
  200. linkElement.appendChild(imgElement);
  201. imgElement.src = LINK_ICON;
  202. hideInfobar(infobarContent);
  203. document.addEventListener("click", event => {
  204. if (event.button === 0) {
  205. let element = event.target;
  206. while (element && element != infobarElement) {
  207. element = element.parentElement;
  208. }
  209. if (element != infobarElement) {
  210. hideInfobar(infobarContent);
  211. }
  212. }
  213. });
  214. }
  215. }
  216. function displayInfobar(infobarContent) {
  217. if (!SHADOW_DOM_SUPPORTED) {
  218. const infobarElement = document.querySelector(INFOBAR_TAGNAME);
  219. const frameElement = infobarElement.childNodes[0];
  220. frameElement.contentWindow.getSelection().removeAllRanges();
  221. }
  222. infobarContent.classList.add("infobar-open");
  223. infobarContent.onclick = null;
  224. infobarContent.onmouseout = null;
  225. if (!SHADOW_DOM_SUPPORTED) {
  226. const infobarElement = document.querySelector(INFOBAR_TAGNAME);
  227. const frameElement = infobarElement.childNodes[0];
  228. frameElement.style.setProperty("width", "100vw", "important");
  229. frameElement.style.setProperty("height", "100vh", "important");
  230. frameElement.style.setProperty("width", (infobarContent.getBoundingClientRect().width + 33) + "px", "important");
  231. frameElement.style.setProperty("height", (infobarContent.getBoundingClientRect().height + 21) + "px", "important");
  232. }
  233. }
  234. function hideInfobar(infobarContent) {
  235. infobarContent.classList.remove("infobar-open");
  236. infobarContent.onclick = event => {
  237. if (event.button === 0) {
  238. displayInfobar(infobarContent);
  239. return false;
  240. }
  241. };
  242. if (!SHADOW_DOM_SUPPORTED) {
  243. const infobarElement = document.querySelector(INFOBAR_TAGNAME);
  244. const frameElement = infobarElement.childNodes[0];
  245. frameElement.style.setProperty("width", "44px", "important");
  246. frameElement.style.setProperty("height", "48px", "important");
  247. }
  248. }
  249. function createElement(tagName, parentElement) {
  250. const element = document.createElement(tagName);
  251. parentElement.appendChild(element);
  252. Array.from(getComputedStyle(element)).forEach(property => element.style.setProperty(property, "initial", "important"));
  253. return element;
  254. }
  255. async function getShadowRoot(element) {
  256. if (element.attachShadow) {
  257. return element.attachShadow({ mode: "open" });
  258. } else {
  259. SHADOW_DOM_SUPPORTED = false;
  260. const iframe = createElement("iframe", element);
  261. iframe.style.setProperty("background-color", "transparent", "important");
  262. iframe.style.setProperty("position", "fixed", "important");
  263. iframe.style.setProperty("top", 0, "important");
  264. iframe.style.setProperty("right", 0, "important");
  265. iframe.style.setProperty("width", "44px", "important");
  266. iframe.style.setProperty("height", "48px", "important");
  267. iframe.style.setProperty("z-index", 2147483647, "important");
  268. return new Promise(resolve => {
  269. iframe.contentDocument.body.style.setProperty("margin", 0);
  270. iframe.onload = () => resolve(iframe.contentDocument.body);
  271. });
  272. }
  273. }
  274. })();