util.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585
  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. if (!window.requestAnimationFrame) {
  335. el.scrollTop = end;
  336. return;
  337. }
  338. smoothEnd = end;
  339. if (smoothScroll) {
  340. const newDistance = smoothEnd - smoothStart;
  341. smoothDuration += Math.min(300, Math.abs(smoothDistance - newDistance));
  342. smoothDistance = newDistance;
  343. return;
  344. }
  345. smoothStart = el.scrollTop;
  346. smoothDistance = smoothEnd - smoothStart;
  347. smoothDuration = Math.min(300, Math.abs(smoothDistance));
  348. const startTime = Date.now();
  349. smoothScroll = function () {
  350. const p = Math.min(1, (Date.now() - startTime) / smoothDuration);
  351. const y = Math.max(
  352. 0,
  353. Math.floor(
  354. smoothStart +
  355. smoothDistance * (p < 0.5 ? 2 * p * p : p * (4 - p * 2) - 1),
  356. ),
  357. );
  358. el.scrollTop = y;
  359. if (p === 1) {
  360. return (smoothScroll = null);
  361. } else {
  362. return requestAnimationFrame(smoothScroll);
  363. }
  364. };
  365. return requestAnimationFrame(smoothScroll);
  366. };
  367. //
  368. // Utilities
  369. //
  370. $.makeArray = function (object) {
  371. if (Array.isArray(object)) {
  372. return object;
  373. } else {
  374. return Array.prototype.slice.apply(object);
  375. }
  376. };
  377. $.arrayDelete = function (array, object) {
  378. const index = array.indexOf(object);
  379. if (index >= 0) {
  380. array.splice(index, 1);
  381. return true;
  382. } else {
  383. return false;
  384. }
  385. };
  386. // Returns true if the object is an array or a collection of DOM elements.
  387. $.isCollection = (object) =>
  388. Array.isArray(object) || typeof object?.item === "function";
  389. const ESCAPE_HTML_MAP = {
  390. "&": "&amp;",
  391. "<": "&lt;",
  392. ">": "&gt;",
  393. '"': "&quot;",
  394. "'": "&#x27;",
  395. "/": "&#x2F;",
  396. };
  397. const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
  398. $.escape = (string) =>
  399. string.replace(ESCAPE_HTML_REGEXP, (match) => ESCAPE_HTML_MAP[match]);
  400. const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
  401. $.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1");
  402. $.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20"));
  403. $.classify = function (string) {
  404. string = string.split("_");
  405. for (let i = 0; i < string.length; i++) {
  406. var substr = string[i];
  407. string[i] = substr[0].toUpperCase() + substr.slice(1);
  408. }
  409. return string.join("");
  410. };
  411. $.framify = function (fn, obj) {
  412. if (window.requestAnimationFrame) {
  413. return (...args) => requestAnimationFrame(fn.bind(obj, ...args));
  414. } else {
  415. return fn;
  416. }
  417. };
  418. $.requestAnimationFrame = function (fn) {
  419. if (window.requestAnimationFrame) {
  420. requestAnimationFrame(fn);
  421. } else {
  422. setTimeout(fn, 0);
  423. }
  424. };
  425. //
  426. // Miscellaneous
  427. //
  428. $.noop = function () {};
  429. $.popup = function (value) {
  430. try {
  431. const win = window.open();
  432. if (win.opener) {
  433. win.opener = null;
  434. }
  435. win.location = value.href || value;
  436. } catch (error) {
  437. window.open(value.href || value, "_blank");
  438. }
  439. };
  440. let isMac = null;
  441. $.isMac = () =>
  442. isMac != null ? isMac : (isMac = navigator.userAgent.includes("Mac"));
  443. let isIE = null;
  444. $.isIE = () =>
  445. isIE != null
  446. ? isIE
  447. : (isIE =
  448. navigator.userAgent.includes("MSIE") ||
  449. navigator.userAgent.includes("rv:11.0"));
  450. let isChromeForAndroid = null;
  451. $.isChromeForAndroid = () =>
  452. isChromeForAndroid != null
  453. ? isChromeForAndroid
  454. : (isChromeForAndroid =
  455. navigator.userAgent.includes("Android") &&
  456. /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
  457. let isAndroid = null;
  458. $.isAndroid = () =>
  459. isAndroid != null
  460. ? isAndroid
  461. : (isAndroid = navigator.userAgent.includes("Android"));
  462. let isIOS = null;
  463. $.isIOS = () =>
  464. isIOS != null
  465. ? isIOS
  466. : (isIOS =
  467. navigator.userAgent.includes("iPhone") ||
  468. navigator.userAgent.includes("iPad"));
  469. $.overlayScrollbarsEnabled = function () {
  470. if (!$.isMac()) {
  471. return false;
  472. }
  473. const div = document.createElement("div");
  474. div.setAttribute(
  475. "style",
  476. "width: 100px; height: 100px; overflow: scroll; position: absolute",
  477. );
  478. document.body.appendChild(div);
  479. const result = div.offsetWidth === div.clientWidth;
  480. document.body.removeChild(div);
  481. return result;
  482. };
  483. const HIGHLIGHT_DEFAULTS = {
  484. className: "highlight",
  485. delay: 1000,
  486. };
  487. $.highlight = function (el, options) {
  488. options = { ...HIGHLIGHT_DEFAULTS, ...(options || {}) };
  489. el.classList.add(options.className);
  490. setTimeout(() => el.classList.remove(options.className), options.delay);
  491. };
  492. $.copyToClipboard = function (string) {
  493. let result;
  494. const textarea = document.createElement("textarea");
  495. textarea.style.position = "fixed";
  496. textarea.style.opacity = 0;
  497. textarea.value = string;
  498. document.body.appendChild(textarea);
  499. try {
  500. textarea.select();
  501. result = !!document.execCommand("copy");
  502. } catch (error) {
  503. result = false;
  504. } finally {
  505. document.body.removeChild(textarea);
  506. }
  507. return result;
  508. };