util.js 14 KB

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