util.js 12 KB

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