util.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  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.indexOf(" ") >= 0) {
  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.indexOf(" ") >= 0) {
  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) ||
  400. typeof (object != null ? object.item : undefined) === "function";
  401. const ESCAPE_HTML_MAP = {
  402. "&": "&amp;",
  403. "<": "&lt;",
  404. ">": "&gt;",
  405. '"': "&quot;",
  406. "'": "&#x27;",
  407. "/": "&#x2F;",
  408. };
  409. const ESCAPE_HTML_REGEXP = /[&<>"'\/]/g;
  410. $.escape = (string) =>
  411. string.replace(ESCAPE_HTML_REGEXP, (match) => ESCAPE_HTML_MAP[match]);
  412. const ESCAPE_REGEXP = /([.*+?^=!:${}()|\[\]\/\\])/g;
  413. $.escapeRegexp = (string) => string.replace(ESCAPE_REGEXP, "\\$1");
  414. $.urlDecode = (string) => decodeURIComponent(string.replace(/\+/g, "%20"));
  415. $.classify = function (string) {
  416. string = string.split("_");
  417. for (let i = 0; i < string.length; i++) {
  418. var substr = string[i];
  419. string[i] = substr[0].toUpperCase() + substr.slice(1);
  420. }
  421. return string.join("");
  422. };
  423. $.framify = function (fn, obj) {
  424. if (window.requestAnimationFrame) {
  425. return (...args) => requestAnimationFrame(fn.bind(obj, ...args));
  426. } else {
  427. return fn;
  428. }
  429. };
  430. $.requestAnimationFrame = function (fn) {
  431. if (window.requestAnimationFrame) {
  432. requestAnimationFrame(fn);
  433. } else {
  434. setTimeout(fn, 0);
  435. }
  436. };
  437. //
  438. // Miscellaneous
  439. //
  440. $.noop = function () {};
  441. $.popup = function (value) {
  442. try {
  443. const win = window.open();
  444. if (win.opener) {
  445. win.opener = null;
  446. }
  447. win.location = value.href || value;
  448. } catch (error) {
  449. window.open(value.href || value, "_blank");
  450. }
  451. };
  452. let isMac = null;
  453. $.isMac = () =>
  454. isMac != null
  455. ? isMac
  456. : (isMac =
  457. (navigator.userAgent != null
  458. ? navigator.userAgent.indexOf("Mac")
  459. : undefined) >= 0);
  460. let isIE = null;
  461. $.isIE = () =>
  462. isIE != null
  463. ? isIE
  464. : (isIE =
  465. (navigator.userAgent != null
  466. ? navigator.userAgent.indexOf("MSIE")
  467. : undefined) >= 0 ||
  468. (navigator.userAgent != null
  469. ? navigator.userAgent.indexOf("rv:11.0")
  470. : undefined) >= 0);
  471. let isChromeForAndroid = null;
  472. $.isChromeForAndroid = () =>
  473. isChromeForAndroid != null
  474. ? isChromeForAndroid
  475. : (isChromeForAndroid =
  476. (navigator.userAgent != null
  477. ? navigator.userAgent.indexOf("Android")
  478. : undefined) >= 0 &&
  479. /Chrome\/([.0-9])+ Mobile/.test(navigator.userAgent));
  480. let isAndroid = null;
  481. $.isAndroid = () =>
  482. isAndroid != null
  483. ? isAndroid
  484. : (isAndroid =
  485. (navigator.userAgent != null
  486. ? navigator.userAgent.indexOf("Android")
  487. : undefined) >= 0);
  488. let isIOS = null;
  489. $.isIOS = () =>
  490. isIOS != null
  491. ? isIOS
  492. : (isIOS =
  493. (navigator.userAgent != null
  494. ? navigator.userAgent.indexOf("iPhone")
  495. : undefined) >= 0 ||
  496. (navigator.userAgent != null
  497. ? navigator.userAgent.indexOf("iPad")
  498. : undefined) >= 0);
  499. $.overlayScrollbarsEnabled = function () {
  500. if (!$.isMac()) {
  501. return false;
  502. }
  503. const div = document.createElement("div");
  504. div.setAttribute(
  505. "style",
  506. "width: 100px; height: 100px; overflow: scroll; position: absolute",
  507. );
  508. document.body.appendChild(div);
  509. const result = div.offsetWidth === div.clientWidth;
  510. document.body.removeChild(div);
  511. return result;
  512. };
  513. const HIGHLIGHT_DEFAULTS = {
  514. className: "highlight",
  515. delay: 1000,
  516. };
  517. $.highlight = function (el, options) {
  518. if (options == null) {
  519. options = {};
  520. }
  521. options = $.extend({}, HIGHLIGHT_DEFAULTS, options);
  522. el.classList.add(options.className);
  523. setTimeout(() => el.classList.remove(options.className), options.delay);
  524. };
  525. $.copyToClipboard = function (string) {
  526. let result;
  527. const textarea = document.createElement("textarea");
  528. textarea.style.position = "fixed";
  529. textarea.style.opacity = 0;
  530. textarea.value = string;
  531. document.body.appendChild(textarea);
  532. try {
  533. textarea.select();
  534. result = !!document.execCommand("copy");
  535. } catch (error) {
  536. result = false;
  537. } finally {
  538. document.body.removeChild(textarea);
  539. }
  540. return result;
  541. };