util.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. //
  2. // Traversing
  3. //
  4. let smoothDistance, smoothDuration, smoothEnd, smoothStart;
  5. this.$ = function (selector, el) {
  6. if (el == null) {
  7. el = document;
  8. }
  9. try {
  10. return el.querySelector(selector);
  11. } catch (error) {}
  12. };
  13. this.$$ = function (selector, el) {
  14. if (el == null) {
  15. el = document;
  16. }
  17. try {
  18. return el.querySelectorAll(selector);
  19. } catch (error) {}
  20. };
  21. $.id = (id) => document.getElementById(id);
  22. $.hasChild = function (parent, el) {
  23. if (!parent) {
  24. return;
  25. }
  26. while (el) {
  27. if (el === parent) {
  28. return true;
  29. }
  30. if (el === document.body) {
  31. return;
  32. }
  33. el = el.parentNode;
  34. }
  35. };
  36. $.closestLink = function (el, parent) {
  37. if (parent == null) {
  38. parent = document.body;
  39. }
  40. while (el) {
  41. if (el.tagName === "A") {
  42. return el;
  43. }
  44. if (el === parent) {
  45. return;
  46. }
  47. el = el.parentNode;
  48. }
  49. };
  50. //
  51. // Events
  52. //
  53. $.on = function (el, event, callback, useCapture) {
  54. if (useCapture == null) {
  55. useCapture = false;
  56. }
  57. if (event.includes(" ")) {
  58. for (var name of event.split(" ")) {
  59. $.on(el, name, callback);
  60. }
  61. } else {
  62. el.addEventListener(event, callback, useCapture);
  63. }
  64. };
  65. $.off = function (el, event, callback, useCapture) {
  66. if (useCapture == null) {
  67. useCapture = false;
  68. }
  69. if (event.includes(" ")) {
  70. for (var name of event.split(" ")) {
  71. $.off(el, name, callback);
  72. }
  73. } else {
  74. el.removeEventListener(event, callback, useCapture);
  75. }
  76. };
  77. $.trigger = function (el, type, canBubble, cancelable) {
  78. const event = new Event(type, {
  79. bubbles: canBubble ?? true,
  80. cancelable: cancelable ?? true,
  81. });
  82. el.dispatchEvent(event);
  83. };
  84. $.click = function (el) {
  85. const event = new MouseEvent("click", {
  86. bubbles: true,
  87. cancelable: true,
  88. });
  89. el.dispatchEvent(event);
  90. };
  91. $.stopEvent = function (event) {
  92. event.preventDefault();
  93. event.stopPropagation();
  94. event.stopImmediatePropagation();
  95. };
  96. $.eventTarget = (event) => event.target.correspondingUseElement || event.target;
  97. //
  98. // Manipulation
  99. //
  100. const buildFragment = function (value) {
  101. const fragment = document.createDocumentFragment();
  102. if ($.isCollection(value)) {
  103. for (var child of $.makeArray(value)) {
  104. fragment.appendChild(child);
  105. }
  106. } else {
  107. fragment.innerHTML = value;
  108. }
  109. return fragment;
  110. };
  111. $.append = function (el, value) {
  112. if (typeof value === "string") {
  113. el.insertAdjacentHTML("beforeend", value);
  114. } else {
  115. if ($.isCollection(value)) {
  116. value = buildFragment(value);
  117. }
  118. el.appendChild(value);
  119. }
  120. };
  121. $.prepend = function (el, value) {
  122. if (!el.firstChild) {
  123. $.append(value);
  124. } else if (typeof value === "string") {
  125. el.insertAdjacentHTML("afterbegin", value);
  126. } else {
  127. if ($.isCollection(value)) {
  128. value = buildFragment(value);
  129. }
  130. el.insertBefore(value, el.firstChild);
  131. }
  132. };
  133. $.before = function (el, value) {
  134. if (typeof value === "string" || $.isCollection(value)) {
  135. value = buildFragment(value);
  136. }
  137. el.parentNode.insertBefore(value, el);
  138. };
  139. $.after = function (el, value) {
  140. if (typeof value === "string" || $.isCollection(value)) {
  141. value = buildFragment(value);
  142. }
  143. if (el.nextSibling) {
  144. el.parentNode.insertBefore(value, el.nextSibling);
  145. } else {
  146. el.parentNode.appendChild(value);
  147. }
  148. };
  149. $.remove = function (value) {
  150. if ($.isCollection(value)) {
  151. for (var el of $.makeArray(value)) {
  152. if (el.parentNode != null) {
  153. el.parentNode.removeChild(el);
  154. }
  155. }
  156. } else {
  157. if (value.parentNode != null) {
  158. value.parentNode.removeChild(value);
  159. }
  160. }
  161. };
  162. $.empty = function (el) {
  163. while (el.firstChild) {
  164. el.removeChild(el.firstChild);
  165. }
  166. };
  167. // Calls the function while the element is off the DOM to avoid triggering
  168. // unnecessary reflows and repaints.
  169. $.batchUpdate = function (el, fn) {
  170. const parent = el.parentNode;
  171. const sibling = el.nextSibling;
  172. parent.removeChild(el);
  173. fn(el);
  174. if (sibling) {
  175. parent.insertBefore(el, sibling);
  176. } else {
  177. parent.appendChild(el);
  178. }
  179. };
  180. //
  181. // Offset
  182. //
  183. $.rect = (el) => el.getBoundingClientRect();
  184. $.offset = function (el, container) {
  185. if (container == null) {
  186. container = document.body;
  187. }
  188. let top = 0;
  189. let left = 0;
  190. while (el && el !== container) {
  191. top += el.offsetTop;
  192. left += el.offsetLeft;
  193. el = el.offsetParent;
  194. }
  195. return {
  196. top,
  197. left,
  198. };
  199. };
  200. $.scrollParent = function (el) {
  201. while ((el = el.parentNode) && el.nodeType === 1) {
  202. if (el.scrollTop > 0) {
  203. break;
  204. }
  205. if (["auto", "scroll"].includes(getComputedStyle(el)?.overflowY ?? "")) {
  206. break;
  207. }
  208. }
  209. return el;
  210. };
  211. $.scrollTo = function (el, parent, position, options) {
  212. if (position == null) {
  213. position = "center";
  214. }
  215. if (options == null) {
  216. options = {};
  217. }
  218. if (!el) {
  219. return;
  220. }
  221. if (parent == null) {
  222. parent = $.scrollParent(el);
  223. }
  224. if (!parent) {
  225. return;
  226. }
  227. const parentHeight = parent.clientHeight;
  228. const parentScrollHeight = parent.scrollHeight;
  229. if (!(parentScrollHeight > parentHeight)) {
  230. return;
  231. }
  232. const { top } = $.offset(el, parent);
  233. const { offsetTop } = parent.firstElementChild;
  234. switch (position) {
  235. case "top":
  236. parent.scrollTop = top - offsetTop - (options.margin || 0);
  237. break;
  238. case "center":
  239. parent.scrollTop =
  240. top - Math.round(parentHeight / 2 - el.offsetHeight / 2);
  241. break;
  242. case "continuous":
  243. var { scrollTop } = parent;
  244. var height = el.offsetHeight;
  245. var lastElementOffset =
  246. parent.lastElementChild.offsetTop +
  247. parent.lastElementChild.offsetHeight;
  248. var offsetBottom =
  249. lastElementOffset > 0 ? parentScrollHeight - lastElementOffset : 0;
  250. // If the target element is above the visible portion of its scrollable
  251. // ancestor, move it near the top with a gap = options.topGap * target's height.
  252. if (top - offsetTop <= scrollTop + height * (options.topGap || 1)) {
  253. parent.scrollTop = top - offsetTop - height * (options.topGap || 1);
  254. // If the target element is below the visible portion of its scrollable
  255. // ancestor, move it near the bottom with a gap = options.bottomGap * target's height.
  256. } else if (
  257. top + offsetBottom >=
  258. scrollTop + parentHeight - height * ((options.bottomGap || 1) + 1)
  259. ) {
  260. parent.scrollTop =
  261. top +
  262. offsetBottom -
  263. parentHeight +
  264. height * ((options.bottomGap || 1) + 1);
  265. }
  266. break;
  267. }
  268. };
  269. $.scrollToWithImageLock = function (el, parent, ...args) {
  270. if (parent == null) {
  271. parent = $.scrollParent(el);
  272. }
  273. if (!parent) {
  274. return;
  275. }
  276. $.scrollTo(el, parent, ...args);
  277. // Lock the scroll position on the target element for up to 3 seconds while
  278. // nearby images are loaded and rendered.
  279. for (var image of parent.getElementsByTagName("img")) {
  280. if (!image.complete) {
  281. (function () {
  282. let timeout;
  283. const onLoad = function (event) {
  284. clearTimeout(timeout);
  285. unbind(event.target);
  286. return $.scrollTo(el, parent, ...args);
  287. };
  288. var unbind = (target) => $.off(target, "load", onLoad);
  289. $.on(image, "load", onLoad);
  290. return (timeout = setTimeout(unbind.bind(null, image), 3000));
  291. })();
  292. }
  293. }
  294. };
  295. // Calls the function while locking the element's position relative to the window.
  296. $.lockScroll = function (el, fn) {
  297. let parent;
  298. if ((parent = $.scrollParent(el))) {
  299. let { top } = $.rect(el);
  300. if (![document.body, document.documentElement].includes(parent)) {
  301. top -= $.rect(parent).top;
  302. }
  303. fn();
  304. parent.scrollTop = $.offset(el, parent).top - top;
  305. } else {
  306. fn();
  307. }
  308. };
  309. let smoothScroll =
  310. (smoothStart =
  311. smoothEnd =
  312. smoothDistance =
  313. smoothDuration =
  314. null);
  315. $.smoothScroll = function (el, end) {
  316. smoothEnd = end;
  317. if (smoothScroll) {
  318. const newDistance = smoothEnd - smoothStart;
  319. smoothDuration += Math.min(300, Math.abs(smoothDistance - newDistance));
  320. smoothDistance = newDistance;
  321. return;
  322. }
  323. smoothStart = el.scrollTop;
  324. smoothDistance = smoothEnd - smoothStart;
  325. smoothDuration = Math.min(300, Math.abs(smoothDistance));
  326. const startTime = Date.now();
  327. smoothScroll = function () {
  328. const p = Math.min(1, (Date.now() - startTime) / smoothDuration);
  329. const y = Math.max(
  330. 0,
  331. Math.floor(
  332. smoothStart +
  333. smoothDistance * (p < 0.5 ? 2 * p * p : p * (4 - p * 2) - 1),
  334. ),
  335. );
  336. el.scrollTop = y;
  337. if (p === 1) {
  338. return (smoothScroll = null);
  339. } else {
  340. return requestAnimationFrame(smoothScroll);
  341. }
  342. };
  343. return requestAnimationFrame(smoothScroll);
  344. };
  345. //
  346. // Utilities
  347. //
  348. $.makeArray = function (object) {
  349. if (Array.isArray(object)) {
  350. return object;
  351. } else {
  352. return Array.prototype.slice.apply(object);
  353. }
  354. };
  355. $.arrayDelete = function (array, object) {
  356. const index = array.indexOf(object);
  357. if (index >= 0) {
  358. array.splice(index, 1);
  359. return true;
  360. } else {
  361. return false;
  362. }
  363. };
  364. // Returns true if the object is an array or a collection of DOM elements.
  365. $.isCollection = (object) =>
  366. Array.isArray(object) || typeof object?.item === "function";
  367. const ESCAPE_HTML_MAP = {
  368. "&": "&amp;",
  369. "<": "&lt;",
  370. ">": "&gt;",
  371. '"': "&quot;",
  372. "'": "&#x27;",
  373. "/": "&#x2F;",
  374. };
  375. const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
  376. $.escape = (string) =>
  377. string.replace(ESCAPE_HTML_REGEXP, (match) => ESCAPE_HTML_MAP[match]);
  378. const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
  379. $.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1");
  380. $.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20"));
  381. $.classify = function (string) {
  382. string = string.split("_");
  383. for (let i = 0; i < string.length; i++) {
  384. var substr = string[i];
  385. string[i] = substr[0].toUpperCase() + substr.slice(1);
  386. }
  387. return string.join("");
  388. };
  389. //
  390. // Miscellaneous
  391. //
  392. $.noop = function () {};
  393. $.popup = function (value) {
  394. try {
  395. const win = window.open();
  396. if (win.opener) {
  397. win.opener = null;
  398. }
  399. win.location = value.href || value;
  400. } catch (error) {
  401. window.open(value.href || value, "_blank");
  402. }
  403. };
  404. let isMac = null;
  405. $.isMac = () =>
  406. isMac != null ? isMac : (isMac = navigator.userAgent.includes("Mac"));
  407. let isIE = null;
  408. $.isIE = () =>
  409. isIE != null
  410. ? isIE
  411. : (isIE =
  412. navigator.userAgent.includes("MSIE") ||
  413. navigator.userAgent.includes("rv:11.0"));
  414. let isChromeForAndroid = null;
  415. $.isChromeForAndroid = () =>
  416. isChromeForAndroid != null
  417. ? isChromeForAndroid
  418. : (isChromeForAndroid =
  419. navigator.userAgent.includes("Android") &&
  420. /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
  421. let isAndroid = null;
  422. $.isAndroid = () =>
  423. isAndroid != null
  424. ? isAndroid
  425. : (isAndroid = navigator.userAgent.includes("Android"));
  426. let isIOS = null;
  427. $.isIOS = () =>
  428. isIOS != null
  429. ? isIOS
  430. : (isIOS =
  431. navigator.userAgent.includes("iPhone") ||
  432. navigator.userAgent.includes("iPad"));
  433. $.overlayScrollbarsEnabled = function () {
  434. if (!$.isMac()) {
  435. return false;
  436. }
  437. const div = document.createElement("div");
  438. div.setAttribute(
  439. "style",
  440. "width: 100px; height: 100px; overflow: scroll; position: absolute",
  441. );
  442. document.body.appendChild(div);
  443. const result = div.offsetWidth === div.clientWidth;
  444. document.body.removeChild(div);
  445. return result;
  446. };
  447. const HIGHLIGHT_DEFAULTS = {
  448. className: "highlight",
  449. delay: 1000,
  450. };
  451. $.highlight = function (el, options) {
  452. options = { ...HIGHLIGHT_DEFAULTS, ...(options || {}) };
  453. el.classList.add(options.className);
  454. setTimeout(() => el.classList.remove(options.className), options.delay);
  455. };
  456. $.copyToClipboard = function (string) {
  457. let result;
  458. const textarea = document.createElement("textarea");
  459. textarea.style.position = "fixed";
  460. textarea.style.opacity = 0;
  461. textarea.value = string;
  462. document.body.appendChild(textarea);
  463. try {
  464. textarea.select();
  465. result = !!document.execCommand("copy");
  466. } catch (error) {
  467. result = false;
  468. } finally {
  469. document.body.removeChild(textarea);
  470. }
  471. return result;
  472. };