content-ui.js 20 KB

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