content-ui.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /*
  2. * Copyright 2010-2019 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 SingleFileBrowser, browser, document, prompt, getComputedStyle, addEventListener, removeEventListener, requestAnimationFrame, setTimeout, getSelection, Node */
  21. this.singlefile.ui = this.singlefile.ui || (() => {
  22. const SingleFile = SingleFileBrowser.getClass();
  23. const MASK_TAGNAME = "singlefile-mask";
  24. const PROGRESS_BAR_TAGNAME = "singlefile-progress-bar";
  25. const PROGRESS_CURSOR_TAGNAME = "singlefile-progress-cursor";
  26. const SELECTION_ZONE_TAGNAME = "single-file-selection-zone";
  27. const LOGS_WINDOW_TAGNAME = "singlefile-logs-window";
  28. const LOGS_LINE_TAGNAME = "singlefile-logs-line";
  29. const LOGS_LINE_ELEMENT_TAGNAME = "singlefile-logs-element";
  30. const SINGLE_FILE_UI_ELEMENT_CLASS = "single-file-ui-element";
  31. const SELECT_PX_THRESHOLD = 8;
  32. let selectedAreaElement;
  33. let logsWindowElement = createLogsWindowElement();
  34. const allProperties = new Set();
  35. Array.from(getComputedStyle(document.body)).forEach(property => allProperties.add(property));
  36. return {
  37. markSelection,
  38. unmarkSelection,
  39. prompt(message, defaultValue) {
  40. return prompt(message, defaultValue);
  41. },
  42. onStartPage() {
  43. let maskElement = document.querySelector(MASK_TAGNAME);
  44. if (!maskElement) {
  45. const maskElement = createMaskElement();
  46. createProgressBarElement(maskElement);
  47. document.body.appendChild(logsWindowElement);
  48. setLogsWindowStyle();
  49. maskElement.offsetWidth;
  50. maskElement.style.setProperty("background-color", "black", "important");
  51. maskElement.style.setProperty("opacity", .3, "important");
  52. document.body.offsetWidth;
  53. }
  54. },
  55. onEndPage() {
  56. const maskElement = document.querySelector(MASK_TAGNAME);
  57. logsWindowElement.remove();
  58. clearLogs();
  59. if (maskElement) {
  60. maskElement.remove();
  61. }
  62. },
  63. onLoadResource(index, maxIndex) {
  64. const progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  65. if (progressBarElement && maxIndex) {
  66. const width = Math.floor((index / maxIndex) * 100) + "%";
  67. if (progressBarElement.style.width != width) {
  68. requestAnimationFrame(() => progressBarElement.style.setProperty("width", Math.floor((index / maxIndex) * 100) + "%", "important"));
  69. }
  70. }
  71. },
  72. onLoadingDeferResources() {
  73. updateLog("load-deferred-images", browser.i18n.getMessage("logPanelDeferredImages"), "…");
  74. },
  75. onLoadDeferResources() {
  76. updateLog("load-deferred-images", browser.i18n.getMessage("logPanelDeferredImages"), "✓");
  77. },
  78. onLoadingFrames() {
  79. updateLog("load-frames", browser.i18n.getMessage("logPanelFrameContents"), "…");
  80. },
  81. onLoadFrames() {
  82. updateLog("load-frames", browser.i18n.getMessage("logPanelFrameContents"), "✓");
  83. },
  84. onStartStage(step) {
  85. updateLog("step-" + step, `${browser.i18n.getMessage("logPanelStep")} ${step + 1} / 3`, "…");
  86. },
  87. onEndStage(step) {
  88. updateLog("step-" + step, `${browser.i18n.getMessage("logPanelStep")} ${step + 1} / 3`, "✓");
  89. },
  90. onPageLoading() { },
  91. onLoadPage() { },
  92. onStartStageTask() { },
  93. onEndStageTask() { }
  94. };
  95. async function markSelection() {
  96. let selectionFound = markSelectedContent();
  97. if (selectionFound) {
  98. return selectionFound;
  99. } else {
  100. const selectedArea = await getSelectedArea();
  101. if (selectedArea) {
  102. markSelectedArea(selectedArea);
  103. selectionFound = true;
  104. }
  105. return selectionFound;
  106. }
  107. }
  108. function markSelectedContent() {
  109. const selection = getSelection();
  110. let selectionFound;
  111. for (let indexRange = 0; indexRange < selection.rangeCount; indexRange++) {
  112. let range = selection.getRangeAt(indexRange);
  113. if (range && range.commonAncestorContainer) {
  114. const treeWalker = document.createTreeWalker(range.commonAncestorContainer);
  115. markSelectedParents(treeWalker.currentNode);
  116. if (treeWalker.currentNode == range.endContainer) {
  117. selectionFound = true;
  118. markSelectedNode(treeWalker.currentNode);
  119. treeWalker.currentNode.querySelectorAll("*").forEach(descendantElement => markSelectedNode(descendantElement));
  120. } else {
  121. let rangeSelectionFound = false;
  122. while (treeWalker.currentNode != range.endContainer) {
  123. if (rangeSelectionFound || treeWalker.currentNode == range.startContainer || treeWalker.currentNode == range.endContainer) {
  124. rangeSelectionFound = true;
  125. selectionFound = true;
  126. markSelectedNode(treeWalker.currentNode);
  127. }
  128. treeWalker.nextNode();
  129. }
  130. }
  131. }
  132. }
  133. return selectionFound;
  134. }
  135. function markSelectedNode(node) {
  136. const element = node.nodeType == Node.ELEMENT_NODE ? node : node.parentElement;
  137. element.setAttribute(SingleFile.SELECTED_CONTENT_ATTRIBUTE_NAME, "");
  138. }
  139. function markSelectedParents(node) {
  140. if (node.parentElement) {
  141. markSelectedNode(node);
  142. markSelectedParents(node.parentElement);
  143. }
  144. }
  145. function markSelectedArea(selectedAreaElement) {
  146. selectedAreaElement.querySelectorAll("*").forEach(element => markSelectedNode(element));
  147. }
  148. function unmarkSelection() {
  149. document.querySelectorAll("[" + SingleFile.SELECTED_CONTENT_ATTRIBUTE_NAME + "]").forEach(selectedContent => selectedContent.removeAttribute(SingleFile.SELECTED_CONTENT_ATTRIBUTE_NAME));
  150. }
  151. function getSelectedArea() {
  152. return new Promise(resolve => {
  153. addEventListener("mousemove", mousemoveListener, true);
  154. addEventListener("click", clickListener, true);
  155. addEventListener("keyup", keypressListener, true);
  156. document.addEventListener("contextmenu", contextmenuListener, true);
  157. function contextmenuListener(event) {
  158. select();
  159. event.preventDefault();
  160. }
  161. function mousemoveListener(event) {
  162. const targetElement = getTarget(event);
  163. if (targetElement) {
  164. selectedAreaElement = targetElement;
  165. moveAreaSelector(targetElement);
  166. }
  167. }
  168. function clickListener(event) {
  169. select(event.button === 0 ? selectedAreaElement : null);
  170. event.preventDefault();
  171. event.stopPropagation();
  172. }
  173. function keypressListener(event) {
  174. if (event.key == "Escape") {
  175. select();
  176. }
  177. }
  178. function select(selectedElement) {
  179. removeEventListener("mousemove", mousemoveListener, true);
  180. removeEventListener("click", clickListener, true);
  181. removeEventListener("keyup", keypressListener, true);
  182. createAreaSelector().remove();
  183. resolve(selectedElement);
  184. selectedAreaElement = null;
  185. setTimeout(() => document.removeEventListener("contextmenu", contextmenuListener, true), 0);
  186. }
  187. });
  188. }
  189. function getTarget(event) {
  190. let newTarget, target = event.target, boundingRect = target.getBoundingClientRect();
  191. newTarget = determineTargetElement("floor", target, event.clientX - boundingRect.left, getMatchedParents(target, "left"));
  192. if (newTarget == target) {
  193. newTarget = determineTargetElement("ceil", target, boundingRect.left + boundingRect.width - event.clientX, getMatchedParents(target, "right"));
  194. }
  195. if (newTarget == target) {
  196. newTarget = determineTargetElement("floor", target, event.clientY - boundingRect.top, getMatchedParents(target, "top"));
  197. }
  198. if (newTarget == target) {
  199. newTarget = determineTargetElement("ceil", target, boundingRect.top + boundingRect.height - event.clientY, getMatchedParents(target, "bottom"));
  200. }
  201. target = newTarget;
  202. while (target && target.clientWidth <= SELECT_PX_THRESHOLD && target.clientHeight <= SELECT_PX_THRESHOLD) {
  203. target = target.parentElement;
  204. }
  205. return target;
  206. }
  207. function moveAreaSelector(target) {
  208. requestAnimationFrame(() => {
  209. const selectorElement = createAreaSelector();
  210. const boundingRect = target.getBoundingClientRect();
  211. selectorElement.style.setProperty("top", (document.documentElement.scrollTop + boundingRect.top - 10) + "px");
  212. selectorElement.style.setProperty("left", (document.documentElement.scrollLeft + boundingRect.left - 10) + "px");
  213. selectorElement.style.setProperty("width", (boundingRect.width + 20) + "px");
  214. selectorElement.style.setProperty("height", (boundingRect.height + 20) + "px");
  215. });
  216. }
  217. function createAreaSelector() {
  218. let selectorElement = document.querySelector(SELECTION_ZONE_TAGNAME);
  219. if (!selectorElement) {
  220. selectorElement = createElement(SELECTION_ZONE_TAGNAME, document.body);
  221. selectorElement.style.setProperty("box-sizing", "border-box", "important");
  222. selectorElement.style.setProperty("background-color", "#3ea9d7", "important");
  223. selectorElement.style.setProperty("border", "10px solid #0b4892", "important");
  224. selectorElement.style.setProperty("border-radius", "2px", "important");
  225. selectorElement.style.setProperty("opacity", ".25", "important");
  226. selectorElement.style.setProperty("pointer-events", "none", "important");
  227. selectorElement.style.setProperty("position", "absolute", "important");
  228. selectorElement.style.setProperty("transition", "all 100ms", "important");
  229. selectorElement.style.setProperty("cursor", "pointer", "important");
  230. selectorElement.style.setProperty("z-index", "2147483647", "important");
  231. selectorElement.style.removeProperty("border-inline-end");
  232. selectorElement.style.removeProperty("border-inline-start");
  233. selectorElement.style.removeProperty("inline-size");
  234. selectorElement.style.removeProperty("block-size");
  235. selectorElement.style.removeProperty("inset-block-start");
  236. selectorElement.style.removeProperty("inset-inline-end");
  237. selectorElement.style.removeProperty("inset-block-end");
  238. selectorElement.style.removeProperty("inset-inline-start");
  239. }
  240. return selectorElement;
  241. }
  242. function createMaskElement() {
  243. let maskElement = document.querySelector(MASK_TAGNAME);
  244. if (!maskElement) {
  245. maskElement = createElement(MASK_TAGNAME, document.body);
  246. maskElement.style.setProperty("opacity", 0, "important");
  247. maskElement.style.setProperty("background-color", "transparent", "important");
  248. maskElement.offsetWidth;
  249. maskElement.style.setProperty("position", "fixed", "important");
  250. maskElement.style.setProperty("top", "0", "important");
  251. maskElement.style.setProperty("left", "0", "important");
  252. maskElement.style.setProperty("width", "100%", "important");
  253. maskElement.style.setProperty("height", "100%", "important");
  254. maskElement.style.setProperty("z-index", 2147483646, "important");
  255. maskElement.style.setProperty("transition", "opacity 250ms", "important");
  256. }
  257. return maskElement;
  258. }
  259. function createProgressBarElement(maskElement) {
  260. let progressBarElementContainer = document.querySelector(PROGRESS_BAR_TAGNAME);
  261. if (!progressBarElementContainer) {
  262. progressBarElementContainer = createElement(PROGRESS_BAR_TAGNAME, maskElement);
  263. const styleElement = document.createElement("style");
  264. styleElement.textContent = "@keyframes single-file-progress { 0% { left: -50px } 100% { left: 0 }";
  265. maskElement.appendChild(styleElement);
  266. progressBarElementContainer.style.setProperty("position", "fixed", "important");
  267. progressBarElementContainer.style.setProperty("top", "0", "important");
  268. progressBarElementContainer.style.setProperty("left", "0", "important");
  269. progressBarElementContainer.style.setProperty("width", "0", "important");
  270. progressBarElementContainer.style.setProperty("height", "8px", "important");
  271. progressBarElementContainer.style.setProperty("overflow", "hidden", "important");
  272. progressBarElementContainer.style.setProperty("transition", "width 200ms", "important");
  273. progressBarElementContainer.style.setProperty("will-change", "width", "important");
  274. const progressBarElement = createElement(PROGRESS_CURSOR_TAGNAME, progressBarElementContainer);
  275. progressBarElement.style.setProperty("position", "absolute", "important");
  276. progressBarElement.style.setProperty("left", "0");
  277. progressBarElement.style.setProperty("animation", "single-file-progress 5s linear infinite reverse", "important");
  278. 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");
  279. progressBarElement.style.setProperty("width", "calc(100% + 50px)", "important");
  280. progressBarElement.style.setProperty("height", "100%", "important");
  281. progressBarElement.style.setProperty("inset-inline-start", "auto");
  282. }
  283. return progressBarElementContainer;
  284. }
  285. function createLogsWindowElement() {
  286. let logsWindowElement = document.querySelector(LOGS_WINDOW_TAGNAME);
  287. if (!logsWindowElement) {
  288. logsWindowElement = document.createElement(LOGS_WINDOW_TAGNAME);
  289. logsWindowElement.className = SINGLE_FILE_UI_ELEMENT_CLASS;
  290. }
  291. const styleElement = document.createElement("style");
  292. logsWindowElement.appendChild(styleElement);
  293. styleElement.textContent = "@keyframes single-file-pulse { 0% { opacity: .5 } 100% { opacity: 1 }";
  294. return logsWindowElement;
  295. }
  296. function setLogsWindowStyle() {
  297. initStyle(logsWindowElement);
  298. logsWindowElement.style.setProperty("opacity", "0.9", "important");
  299. logsWindowElement.style.setProperty("padding", "4px", "important");
  300. logsWindowElement.style.setProperty("position", "fixed", "important");
  301. logsWindowElement.style.setProperty("bottom", "24px", "important");
  302. logsWindowElement.style.setProperty("left", "8px", "important");
  303. logsWindowElement.style.setProperty("z-index", 2147483647, "important");
  304. logsWindowElement.style.setProperty("background-color", "white", "important");
  305. logsWindowElement.style.setProperty("min-width", browser.i18n.getMessage("logPanelWidth") + "px", "important");
  306. logsWindowElement.style.setProperty("min-height", "16px", "important");
  307. logsWindowElement.style.setProperty("transition", "height 100ms", "important");
  308. logsWindowElement.style.setProperty("will-change", "height", "important");
  309. }
  310. function updateLog(id, textContent, textStatus) {
  311. let lineElement = logsWindowElement.querySelector("[data-id='" + id + "']");
  312. if (!lineElement) {
  313. lineElement = createElement(LOGS_LINE_TAGNAME, logsWindowElement);
  314. lineElement.setAttribute("data-id", id);
  315. lineElement.style.setProperty("display", "flex");
  316. lineElement.style.setProperty("justify-content", "space-between");
  317. const textElement = createElement(LOGS_LINE_ELEMENT_TAGNAME, lineElement);
  318. textElement.style.setProperty("font-size", "13px", "important");
  319. textElement.style.setProperty("font-family", "arial, sans-serif", "important");
  320. textElement.style.setProperty("color", "black", "important");
  321. textElement.style.setProperty("background-color", "white", "important");
  322. textElement.style.setProperty("opacity", "1", "important");
  323. textElement.style.setProperty("transition", "opacity 200ms", "important");
  324. textElement.textContent = textContent;
  325. const statusElement = createElement(LOGS_LINE_ELEMENT_TAGNAME, lineElement);
  326. statusElement.style.setProperty("font-size", "11px", "important");
  327. statusElement.style.setProperty("font-family", "arial, sans-serif", "important");
  328. statusElement.style.setProperty("color", "black", "important");
  329. statusElement.style.setProperty("background-color", "white", "important");
  330. statusElement.style.setProperty("min-width", "15px", "important");
  331. statusElement.style.setProperty("text-align", "center", "important");
  332. statusElement.style.setProperty("will-change", "opacity", "important");
  333. }
  334. updateLogLine(lineElement, textContent, textStatus);
  335. }
  336. function updateLogLine(lineElement, textContent, textStatus) {
  337. const textElement = lineElement.childNodes[0];
  338. const statusElement = lineElement.childNodes[1];
  339. textElement.textContent = textContent;
  340. statusElement.style.setProperty("color", textStatus == "✓" ? "#055000" : "black", "important");
  341. if (textStatus == "✓") {
  342. textElement.style.setProperty("opacity", ".5", "important");
  343. statusElement.style.setProperty("animation", "none", "important");
  344. } else {
  345. statusElement.style.setProperty("opacity", ".5", "important");
  346. statusElement.style.setProperty("animation", "single-file-pulse 1s linear infinite alternate", "important");
  347. }
  348. statusElement.textContent = textStatus;
  349. }
  350. function clearLogs() {
  351. logsWindowElement = createLogsWindowElement();
  352. }
  353. function getMatchedParents(target, property) {
  354. let element = target, matchedParent, parents = [];
  355. do {
  356. const boundingRect = element.getBoundingClientRect();
  357. if (element.parentElement) {
  358. const parentBoundingRect = element.parentElement.getBoundingClientRect();
  359. matchedParent = Math.abs(parentBoundingRect[property] - boundingRect[property]) <= SELECT_PX_THRESHOLD;
  360. if (matchedParent) {
  361. if (element.parentElement.clientWidth > SELECT_PX_THRESHOLD && element.parentElement.clientHeight > SELECT_PX_THRESHOLD &&
  362. ((element.parentElement.clientWidth - element.clientWidth > SELECT_PX_THRESHOLD) || (element.parentElement.clientHeight - element.clientHeight > SELECT_PX_THRESHOLD))) {
  363. parents.push(element.parentElement);
  364. }
  365. element = element.parentElement;
  366. }
  367. } else {
  368. matchedParent = false;
  369. }
  370. } while (matchedParent && element);
  371. return parents;
  372. }
  373. function determineTargetElement(roundingMethod, target, widthDistance, parents) {
  374. if (Math[roundingMethod](widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  375. target = parents[parents.length - Math[roundingMethod](widthDistance / SELECT_PX_THRESHOLD) - 1];
  376. }
  377. return target;
  378. }
  379. function createElement(tagName, parentElement) {
  380. const element = document.createElement(tagName);
  381. element.className = SINGLE_FILE_UI_ELEMENT_CLASS;
  382. parentElement.appendChild(element);
  383. initStyle(element);
  384. return element;
  385. }
  386. function initStyle(element) {
  387. allProperties.forEach(property => element.style.setProperty(property, "initial", "important"));
  388. }
  389. })();