content-ui.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320
  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 document, getComputedStyle, addEventListener, removeEventListener, requestAnimationFrame, scrollX, scrollY, 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 SELECT_PX_THRESHOLD = 8;
  27. let selectedAreaElement;
  28. const logsWindowElement = createLogsWindowElement();
  29. return {
  30. getSelectedArea,
  31. onStartPage() {
  32. let maskElement = document.querySelector(MASK_TAGNAME);
  33. if (!maskElement) {
  34. requestAnimationFrame(() => {
  35. const maskElement = createMaskElement();
  36. createProgressBarElement(maskElement);
  37. document.body.appendChild(logsWindowElement);
  38. setLogsWindowStyle();
  39. maskElement.offsetWidth;
  40. maskElement.style.setProperty("background-color", "black", "important");
  41. maskElement.style.setProperty("opacity", .3, "important");
  42. document.body.offsetWidth;
  43. });
  44. }
  45. },
  46. onEndPage() {
  47. const maskElement = document.querySelector(MASK_TAGNAME);
  48. logsWindowElement.remove();
  49. clearLogs();
  50. if (maskElement) {
  51. requestAnimationFrame(() => maskElement.remove());
  52. }
  53. },
  54. onLoadResource(index, maxIndex) {
  55. const progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  56. if (progressBarElement && maxIndex) {
  57. const width = Math.floor((index / maxIndex) * 100) + "%";
  58. if (progressBarElement.style.width != width) {
  59. requestAnimationFrame(() => progressBarElement.style.setProperty("width", Math.floor((index / maxIndex) * 100) + "%", "important"));
  60. }
  61. }
  62. },
  63. onLoadingDeferResources() {
  64. appendLog("Deferred images", "…");
  65. },
  66. onLoadDeferResources() {
  67. appendLog("Deferred images", "✓");
  68. },
  69. onLoadingFrames() {
  70. appendLog("Frame contents", "…");
  71. },
  72. onLoadFrames() {
  73. appendLog("Frame contents", "✓");
  74. },
  75. onStartStage(step) {
  76. appendLog(`Step ${step + 1} / 4`, "…");
  77. },
  78. onEndStage(step) {
  79. appendLog(`Step ${step + 1} / 4`, "✓");
  80. },
  81. onPageLoading() { },
  82. onLoadPage() { },
  83. onStartStageTask() { },
  84. onEndStageTask() { }
  85. };
  86. function appendLog(textContent, textStatus) {
  87. const lineElement = createElement("div", logsWindowElement);
  88. lineElement.style.setProperty("display", "flex");
  89. lineElement.style.setProperty("justify-content", "space-between");
  90. const textElement = createElement("span", lineElement);
  91. textElement.style.setProperty("font-size", "13px", "important");
  92. textElement.style.setProperty("font-family", "arial, sans-serif", "important");
  93. textElement.style.setProperty("color", "black", "important");
  94. textElement.style.setProperty("background-color", "white", "important");
  95. textElement.textContent = textContent;
  96. const statusElement = createElement("span", lineElement);
  97. statusElement.style.setProperty("font-size", "13px", "important");
  98. statusElement.style.setProperty("font-family", "arial, sans-serif", "important");
  99. statusElement.style.setProperty("color", "black", "important");
  100. statusElement.style.setProperty("background-color", "white", "important");
  101. statusElement.style.setProperty("min-width", "15px", "important");
  102. statusElement.style.setProperty("text-align", "center", "important");
  103. statusElement.textContent = textStatus;
  104. }
  105. function clearLogs() {
  106. logsWindowElement.childNodes.forEach(node => node.remove());
  107. }
  108. function getSelectedArea() {
  109. return new Promise(resolve => {
  110. addEventListener("mousemove", mousemoveListener, true);
  111. addEventListener("click", clickListener, true);
  112. addEventListener("keyup", keypressListener, true);
  113. document.addEventListener("contextmenu", contextmenuListener, true);
  114. function contextmenuListener(event) {
  115. select();
  116. event.preventDefault();
  117. }
  118. function mousemoveListener(event) {
  119. const targetElement = getTarget(event);
  120. if (targetElement) {
  121. selectedAreaElement = targetElement;
  122. moveAreaSelector(targetElement);
  123. }
  124. }
  125. function clickListener(event) {
  126. select(event.button === 0 ? selectedAreaElement : null);
  127. event.preventDefault();
  128. event.stopPropagation();
  129. }
  130. function keypressListener(event) {
  131. if (event.key == "Escape") {
  132. select();
  133. }
  134. }
  135. function select(selectedElement) {
  136. removeEventListener("mousemove", mousemoveListener, true);
  137. removeEventListener("click", clickListener, true);
  138. removeEventListener("keyup", keypressListener, true);
  139. createAreaSelector().remove();
  140. resolve(selectedElement);
  141. selectedAreaElement = null;
  142. setTimeout(() => document.removeEventListener("contextmenu", contextmenuListener, true), 0);
  143. }
  144. });
  145. }
  146. function getTarget(event) {
  147. let newTarget, target = event.target, boundingRect = target.getBoundingClientRect();
  148. newTarget = determineTargetElementFloor(target, event.clientX - boundingRect.left, getMatchedParents(target, "left"));
  149. if (newTarget == target) {
  150. newTarget = determineTargetElementCeil(target, boundingRect.left + boundingRect.width - event.clientX, getMatchedParents(target, "right"));
  151. }
  152. if (newTarget == target) {
  153. newTarget = determineTargetElementFloor(target, event.clientY - boundingRect.top, getMatchedParents(target, "top"));
  154. }
  155. if (newTarget == target) {
  156. newTarget = determineTargetElementCeil(target, boundingRect.top + boundingRect.height - event.clientY, getMatchedParents(target, "bottom"));
  157. }
  158. target = newTarget;
  159. while (target && target.clientWidth <= SELECT_PX_THRESHOLD && target.clientHeight <= SELECT_PX_THRESHOLD) {
  160. target = target.parentElement;
  161. }
  162. return target;
  163. }
  164. function moveAreaSelector(target) {
  165. requestAnimationFrame(() => {
  166. const selectorElement = createAreaSelector();
  167. const boundingRect = target.getBoundingClientRect();
  168. selectorElement.style.setProperty("top", (scrollY + boundingRect.top - 10) + "px");
  169. selectorElement.style.setProperty("left", (scrollX + boundingRect.left - 10) + "px");
  170. selectorElement.style.setProperty("width", (boundingRect.width + 20) + "px");
  171. selectorElement.style.setProperty("height", (boundingRect.height + 20) + "px");
  172. });
  173. }
  174. function createAreaSelector() {
  175. let selectorElement = document.querySelector(SELECTION_ZONE_TAGNAME);
  176. if (!selectorElement) {
  177. selectorElement = createElement(SELECTION_ZONE_TAGNAME, document.body);
  178. selectorElement.style.setProperty("box-sizing", "border-box", "important");
  179. selectorElement.style.setProperty("background-color", "#3ea9d7", "important");
  180. selectorElement.style.setProperty("border", "10px solid #0b4892", "important");
  181. selectorElement.style.setProperty("border-radius", "2px", "important");
  182. selectorElement.style.setProperty("opacity", ".25", "important");
  183. selectorElement.style.setProperty("pointer-events", "none", "important");
  184. selectorElement.style.setProperty("position", "absolute", "important");
  185. selectorElement.style.setProperty("transition", "all 100ms", "important");
  186. selectorElement.style.setProperty("cursor", "pointer", "important");
  187. selectorElement.style.setProperty("z-index", "2147483647", "important");
  188. selectorElement.style.removeProperty("border-inline-end");
  189. selectorElement.style.removeProperty("border-inline-start");
  190. selectorElement.style.removeProperty("inline-size");
  191. selectorElement.style.removeProperty("block-size");
  192. selectorElement.style.removeProperty("inset-block-start");
  193. selectorElement.style.removeProperty("inset-inline-end");
  194. selectorElement.style.removeProperty("inset-block-end");
  195. selectorElement.style.removeProperty("inset-inline-start");
  196. }
  197. return selectorElement;
  198. }
  199. function createMaskElement() {
  200. let maskElement = document.querySelector(MASK_TAGNAME);
  201. if (!maskElement) {
  202. maskElement = createElement(MASK_TAGNAME, document.body);
  203. maskElement.style.setProperty("opacity", 0, "important");
  204. maskElement.style.setProperty("background-color", "transparent", "important");
  205. maskElement.offsetWidth;
  206. maskElement.style.setProperty("position", "fixed", "important");
  207. maskElement.style.setProperty("top", "0", "important");
  208. maskElement.style.setProperty("left", "0", "important");
  209. maskElement.style.setProperty("width", "100%", "important");
  210. maskElement.style.setProperty("height", "100%", "important");
  211. maskElement.style.setProperty("z-index", 2147483646, "important");
  212. maskElement.style.setProperty("transition", "opacity 250ms", "important");
  213. }
  214. return maskElement;
  215. }
  216. function createProgressBarElement(maskElement) {
  217. let progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  218. if (!progressBarElement) {
  219. progressBarElement = createElement(PROGRESS_BAR_TAGNAME, maskElement);
  220. progressBarElement.style.setProperty("background-color", "white", "important");
  221. progressBarElement.style.setProperty("position", "fixed", "important");
  222. progressBarElement.style.setProperty("top", "0", "important");
  223. progressBarElement.style.setProperty("left", "0", "important");
  224. progressBarElement.style.setProperty("width", "0", "important");
  225. progressBarElement.style.setProperty("height", "8px", "important");
  226. progressBarElement.style.setProperty("transition", "width 100ms", "important");
  227. progressBarElement.style.setProperty("will-change", "width", "important");
  228. }
  229. return progressBarElement;
  230. }
  231. function createLogsWindowElement() {
  232. let logsWindowElement = document.querySelector(LOGS_WINDOW_TAGNAME);
  233. if (!logsWindowElement) {
  234. logsWindowElement = document.createElement(LOGS_WINDOW_TAGNAME);
  235. }
  236. return logsWindowElement;
  237. }
  238. function setLogsWindowStyle() {
  239. initStyle(logsWindowElement);
  240. logsWindowElement.style.setProperty("opacity", "0.9", "important");
  241. logsWindowElement.style.setProperty("padding", "4px", "important");
  242. logsWindowElement.style.setProperty("position", "fixed", "important");
  243. logsWindowElement.style.setProperty("bottom", "8px", "important");
  244. logsWindowElement.style.setProperty("left", "8px", "important");
  245. logsWindowElement.style.setProperty("z-index", 2147483647, "important");
  246. logsWindowElement.style.setProperty("background-color", "white", "important");
  247. logsWindowElement.style.setProperty("min-width", "120px", "important");
  248. logsWindowElement.style.setProperty("min-height", "16px", "important");
  249. logsWindowElement.style.setProperty("transition", "height 100ms", "important");
  250. logsWindowElement.style.setProperty("will-change", "height", "important");
  251. }
  252. function getMatchedParents(target, property) {
  253. let element = target, matchedParent, parents = [];
  254. do {
  255. const boundingRect = element.getBoundingClientRect();
  256. if (element.parentElement) {
  257. const parentBoundingRect = element.parentElement.getBoundingClientRect();
  258. matchedParent = Math.abs(parentBoundingRect[property] - boundingRect[property]) <= SELECT_PX_THRESHOLD;
  259. if (matchedParent) {
  260. if (element.parentElement.clientWidth > SELECT_PX_THRESHOLD && element.parentElement.clientHeight > SELECT_PX_THRESHOLD &&
  261. ((element.parentElement.clientWidth - element.clientWidth > SELECT_PX_THRESHOLD) || (element.parentElement.clientHeight - element.clientHeight > SELECT_PX_THRESHOLD))) {
  262. parents.push(element.parentElement);
  263. }
  264. element = element.parentElement;
  265. }
  266. } else {
  267. matchedParent = false;
  268. }
  269. } while (matchedParent && element);
  270. return parents;
  271. }
  272. function determineTargetElementCeil(target, widthDistance, parents) {
  273. if (Math.ceil(widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  274. target = parents[parents.length - Math.ceil(widthDistance / SELECT_PX_THRESHOLD) - 1];
  275. }
  276. return target;
  277. }
  278. function determineTargetElementFloor(target, widthDistance, parents) {
  279. if (Math.floor(widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  280. target = parents[parents.length - Math.floor(widthDistance / SELECT_PX_THRESHOLD) - 1];
  281. }
  282. return target;
  283. }
  284. function createElement(tagName, parentElement) {
  285. const element = document.createElement(tagName);
  286. parentElement.appendChild(element);
  287. initStyle(element);
  288. return element;
  289. }
  290. function initStyle(element) {
  291. Array.from(getComputedStyle(element)).forEach(property => element.style.setProperty(property, "initial", "important"));
  292. }
  293. })();