util.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600
  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. $.extend = function (target, ...objects) {
  371. for (var object of objects) {
  372. if (object) {
  373. for (var key in object) {
  374. var value = object[key];
  375. target[key] = value;
  376. }
  377. }
  378. }
  379. return target;
  380. };
  381. $.makeArray = function (object) {
  382. if (Array.isArray(object)) {
  383. return object;
  384. } else {
  385. return Array.prototype.slice.apply(object);
  386. }
  387. };
  388. $.arrayDelete = function (array, object) {
  389. const index = array.indexOf(object);
  390. if (index >= 0) {
  391. array.splice(index, 1);
  392. return true;
  393. } else {
  394. return false;
  395. }
  396. };
  397. // Returns true if the object is an array or a collection of DOM elements.
  398. $.isCollection = (object) =>
  399. Array.isArray(object) || typeof object?.item === "function";
  400. const ESCAPE_HTML_MAP = {
  401. "&": "&amp;",
  402. "<": "&lt;",
  403. ">": "&gt;",
  404. '"': "&quot;",
  405. "'": "&#x27;",
  406. "/": "&#x2F;",
  407. };
  408. const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
  409. $.escape = (string) =>
  410. string.replace(ESCAPE_HTML_REGEXP, (match) => ESCAPE_HTML_MAP[match]);
  411. const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
  412. $.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1");
  413. $.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20"));
  414. $.classify = function (string) {
  415. string = string.split("_");
  416. for (let i = 0; i < string.length; i++) {
  417. var substr = string[i];
  418. string[i] = substr[0].toUpperCase() + substr.slice(1);
  419. }
  420. return string.join("");
  421. };
  422. $.framify = function (fn, obj) {
  423. if (window.requestAnimationFrame) {
  424. return (...args) => requestAnimationFrame(fn.bind(obj, ...args));
  425. } else {
  426. return fn;
  427. }
  428. };
  429. $.requestAnimationFrame = function (fn) {
  430. if (window.requestAnimationFrame) {
  431. requestAnimationFrame(fn);
  432. } else {
  433. setTimeout(fn, 0);
  434. }
  435. };
  436. //
  437. // Miscellaneous
  438. //
  439. $.noop = function () {};
  440. $.popup = function (value) {
  441. try {
  442. const win = window.open();
  443. if (win.opener) {
  444. win.opener = null;
  445. }
  446. win.location = value.href || value;
  447. } catch (error) {
  448. window.open(value.href || value, "_blank");
  449. }
  450. };
  451. let isMac = null;
  452. $.isMac = () =>
  453. isMac != null ? isMac : (isMac = navigator.userAgent.includes("Mac"));
  454. let isIE = null;
  455. $.isIE = () =>
  456. isIE != null
  457. ? isIE
  458. : (isIE =
  459. navigator.userAgent.includes("MSIE") ||
  460. navigator.userAgent.includes("rv:11.0"));
  461. let isChromeForAndroid = null;
  462. $.isChromeForAndroid = () =>
  463. isChromeForAndroid != null
  464. ? isChromeForAndroid
  465. : (isChromeForAndroid =
  466. navigator.userAgent.includes("Android") &&
  467. /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
  468. let isAndroid = null;
  469. $.isAndroid = () =>
  470. isAndroid != null
  471. ? isAndroid
  472. : (isAndroid = navigator.userAgent.includes("Android"));
  473. let isIOS = null;
  474. $.isIOS = () =>
  475. isIOS != null
  476. ? isIOS
  477. : (isIOS =
  478. navigator.userAgent.includes("iPhone") ||
  479. navigator.userAgent.includes("iPad"));
  480. $.overlayScrollbarsEnabled = function () {
  481. if (!$.isMac()) {
  482. return false;
  483. }
  484. const div = document.createElement("div");
  485. div.setAttribute(
  486. "style",
  487. "width: 100px; height: 100px; overflow: scroll; position: absolute",
  488. );
  489. document.body.appendChild(div);
  490. const result = div.offsetWidth === div.clientWidth;
  491. document.body.removeChild(div);
  492. return result;
  493. };
  494. const HIGHLIGHT_DEFAULTS = {
  495. className: "highlight",
  496. delay: 1000,
  497. };
  498. $.highlight = function (el, options) {
  499. if (options == null) {
  500. options = {};
  501. }
  502. options = $.extend({}, HIGHLIGHT_DEFAULTS, options);
  503. el.classList.add(options.className);
  504. setTimeout(() => el.classList.remove(options.className), options.delay);
  505. };
  506. $.copyToClipboard = function (string) {
  507. let result;
  508. const textarea = document.createElement("textarea");
  509. textarea.style.position = "fixed";
  510. textarea.style.opacity = 0;
  511. textarea.value = string;
  512. document.body.appendChild(textarea);
  513. try {
  514. textarea.select();
  515. result = !!document.execCommand("copy");
  516. } catch (error) {
  517. result = false;
  518. } finally {
  519. document.body.removeChild(textarea);
  520. }
  521. return result;
  522. };