content-ui.js 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  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-var";
  24. const SELECTION_ZONE_TAGNAME = "single-file-selection-zone";
  25. const SELECT_PX_THRESHOLD = 8;
  26. let selectedAreaElement;
  27. return {
  28. init() {
  29. let maskElement = document.querySelector(MASK_TAGNAME);
  30. if (!maskElement) {
  31. requestAnimationFrame(() => {
  32. const maskElement = createMaskElement();
  33. createProgressBarElement(maskElement);
  34. maskElement.offsetWidth;
  35. maskElement.style.setProperty("background-color", "black", "important");
  36. maskElement.style.setProperty("opacity", .3, "important");
  37. document.body.offsetWidth;
  38. });
  39. }
  40. },
  41. onprogress(index, maxIndex) {
  42. const progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  43. if (progressBarElement && maxIndex) {
  44. const width = Math.floor((index / maxIndex) * 100) + "%";
  45. if (progressBarElement.style.width != width) {
  46. requestAnimationFrame(() => progressBarElement.style.setProperty("width", Math.floor((index / maxIndex) * 100) + "%", "important"));
  47. }
  48. }
  49. },
  50. end() {
  51. const maskElement = document.querySelector(MASK_TAGNAME);
  52. if (maskElement) {
  53. requestAnimationFrame(() => maskElement.remove());
  54. }
  55. },
  56. getSelectedArea
  57. };
  58. function getSelectedArea() {
  59. return new Promise(resolve => {
  60. addEventListener("mousemove", mousemoveListener, true);
  61. addEventListener("click", clickListener, true);
  62. addEventListener("keyup", keypressListener, true);
  63. document.addEventListener("contextmenu", contextmenuListener, true);
  64. function contextmenuListener(event) {
  65. select();
  66. event.preventDefault();
  67. }
  68. function mousemoveListener(event) {
  69. const targetElement = getTarget(event);
  70. if (targetElement) {
  71. selectedAreaElement = targetElement;
  72. moveAreaSelector(targetElement);
  73. }
  74. }
  75. function clickListener(event) {
  76. select(event.button === 0 ? selectedAreaElement : null);
  77. event.preventDefault();
  78. event.stopPropagation();
  79. }
  80. function keypressListener(event) {
  81. if (event.key == "Escape") {
  82. select();
  83. }
  84. }
  85. function select(selectedElement) {
  86. removeEventListener("mousemove", mousemoveListener, true);
  87. removeEventListener("click", clickListener, true);
  88. removeEventListener("keyup", keypressListener, true);
  89. createAreaSelector().remove();
  90. resolve(selectedElement);
  91. selectedAreaElement = null;
  92. setTimeout(() => document.removeEventListener("contextmenu", contextmenuListener, true), 0);
  93. }
  94. });
  95. }
  96. function getTarget(event) {
  97. let newTarget, target = event.target, boundingRect = target.getBoundingClientRect();
  98. newTarget = determineTargetElementFloor(target, event.clientX - boundingRect.left, getMatchedParents(target, "left"));
  99. if (newTarget == target) {
  100. newTarget = determineTargetElementCeil(target, boundingRect.left + boundingRect.width - event.clientX, getMatchedParents(target, "right"));
  101. }
  102. if (newTarget == target) {
  103. newTarget = determineTargetElementFloor(target, event.clientY - boundingRect.top, getMatchedParents(target, "top"));
  104. }
  105. if (newTarget == target) {
  106. newTarget = determineTargetElementCeil(target, boundingRect.top + boundingRect.height - event.clientY, getMatchedParents(target, "bottom"));
  107. }
  108. target = newTarget;
  109. while (target && target.clientWidth <= SELECT_PX_THRESHOLD && target.clientHeight <= SELECT_PX_THRESHOLD) {
  110. target = target.parentElement;
  111. }
  112. return target;
  113. }
  114. function moveAreaSelector(target) {
  115. requestAnimationFrame(() => {
  116. const selectorElement = createAreaSelector();
  117. const boundingRect = target.getBoundingClientRect();
  118. selectorElement.style.setProperty("top", (scrollY + boundingRect.top - 10) + "px");
  119. selectorElement.style.setProperty("left", (scrollX + boundingRect.left - 10) + "px");
  120. selectorElement.style.setProperty("width", (boundingRect.width + 20) + "px");
  121. selectorElement.style.setProperty("height", (boundingRect.height + 20) + "px");
  122. });
  123. }
  124. function createAreaSelector() {
  125. let selectorElement = document.querySelector(SELECTION_ZONE_TAGNAME);
  126. if (!selectorElement) {
  127. selectorElement = createElement(SELECTION_ZONE_TAGNAME, document.body);
  128. selectorElement.style.setProperty("box-sizing", "border-box", "important");
  129. selectorElement.style.setProperty("background-color", "#3ea9d7", "important");
  130. selectorElement.style.setProperty("border", "10px solid #0b4892", "important");
  131. selectorElement.style.setProperty("border-radius", "2px", "important");
  132. selectorElement.style.setProperty("opacity", ".25", "important");
  133. selectorElement.style.setProperty("pointer-events", "none", "important");
  134. selectorElement.style.setProperty("position", "absolute", "important");
  135. selectorElement.style.setProperty("transition", "all 100ms", "important");
  136. selectorElement.style.setProperty("cursor", "pointer", "important");
  137. selectorElement.style.setProperty("z-index", "2147483647", "important");
  138. selectorElement.style.removeProperty("border-inline-end");
  139. selectorElement.style.removeProperty("border-inline-start");
  140. selectorElement.style.removeProperty("inline-size");
  141. selectorElement.style.removeProperty("block-size");
  142. selectorElement.style.removeProperty("inset-block-start");
  143. selectorElement.style.removeProperty("inset-inline-end");
  144. selectorElement.style.removeProperty("inset-block-end");
  145. selectorElement.style.removeProperty("inset-inline-start");
  146. }
  147. return selectorElement;
  148. }
  149. function createMaskElement() {
  150. let maskElement = document.querySelector(MASK_TAGNAME);
  151. if (!maskElement) {
  152. maskElement = createElement(MASK_TAGNAME, document.body);
  153. maskElement.style.setProperty("opacity", 0, "important");
  154. maskElement.style.setProperty("background-color", "transparent", "important");
  155. maskElement.offsetWidth;
  156. maskElement.style.setProperty("position", "fixed", "important");
  157. maskElement.style.setProperty("top", "0", "important");
  158. maskElement.style.setProperty("left", "0", "important");
  159. maskElement.style.setProperty("width", "100%", "important");
  160. maskElement.style.setProperty("height", "100%", "important");
  161. maskElement.style.setProperty("z-index", 2147483647, "important");
  162. maskElement.style.setProperty("transition", "opacity 250ms", "important");
  163. }
  164. return maskElement;
  165. }
  166. function createProgressBarElement(maskElement) {
  167. let progressBarElement = document.querySelector(PROGRESS_BAR_TAGNAME);
  168. if (!progressBarElement) {
  169. progressBarElement = createElement(PROGRESS_BAR_TAGNAME, maskElement);
  170. progressBarElement.style.setProperty("background-color", "white", "important");
  171. progressBarElement.style.setProperty("position", "fixed", "important");
  172. progressBarElement.style.setProperty("top", "0", "important");
  173. progressBarElement.style.setProperty("left", "0", "important");
  174. progressBarElement.style.setProperty("width", "0", "important");
  175. progressBarElement.style.setProperty("height", "8px", "important");
  176. progressBarElement.style.setProperty("transition", "width 100ms", "important");
  177. progressBarElement.style.setProperty("will-change", "width", "important");
  178. }
  179. return progressBarElement;
  180. }
  181. function getMatchedParents(target, property) {
  182. let element = target, matchedParent, parents = [];
  183. do {
  184. const boundingRect = element.getBoundingClientRect();
  185. if (element.parentElement) {
  186. const parentBoundingRect = element.parentElement.getBoundingClientRect();
  187. matchedParent = Math.abs(parentBoundingRect[property] - boundingRect[property]) <= SELECT_PX_THRESHOLD;
  188. if (matchedParent) {
  189. if (element.parentElement.clientWidth > SELECT_PX_THRESHOLD && element.parentElement.clientHeight > SELECT_PX_THRESHOLD &&
  190. ((element.parentElement.clientWidth - element.clientWidth > SELECT_PX_THRESHOLD) || (element.parentElement.clientHeight - element.clientHeight > SELECT_PX_THRESHOLD))) {
  191. parents.push(element.parentElement);
  192. }
  193. element = element.parentElement;
  194. }
  195. } else {
  196. matchedParent = false;
  197. }
  198. } while (matchedParent && element);
  199. return parents;
  200. }
  201. function determineTargetElementCeil(target, widthDistance, parents) {
  202. if (Math.ceil(widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  203. target = parents[parents.length - Math.ceil(widthDistance / SELECT_PX_THRESHOLD) - 1];
  204. }
  205. return target;
  206. }
  207. function determineTargetElementFloor(target, widthDistance, parents) {
  208. if (Math.floor(widthDistance / SELECT_PX_THRESHOLD) <= parents.length) {
  209. target = parents[parents.length - Math.floor(widthDistance / SELECT_PX_THRESHOLD) - 1];
  210. }
  211. return target;
  212. }
  213. function createElement(tagName, parentElement) {
  214. const element = document.createElement(tagName);
  215. parentElement.appendChild(element);
  216. Array.from(getComputedStyle(element)).forEach(property => element.style.setProperty(property, "initial", "important"));
  217. return element;
  218. }
  219. })();