content-ui.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. /*
  2. * Copyright 2018 Gildas Lormeau
  3. * contact : gildas.lormeau <at> gmail.com
  4. *
  5. * This file is part of SingleFile.
  6. *
  7. * SingleFile is free software: you can redistribute it and/or modify
  8. * it under the terms of the GNU Lesser General Public License as published by
  9. * the Free Software Foundation, either version 3 of the License, or
  10. * (at your option) any later version.
  11. *
  12. * SingleFile 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
  15. * GNU Lesser General Public License for more details.
  16. *
  17. * You should have received a copy of the GNU Lesser General Public License
  18. * along with SingleFile. If not, see <http://www.gnu.org/licenses/>.
  19. */
  20. /* global browser, document, getComputedStyle, addEventListener, removeEventListener, requestAnimationFrame, setTimeout */
  21. this.singlefile.ui = this.singlefile.ui || (() => {
  22. const MASK_TAGNAME = "singlefile-mask";
  23. const PROGRESS_BAR_TAGNAME = "singlefile-progress-bar";
  24. const SELECTION_ZONE_TAGNAME = "single-file-selection-zone";
  25. const LOGS_WINDOW_TAGNAME = "singlefile-logs-window";
  26. const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
  27. const SELECT_PX_THRESHOLD = 8;
  28. let selectedAreaElement;
  29. let logsWindowElement = createLogsWindowElement();
  30. const allProperties = new Set();
  31. Array.from(getComputedStyle(document.body)).forEach(property => allProperties.add(property));
  32. return {
  33. getSelectedArea,
  34. onStartPage() {
  35. let maskElement = document.querySelector(MASK_TAGNAME);
  36. if (!maskElement) {
  37. requestAnimationFrame(() => {
  38. const maskElement = createMaskElement();
  39. createProgressBarElement(maskElement);
  40. document.body.appendChild(logsWindowElement);
  41. setLogsWindowStyle();
  42. maskElement.offsetWidth;
  43. maskElement.style.setProperty("background-color", "black", "important");
  44. maskElement.style.setProperty("opacity", .3, "important");
  45. document.body.offsetWidth;
  46. });
  47. }
  48. },
  49. onEndPage() {
  50. const maskElement = document.querySelector(MASK_TAGNAME);
  51. logsWindowElement.remove();
  52. clearLogs();
  53. if (maskElement) {
  54. requestAnimationFrame(() => maskElement.remove());
  55. }
  56. },
  57. onLoadResource(index, maxIndex) {
  58. const progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  59. if (progressBarElement && maxIndex) {
  60. const width = Math.floor((index / maxIndex) * 100) + "%";
  61. if (progressBarElement.style.width != width) {
  62. requestAnimationFrame(() => progressBarElement.style.setProperty("width", Math.floor((index / maxIndex) * 100) + "%", "important"));
  63. }
  64. }
  65. },
  66. onLoadingDeferResources() {
  67. updateLog("load-deferred-images", browser.i18n.getMessage("logPanelDeferredImages"), "…");
  68. },
  69. onLoadDeferResources() {
  70. updateLog("load-deferred-images", browser.i18n.getMessage("logPanelDeferredImages"), "✓");
  71. },
  72. onLoadingFrames() {
  73. updateLog("load-frames", browser.i18n.getMessage("logPanelFrameContents"), "…");
  74. },
  75. onLoadFrames() {
  76. updateLog("load-frames", browser.i18n.getMessage("logPanelFrameContents"), "✓");
  77. },
  78. onStartStage(step) {
  79. updateLog("step-" + step, `${browser.i18n.getMessage("logPanelStep")} ${step + 1} / 3`, "…");
  80. },
  81. onEndStage(step) {
  82. updateLog("step-" + step, `${browser.i18n.getMessage("logPanelStep")} ${step + 1} / 3`, "✓");
  83. },
  84. onPageLoading() { },
  85. onLoadPage() { },
  86. onStartStageTask() { },
  87. onEndStageTask() { }
  88. };
  89. function getSelectedArea() {
  90. return new Promise(resolve => {
  91. addEventListener("mousemove", mousemoveListener, true);
  92. addEventListener("click", clickListener, true);
  93. addEventListener("keyup", keypressListener, true);
  94. document.addEventListener("contextmenu", contextmenuListener, true);
  95. function contextmenuListener(event) {
  96. select();
  97. event.preventDefault();
  98. }
  99. function mousemoveListener(event) {
  100. const targetElement = getTarget(event);
  101. if (targetElement) {
  102. selectedAreaElement = targetElement;
  103. moveAreaSelector(targetElement);
  104. }
  105. }
  106. function clickListener(event) {
  107. select(event.button === 0 ? selectedAreaElement : null);
  108. event.preventDefault();
  109. event.stopPropagation();
  110. }
  111. function keypressListener(event) {
  112. if (event.key == "Escape") {
  113. select();
  114. }
  115. }
  116. function select(selectedElement) {
  117. removeEventListener("mousemove", mousemoveListener, true);
  118. removeEventListener("click", clickListener, true);
  119. removeEventListener("keyup", keypressListener, true);
  120. createAreaSelector().remove();
  121. resolve(selectedElement);
  122. selectedAreaElement = null;
  123. setTimeout(() => document.removeEventListener("contextmenu", contextmenuListener, true), 0);
  124. }
  125. });
  126. }
  127. function getTarget(event) {
  128. let newTarget, target = event.target, boundingRect = target.getBoundingClientRect();
  129. newTarget = determineTargetElementFloor(target, event.clientX - boundingRect.left, getMatchedParents(target, "left"));
  130. if (newTarget == target) {
  131. newTarget = determineTargetElementCeil(target, boundingRect.left + boundingRect.width - event.clientX, getMatchedParents(target, "right"));
  132. }
  133. if (newTarget == target) {
  134. newTarget = determineTargetElementFloor(target, event.clientY - boundingRect.top, getMatchedParents(target, "top"));
  135. }
  136. if (newTarget == target) {
  137. newTarget = determineTargetElementCeil(target, boundingRect.top + boundingRect.height - event.clientY, getMatchedParents(target, "bottom"));
  138. }
  139. target = newTarget;
  140. while (target && target.clientWidth <= SELECT_PX_THRESHOLD && target.clientHeight <= SELECT_PX_THRESHOLD) {
  141. target = target.parentElement;
  142. }
  143. return target;
  144. }
  145. function moveAreaSelector(target) {
  146. requestAnimationFrame(() => {
  147. const selectorElement = createAreaSelector();
  148. const boundingRect = target.getBoundingClientRect();
  149. selectorElement.style.setProperty("top", (document.documentElement.scrollTop + boundingRect.top - 10) + "px");
  150. selectorElement.style.setProperty("left", (document.documentElement.scrollLeft + boundingRect.left - 10) + "px");
  151. selectorElement.style.setProperty("width", (boundingRect.width + 20) + "px");
  152. selectorElement.style.setProperty("height", (boundingRect.height + 20) + "px");
  153. });
  154. }
  155. function createAreaSelector() {
  156. let selectorElement = document.querySelector(SELECTION_ZONE_TAGNAME);
  157. if (!selectorElement) {
  158. selectorElement = createElement(SELECTION_ZONE_TAGNAME, document.body);
  159. selectorElement.style.setProperty("box-sizing", "border-box", "important");
  160. selectorElement.style.setProperty("background-color", "#3ea9d7", "important");
  161. selectorElement.style.setProperty("border", "10px solid #0b4892", "important");
  162. selectorElement.style.setProperty("border-radius", "2px", "important");
  163. selectorElement.style.setProperty("opacity", ".25", "important");
  164. selectorElement.style.setProperty("pointer-events", "none", "important");
  165. selectorElement.style.setProperty("position", "absolute", "important");
  166. selectorElement.style.setProperty("transition", "all 100ms", "important");
  167. selectorElement.style.setProperty("cursor", "pointer", "important");
  168. selectorElement.style.setProperty("z-index", "2147483647", "important");
  169. selectorElement.style.removeProperty("border-inline-end");
  170. selectorElement.style.removeProperty("border-inline-start");
  171. selectorElement.style.removeProperty("inline-size");
  172. selectorElement.style.removeProperty("block-size");
  173. selectorElement.style.removeProperty("inset-block-start");
  174. selectorElement.style.removeProperty("inset-inline-end");
  175. selectorElement.style.removeProperty("inset-block-end");
  176. selectorElement.style.removeProperty("inset-inline-start");
  177. }
  178. return selectorElement;
  179. }
  180. function createMaskElement() {
  181. let maskElement = document.querySelector(MASK_TAGNAME);
  182. if (!maskElement) {
  183. maskElement = createElement(MASK_TAGNAME, document.body);
  184. maskElement.style.setProperty("opacity", 0, "important");
  185. maskElement.style.setProperty("background-color", "transparent", "important");
  186. maskElement.offsetWidth;
  187. maskElement.style.setProperty("position", "fixed", "important");
  188. maskElement.style.setProperty("top", "0", "important");
  189. maskElement.style.setProperty("left", "0", "important");
  190. maskElement.style.setProperty("width", "100%", "important");
  191. maskElement.style.setProperty("height", "100%", "important");
  192. maskElement.style.setProperty("z-index", 2147483646, "important");
  193. maskElement.style.setProperty("transition", "opacity 250ms", "important");
  194. }
  195. return maskElement;
  196. }
  197. function createProgressBarElement(maskElement) {
  198. let progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  199. if (!progressBarElement) {
  200. progressBarElement = createElement(PROGRESS_BAR_TAGNAME, maskElement);
  201. const styleElement = document.createElement("style");
  202. maskElement.appendChild(styleElement);
  203. styleElement.textContent = "@keyframes single-file-progress { 0% { background-position: 0 0 } 100% { background-position: 50px 50px }";
  204. progressBarElement.style.setProperty("background", "white linear-gradient(-45deg, rgba(0, 0, 0, 0.1) 25%, transparent 25%, transparent 50%, rgba(0, 0, 0, 0.1) 50%, rgba(0, 0, 0, 0.1) 75%, transparent 75%, transparent) repeat scroll 0% 0% / 50px 50px padding-box border-box", "important");
  205. progressBarElement.style.setProperty("position", "fixed", "important");
  206. progressBarElement.style.setProperty("top", "0", "important");
  207. progressBarElement.style.setProperty("left", "0", "important");
  208. progressBarElement.style.setProperty("width", "0", "important");
  209. progressBarElement.style.setProperty("height", "8px", "important");
  210. progressBarElement.style.setProperty("transition", "width 50ms", "important");
  211. progressBarElement.style.setProperty("will-change", "width, background-position", "important");
  212. progressBarElement.style.setProperty("animation", "single-file-progress 5s linear infinite reverse", "important");
  213. }
  214. return progressBarElement;
  215. }
  216. function createLogsWindowElement() {
  217. let logsWindowElement = document.querySelector(LOGS_WINDOW_TAGNAME);
  218. if (!logsWindowElement) {
  219. logsWindowElement = document.createElement(LOGS_WINDOW_TAGNAME);
  220. logsWindowElement.className = SINGLE_FILE_UI_ELEMENT_CLASS;
  221. }
  222. const styleElement = document.createElement("style");
  223. logsWindowElement.appendChild(styleElement);
  224. styleElement.textContent = "@keyframes single-file-pulse { 0% { opacity: .5 } 100% { opacity: 1 }";
  225. return logsWindowElement;
  226. }
  227. function setLogsWindowStyle() {
  228. initStyle(logsWindowElement);
  229. logsWindowElement.style.setProperty("opacity", "0.9", "important");
  230. logsWindowElement.style.setProperty("padding", "4px", "important");
  231. logsWindowElement.style.setProperty("position", "fixed", "important");
  232. logsWindowElement.style.setProperty("bottom", "24px", "important");
  233. logsWindowElement.style.setProperty("left", "8px", "important");
  234. logsWindowElement.style.setProperty("z-index", 2147483647, "important");
  235. logsWindowElement.style.setProperty("background-color", "white", "important");
  236. logsWindowElement.style.setProperty("min-width", browser.i18n.getMessage("logPanelWidth") + "px", "important");
  237. logsWindowElement.style.setProperty("min-height", "18px", "important");
  238. logsWindowElement.style.setProperty("transition", "height 100ms", "important");
  239. logsWindowElement.style.setProperty("will-change", "height", "important");
  240. }
  241. function updateLog(id, textContent, textStatus) {
  242. let lineElement = logsWindowElement.querySelector("[data-id='" + id + "']");
  243. if (!lineElement) {
  244. lineElement = createElement("div", logsWindowElement);
  245. lineElement.setAttribute("data-id", id);
  246. lineElement.style.setProperty("display", "flex");
  247. lineElement.style.setProperty("justify-content", "space-between");
  248. const textElement = createElement("span", lineElement);
  249. textElement.style.setProperty("font-size", "13px", "important");
  250. textElement.style.setProperty("font-family", "arial, sans-serif", "important");
  251. textElement.style.setProperty("color", "black", "important");
  252. textElement.style.setProperty("background-color", "white", "important");
  253. textElement.style.setProperty("opacity", "1", "important");
  254. textElement.style.setProperty("transition", "opacity 200ms", "important");
  255. textElement.textContent = textContent;
  256. const statusElement = createElement("span", lineElement);
  257. statusElement.style.setProperty("font-size", "11px", "important");
  258. statusElement.style.setProperty("font-family", "arial, sans-serif", "important");
  259. statusElement.style.setProperty("color", "black", "important");
  260. statusElement.style.setProperty("background-color", "white", "important");
  261. statusElement.style.setProperty("min-width", "15px", "important");
  262. statusElement.style.setProperty("text-align", "center", "important");
  263. statusElement.style.setProperty("will-change", "opacity", "important");
  264. }
  265. updateLogLine(lineElement, textContent, textStatus);
  266. }
  267. function updateLogLine(lineElement, textContent, textStatus) {
  268. const textElement = lineElement.childNodes[0];
  269. const statusElement = lineElement.childNodes[1];
  270. textElement.textContent = textContent;
  271. statusElement.style.setProperty("color", textStatus == "✓" ? "#055000" : "black", "important");
  272. if (textStatus == "✓") {
  273. textElement.style.setProperty("opacity", ".5", "important");
  274. statusElement.style.setProperty("animation", "none", "important");
  275. } else {
  276. statusElement.style.setProperty("opacity", ".5", "important");
  277. statusElement.style.setProperty("animation", "single-file-pulse 1s linear infinite alternate", "important");
  278. }
  279. statusElement.textContent = textStatus;
  280. }
  281. function clearLogs() {
  282. logsWindowElement = createLogsWindowElement();
  283. }
  284. function getMatchedParents(target, property) {
  285. let element = target, matchedParent, parents = [];
  286. do {
  287. const boundingRect = element.getBoundingClientRect();
  288. if (element.parentElement) {
  289. const parentBoundingRect = element.parentElement.getBoundingClientRect();
  290. matchedParent = Math.abs(parentBoundingRect[property] - boundingRect[property]) <= SELECT_PX_THRESHOLD;
  291. if (matchedParent) {
  292. if (element.parentElement.clientWidth > SELECT_PX_THRESHOLD && element.parentElement.clientHeight > SELECT_PX_THRESHOLD &&
  293. ((element.parentElement.clientWidth - element.clientWidth > SELECT_PX_THRESHOLD) || (element.parentElement.clientHeight - element.clientHeight > SELECT_PX_THRESHOLD))) {
  294. parents.push(element.parentElement);
  295. }
  296. element = element.parentElement;
  297. }
  298. } else {
  299. matchedParent = false;
  300. }
  301. } while (matchedParent && element);
  302. return parents;
  303. }
  304. function determineTargetElementCeil(target, widthDistance, parents) {
  305. if (Math.ceil(widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  306. target = parents[parents.length - Math.ceil(widthDistance / SELECT_PX_THRESHOLD) - 1];
  307. }
  308. return target;
  309. }
  310. function determineTargetElementFloor(target, widthDistance, parents) {
  311. if (Math.floor(widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  312. target = parents[parents.length - Math.floor(widthDistance / SELECT_PX_THRESHOLD) - 1];
  313. }
  314. return target;
  315. }
  316. function createElement(tagName, parentElement) {
  317. const element = document.createElement(tagName);
  318. element.className = SINGLE_FILE_UI_ELEMENT_CLASS;
  319. parentElement.appendChild(element);
  320. initStyle(element);
  321. return element;
  322. }
  323. function initStyle(element) {
  324. allProperties.forEach(property => element.style.setProperty(property, "initial", "important"));
  325. }
  326. })();