content-infobar-web.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  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. outline: 0;
  110. }
  111. .infobar-link-icon {
  112. padding-top: 4px;
  113. padding-left: 2px;
  114. cursor: pointer;
  115. opacity: .7;
  116. transition: opacity 250ms;
  117. height: 16px;
  118. }
  119. .infobar-link-icon:hover {
  120. opacity: 1;
  121. }
  122. .infobar-open .infobar-close-button, .infobar-open .infobar-content, .infobar-open .infobar-link {
  123. display: inline-block;
  124. }`;
  125. let SHADOW_DOM_SUPPORTED = true;
  126. const browser = this.browser;
  127. if (window == top) {
  128. if (document.readyState == "loading") {
  129. document.addEventListener("DOMContentLoaded", displayIcon, false);
  130. } else {
  131. displayIcon();
  132. }
  133. }
  134. async function displayIcon() {
  135. const result = document.evaluate("//comment()", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
  136. let singleFileComment = result && result.singleNodeValue;
  137. if (singleFileComment && isSingleFileComment(singleFileComment)) {
  138. const info = singleFileComment.textContent.split("\n");
  139. const [, , url, saveDate, ...infoData] = info;
  140. if (url && saveDate) {
  141. let options;
  142. if (browser && browser.runtime && browser.runtime.sendMessage) {
  143. options = await browser.runtime.sendMessage({ method: "tabs.getOptions", url });
  144. } else {
  145. options = { displayInfobar: true };
  146. }
  147. if (options.displayInfobar) {
  148. await initInfobar(url, saveDate, infoData);
  149. }
  150. }
  151. }
  152. }
  153. function isSingleFileComment(node) {
  154. return node.nodeType == Node.COMMENT_NODE && node.textContent.includes(SINGLEFILE_COMMENT);
  155. }
  156. async function initInfobar(url, saveDate, infoData) {
  157. let infobarElement = document.querySelector(INFOBAR_TAGNAME);
  158. if (!infobarElement) {
  159. url = url.split("url: ")[1];
  160. saveDate = saveDate.split("saved date: ")[1];
  161. if (infoData && infoData.length > 1) {
  162. let content = infoData[0].split("info: ")[1].trim();
  163. for (let indexLine = 1; indexLine < infoData.length - 1; indexLine++) {
  164. content += "\n" + infoData[indexLine].trim();
  165. }
  166. infoData = content.trim();
  167. } else {
  168. infoData = saveDate;
  169. }
  170. infobarElement = createElement(INFOBAR_TAGNAME, document.body);
  171. infobarElement.className = SINGLE_FILE_UI_ELEMENT_CLASS;
  172. const shadowRoot = await getShadowRoot(infobarElement);
  173. const styleElement = document.createElement("style");
  174. styleElement.textContent = INFOBAR_STYLES;
  175. shadowRoot.appendChild(styleElement);
  176. const infobarContent = document.createElement("div");
  177. infobarContent.classList.add("infobar");
  178. shadowRoot.appendChild(infobarContent);
  179. const closeElement = document.createElement("img");
  180. closeElement.classList.add("infobar-close-button");
  181. infobarContent.appendChild(closeElement);
  182. closeElement.src = CLOSE_ICON;
  183. closeElement.onclick = event => {
  184. if (event.button === 0) {
  185. infobarElement.remove();
  186. }
  187. };
  188. const infoElement = document.createElement("span");
  189. infobarContent.appendChild(infoElement);
  190. infoElement.classList.add("infobar-content");
  191. infoElement.textContent = infoData;
  192. const linkElement = document.createElement("a");
  193. linkElement.classList.add("infobar-link");
  194. infobarContent.appendChild(linkElement);
  195. linkElement.target = "_blank";
  196. linkElement.rel = "noopener noreferrer";
  197. linkElement.title = "Open source URL: " + url;
  198. linkElement.href = url;
  199. const imgElement = document.createElement("img");
  200. imgElement.classList.add("infobar-link-icon");
  201. linkElement.appendChild(imgElement);
  202. imgElement.src = LINK_ICON;
  203. hideInfobar(infobarContent);
  204. document.addEventListener("click", event => {
  205. if (event.button === 0) {
  206. let element = event.target;
  207. while (element && element != infobarElement) {
  208. element = element.parentElement;
  209. }
  210. if (element != infobarElement) {
  211. hideInfobar(infobarContent);
  212. }
  213. }
  214. });
  215. }
  216. }
  217. function displayInfobar(infobarContent) {
  218. if (!SHADOW_DOM_SUPPORTED) {
  219. const infobarElement = document.querySelector(INFOBAR_TAGNAME);
  220. const frameElement = infobarElement.childNodes[0];
  221. frameElement.contentWindow.getSelection().removeAllRanges();
  222. }
  223. infobarContent.classList.add("infobar-open");
  224. infobarContent.onclick = null;
  225. infobarContent.onmouseout = null;
  226. if (!SHADOW_DOM_SUPPORTED) {
  227. const infobarElement = document.querySelector(INFOBAR_TAGNAME);
  228. const frameElement = infobarElement.childNodes[0];
  229. frameElement.style.setProperty("width", "100vw", "important");
  230. frameElement.style.setProperty("height", "100vh", "important");
  231. frameElement.style.setProperty("width", (infobarContent.getBoundingClientRect().width + 33) + "px", "important");
  232. frameElement.style.setProperty("height", (infobarContent.getBoundingClientRect().height + 21) + "px", "important");
  233. }
  234. }
  235. function hideInfobar(infobarContent) {
  236. infobarContent.classList.remove("infobar-open");
  237. infobarContent.onclick = event => {
  238. if (event.button === 0) {
  239. displayInfobar(infobarContent);
  240. return false;
  241. }
  242. };
  243. if (!SHADOW_DOM_SUPPORTED) {
  244. const infobarElement = document.querySelector(INFOBAR_TAGNAME);
  245. const frameElement = infobarElement.childNodes[0];
  246. frameElement.style.setProperty("width", "44px", "important");
  247. frameElement.style.setProperty("height", "48px", "important");
  248. }
  249. }
  250. function createElement(tagName, parentElement) {
  251. const element = document.createElement(tagName);
  252. parentElement.appendChild(element);
  253. Array.from(getComputedStyle(element)).forEach(property => element.style.setProperty(property, "initial", "important"));
  254. return element;
  255. }
  256. async function getShadowRoot(element) {
  257. if (element.attachShadow) {
  258. return element.attachShadow({ mode: "open" });
  259. } else {
  260. SHADOW_DOM_SUPPORTED = false;
  261. const iframe = createElement("iframe", element);
  262. iframe.style.setProperty("background-color", "transparent", "important");
  263. iframe.style.setProperty("position", "fixed", "important");
  264. iframe.style.setProperty("top", 0, "important");
  265. iframe.style.setProperty("right", 0, "important");
  266. iframe.style.setProperty("width", "44px", "important");
  267. iframe.style.setProperty("height", "48px", "important");
  268. iframe.style.setProperty("z-index", 2147483647, "important");
  269. return new Promise(resolve => {
  270. iframe.contentDocument.body.style.setProperty("margin", 0);
  271. iframe.onload = () => resolve(iframe.contentDocument.body);
  272. });
  273. }
  274. }
  275. })();