YouTube 超快聊天

让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。

当前为 2023-07-02 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name YouTube Super Fast Chat
  3. // @version 0.3.1
  4. // @license MIT
  5. // @name:ja YouTube スーパーファーストチャット
  6. // @name:zh-TW YouTube 超快聊天
  7. // @name:zh-CN YouTube 超快聊天
  8. // @namespace UserScript
  9. // @match https://www.youtube.com/live_chat*
  10. // @author CY Fung
  11. // @run-at document-start
  12. // @grant none
  13. // @unwrap
  14. // @allFrames true
  15. // @inject-into page
  16. //
  17. // @description To make your YouTube Live Chat scroll instantly without smoothing transform CSS
  18. // @description:ja YouTubeライブチャットをスムーズな変形CSSなしで瞬時にスクロールさせるために。
  19. // @description:zh-TW 讓您的 YouTube 直播聊天即時滾動,不經過平滑轉換 CSS。
  20. // @description:zh-CN 让您的 YouTube 直播聊天即时滚动,不经过平滑转换 CSS。
  21. //
  22. // ==/UserScript==
  23.  
  24. ((__CONTEXT__) => {
  25.  
  26. // const ACTIVE_DEFERRED_APPEND = false; // somehow buggy
  27.  
  28. const ACTIVE_CONTENT_VISIBILITY = true;
  29. const ACTIVE_CONTAIN_SIZE = true;
  30.  
  31. const addCss = () => document.head.appendChild(document.createElement('style')).textContent = [
  32. !ACTIVE_CONTENT_VISIBILITY ? '' : `
  33.  
  34. @supports (contain:layout paint style) and (content-visibility:auto) and (contain-intrinsic-size:auto var(--wsr94)) {
  35. [wSr93] {
  36. content-visibility: visible;
  37. }
  38.  
  39. [wSr93="hidden"]:nth-last-child(n+4) {
  40. content-visibility: auto;
  41. contain-intrinsic-size: auto var(--wsr94);
  42. }
  43.  
  44. }
  45.  
  46. `,
  47.  
  48. !ACTIVE_CONTAIN_SIZE ? '' : `
  49.  
  50. @supports (contain:layout paint style) {
  51. [wSr93*="i"] {
  52. height: var(--wsr94);
  53. box-sizing: border-box;
  54. contain: size layout style;
  55. }
  56. }
  57. `,
  58.  
  59. `
  60.  
  61.  
  62. @supports (contain:layout paint style) {
  63. [wSr93*="i"] {
  64. height: var(--wsr94);
  65. box-sizing: border-box;
  66. contain: size layout style;
  67. }
  68.  
  69. /* optional */
  70. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  71. height: auto !important;
  72. min-height: unset !important;
  73. }
  74.  
  75. #items.style-scope.yt-live-chat-item-list-renderer {
  76. transform: translateY(0px) !important;
  77. }
  78.  
  79. /* optional */
  80. yt-icon[icon="down_arrow"] > *, yt-icon-button#show-more > * {
  81. pointer-events: none !important;
  82. }
  83.  
  84. #item-list.style-scope.yt-live-chat-renderer, yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer, #item-list.style-scope.yt-live-chat-renderer *, yt-live-chat-item-list-renderer.style-scope.yt-live-chat-renderer * {
  85. will-change: unset !important;
  86. }
  87.  
  88. yt-img-shadow[height][width] {
  89. content-visibility: visible !important;
  90. }
  91.  
  92. #item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer {
  93. position: static !important;
  94. }
  95.  
  96. /* ------------------------------------------------------------------------------------------------------------- */
  97. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip, yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer, yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image, yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  98. contain: layout style;
  99. }
  100.  
  101. /*
  102. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip,
  103. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer,
  104. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image,
  105. yt-live-chat-author-chip #chat-badges.yt-live-chat-author-chip yt-live-chat-author-badge-renderer #image img {
  106. contain: layout style;
  107. display: inline-flex;
  108. vertical-align: middle;
  109. }
  110. */
  111. #items yt-live-chat-text-message-renderer {
  112. contain: layout style;
  113. }
  114.  
  115. yt-live-chat-item-list-renderer:not([allow-scroll]) #item-scroller.yt-live-chat-item-list-renderer {
  116. overflow-y: scroll;
  117. padding-right: 0;
  118. }
  119.  
  120. body yt-live-chat-app {
  121. contain: size layout paint style;
  122. overflow: hidden;
  123. }
  124.  
  125. #items.style-scope.yt-live-chat-item-list-renderer {
  126. contain: layout paint style;
  127. }
  128.  
  129. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  130. contain: style;
  131. }
  132.  
  133. #item-scroller.style-scope.yt-live-chat-item-list-renderer {
  134. contain: size style;
  135. }
  136.  
  137. #contents.style-scope.yt-live-chat-item-list-renderer, #chat.style-scope.yt-live-chat-renderer, img.style-scope.yt-img-shadow[width][height] {
  138. contain: size layout paint style;
  139. }
  140.  
  141. .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label], .style-scope.yt-live-chat-ticker-renderer[role="button"][aria-label] > #container {
  142. contain: layout paint style;
  143. }
  144.  
  145. yt-live-chat-text-message-renderer.style-scope.yt-live-chat-item-list-renderer, yt-live-chat-membership-item-renderer.style-scope.yt-live-chat-item-list-renderer, yt-live-chat-paid-message-renderer.style-scope.yt-live-chat-item-list-renderer, yt-live-chat-banner-manager.style-scope.yt-live-chat-item-list-renderer {
  146. contain: layout style;
  147. }
  148.  
  149. tp-yt-paper-tooltip[style*="inset"][role="tooltip"] {
  150. contain: layout paint style;
  151. }
  152.  
  153. /*
  154. #item-offset.style-scope.yt-live-chat-item-list-renderer {
  155. position: relative !important;
  156. height: auto !important;
  157. }
  158. */
  159.  
  160. /* ------------------------------------------------------------------------------------------------------------- */
  161.  
  162.  
  163. #items.style-scope.yt-live-chat-item-list-renderer {
  164. padding-top: var(--items-top-padding);
  165. }
  166.  
  167. #continuations, #continuations * {
  168. contain: strict;
  169. position: fixed;
  170. top: 2px;
  171. height: 1px;
  172. width: 2px;
  173. height: 1px;
  174. visibility: collapse;
  175. }
  176.  
  177. }
  178.  
  179.  
  180. `
  181.  
  182.  
  183.  
  184.  
  185. ].join('\n');
  186.  
  187. const { Promise, requestAnimationFrame, IntersectionObserver } = __CONTEXT__;
  188.  
  189.  
  190. const isContainSupport = CSS.supports('contain', 'layout paint style');
  191. if (!isContainSupport) {
  192. console.error(`
  193. YouTube Light Chat Scroll: Your browser does not support 'contain'.
  194. Chrome >= 52; Edge >= 79; Safari >= 15.4, Firefox >= 69; Opera >= 39
  195. `.trim());
  196. return;
  197. }
  198.  
  199. // const APPLY_delayAppendChild = false;
  200.  
  201. let activeDeferredAppendChild = false; // deprecated
  202.  
  203. // let delayedAppendParentWS = new WeakSet();
  204. // let delayedAppendOperations = [];
  205. // let commonAppendParentStackSet = new Set();
  206.  
  207. let firstVisibleItemDetected = false; // deprecated
  208.  
  209. const sp7 = Symbol();
  210.  
  211.  
  212. let dt0 = Date.now() - 2000;
  213. const dateNow = () => Date.now() - dt0;
  214. // let lastScroll = 0;
  215. // let lastLShow = 0;
  216. let lastWheel = 0;
  217.  
  218. const proxyHelperFn = (dummy) => ({
  219.  
  220. get(target, prop) {
  221. return (prop in dummy) ? dummy[prop] : prop === sp7 ? target : target[prop];
  222. },
  223. set(target, prop, value) {
  224. if (!(prop in dummy)) {
  225. target[prop] = value;
  226. }
  227. return true;
  228.  
  229. },
  230. has(target, prop) {
  231. return (prop in target)
  232. },
  233. deleteProperty(target, prop) {
  234. return true;
  235. },
  236. ownKeys(target) {
  237. return Object.keys(target);
  238. },
  239. defineProperty(target, key, descriptor) {
  240. return Object.defineProperty(target, key, descriptor);
  241. },
  242. getOwnPropertyDescriptor(target, key) {
  243. return Object.getOwnPropertyDescriptor(target, key);
  244. },
  245.  
  246. });
  247.  
  248.  
  249. // const dummy3v = {
  250. // "background": "",
  251. // "backgroundAttachment": "",
  252. // "backgroundBlendMode": "",
  253. // "backgroundClip": "",
  254. // "backgroundColor": "",
  255. // "backgroundImage": "",
  256. // "backgroundOrigin": "",
  257. // "backgroundPosition": "",
  258. // "backgroundPositionX": "",
  259. // "backgroundPositionY": "",
  260. // "backgroundRepeat": "",
  261. // "backgroundRepeatX": "",
  262. // "backgroundRepeatY": "",
  263. // "backgroundSize": ""
  264. // };
  265. // for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  266. // dummy3v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  267. // }
  268.  
  269. // const dummy3p = phFn(dummy3v);
  270.  
  271. const pt2DecimalFixer = (x) => Math.round(x * 5, 0) / 5;
  272.  
  273. const tickerContainerSetAttribute = function (attrName, attrValue) {
  274.  
  275. let yd = (this.__dataHost || 0).__data;
  276.  
  277. if (arguments.length === 2 && attrName === 'style' && yd && attrValue) {
  278.  
  279. // let v = yd.containerStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  280. let v = `${attrValue}`;
  281. // conside a ticker is 101px width
  282. // 1% = 1.01px
  283. // 0.2% = 0.202px
  284.  
  285.  
  286. const ratio1 = (yd.ratio * 100);
  287. if (ratio1 > -1) { // avoid NaN
  288.  
  289. const ratio2 = pt2DecimalFixer(ratio1);
  290. v = v.replace(`${ratio1}%`, `${ratio2}%`).replace(`${ratio1}%`, `${ratio2}%`)
  291.  
  292. if (yd.__style_last__ === v) return;
  293. yd.__style_last__ = v;
  294.  
  295. }
  296.  
  297. HTMLElement.prototype.setAttribute.call(this, attrName, v);
  298.  
  299.  
  300. } else {
  301. HTMLElement.prototype.setAttribute.apply(this, arguments);
  302. }
  303.  
  304. };
  305.  
  306. const fxOperator = (proto, propertyName) => {
  307. let propertyDescriptorGetter = null;
  308. try {
  309. propertyDescriptorGetter = Object.getOwnPropertyDescriptor(proto, propertyName).get;
  310. } catch (e) { }
  311. return typeof propertyDescriptorGetter === 'function' ? (e) => propertyDescriptorGetter.call(e) : (e) => e[propertyName];
  312. };
  313.  
  314. const nodeParent = fxOperator(Node.prototype, 'parentNode');
  315. // const nFirstElem = fxOperator(HTMLElement.prototype, 'firstElementChild');
  316. const nPrevElem = fxOperator(HTMLElement.prototype, 'previousElementSibling');
  317. const nNextElem = fxOperator(HTMLElement.prototype, 'nextElementSibling');
  318. const nLastElem = fxOperator(HTMLElement.prototype, 'lastElementChild');
  319.  
  320.  
  321. /* globals WeakRef:false */
  322.  
  323. /** @type {(o: Object | null) => WeakRef | null} */
  324. const mWeakRef = typeof WeakRef === 'function' ? (o => o ? new WeakRef(o) : null) : (o => o || null); // typeof InvalidVar == 'undefined'
  325.  
  326. /** @type {(wr: Object | null) => Object | null} */
  327. const kRef = (wr => (wr && wr.deref) ? wr.deref() : wr);
  328.  
  329. const watchUserCSS = () => {
  330.  
  331. // if (!CSS.supports('contain-intrinsic-size', 'auto var(--wsr94)')) return;
  332.  
  333. const getElemFromWR = (nr) => {
  334. const n = kRef(nr);
  335. if (n && n.isConnected) return n;
  336. return null;
  337. }
  338.  
  339. const clearContentVisibilitySizing = () => {
  340. Promise.resolve().then(() => {
  341.  
  342. let btnShowMoreWR = mWeakRef(document.querySelector('#show-more[disabled]'));
  343.  
  344. let lastVisibleItemWR = null;
  345. for (const elm of document.querySelectorAll('[wSr93]')) {
  346. if (elm.getAttribute('wSr93') === 'visible') lastVisibleItemWR = mWeakRef(elm);
  347. elm.setAttribute('wSr93', '');
  348. // custom CSS property --wsr94 not working when attribute wSr93 removed
  349. }
  350. requestAnimationFrame(() => {
  351. const btnShowMore = getElemFromWR(btnShowMoreWR); btnShowMoreWR = null;
  352. if (btnShowMore) btnShowMore.click();
  353. else {
  354. // would not work if switch it frequently
  355. const lastVisibleItem = getElemFromWR(lastVisibleItemWR); lastVisibleItemWR = null;
  356. if (lastVisibleItem) {
  357.  
  358. Promise.resolve()
  359. .then(() => lastVisibleItem.scrollIntoView())
  360. .then(() => lastVisibleItem.scrollIntoView(false))
  361. .then(() => lastVisibleItem.scrollIntoView({ behavior: "instant", block: "end", inline: "nearest" }))
  362. .catch(e => { }) // break the chain when method not callable
  363.  
  364. }
  365. }
  366. })
  367.  
  368. })
  369.  
  370. }
  371.  
  372. const mutObserver = new MutationObserver((mutations) => {
  373. for (const mutation of mutations) {
  374. if ((mutation.addedNodes || 0).length >= 1) {
  375. for (const addedNode of mutation.addedNodes) {
  376. if (addedNode.nodeName === 'STYLE') {
  377. clearContentVisibilitySizing();
  378. return;
  379. }
  380. }
  381. }
  382. if ((mutation.removedNodes || 0).length >= 1) {
  383. for (const removedNode of mutation.removedNodes) {
  384. if (removedNode.nodeName === 'STYLE') {
  385. clearContentVisibilitySizing();
  386. return;
  387. }
  388. }
  389. }
  390. }
  391. });
  392.  
  393. mutObserver.observe(document.documentElement, {
  394. childList: true,
  395. subtree: false
  396. })
  397.  
  398. mutObserver.observe(document.head, {
  399. childList: true,
  400. subtree: false
  401. })
  402. mutObserver.observe(document.body, {
  403. childList: true,
  404. subtree: false
  405. });
  406.  
  407. }
  408.  
  409. const setupStyle = (m1, m2) => {
  410.  
  411. const dummy1v = {
  412. transform: '',
  413. height: '',
  414. minHeight: '',
  415. paddingBottom: '',
  416. paddingTop: ''
  417. };
  418. for (const k of ['toString', 'getPropertyPriority', 'getPropertyValue', 'item', 'removeProperty', 'setProperty']) {
  419. dummy1v[k] = ((k) => (function () { const style = this[sp7]; return style[k](...arguments); }))(k)
  420. }
  421.  
  422. const dummy1p = proxyHelperFn(dummy1v);
  423. const sp1v = new Proxy(m1.style, dummy1p);
  424. const sp2v = new Proxy(m2.style, dummy1p);
  425. Object.defineProperty(m1, 'style', { get() { return sp1v }, set() { }, enumerable: true, configurable: true });
  426. Object.defineProperty(m2, 'style', { get() { return sp2v }, set() { }, enumerable: true, configurable: true });
  427. m1.removeAttribute("style");
  428. m2.removeAttribute("style");
  429.  
  430. }
  431.  
  432. let done = 0;
  433. let main = async (q) => {
  434.  
  435. if (done) return;
  436.  
  437. if (!q) return;
  438. let m1 = nodeParent(q);
  439. let m2 = q;
  440. if (!(m1 && m1.id === 'item-offset' && m2 && m2.id === 'items')) return;
  441.  
  442. done = 1;
  443.  
  444. Promise.resolve().then(watchUserCSS);
  445.  
  446. addCss();
  447.  
  448. setupStyle(m1, m2);
  449.  
  450. let lcRendererWR = null;
  451.  
  452. const lcRendererElm = () => {
  453. let lcRenderer = kRef(lcRendererWR);
  454. if (!lcRenderer || !lcRenderer.isConnected) {
  455. lcRenderer = document.querySelector('yt-live-chat-item-list-renderer.yt-live-chat-renderer');
  456. lcRendererWR = mWeakRef(lcRenderer);
  457. }
  458. return lcRenderer
  459. };
  460.  
  461. let hasFirstShowMore = false;
  462.  
  463. const visObserver = new IntersectionObserver((entries) => {
  464.  
  465. for (const entry of entries) {
  466.  
  467. const target = entry.target;
  468. if (!target) continue;
  469. let isVisible = entry.isIntersecting === true && entry.intersectionRatio > 0.5;
  470. if (isVisible) {
  471. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  472. target.setAttribute('wSr93', 'visible');
  473. if (nNextElem(target) === null) {
  474. firstVisibleItemDetected = true;
  475. /*
  476. if (dateNow() - lastScroll < 80) {
  477. lastLShow = 0;
  478. lastScroll = 0;
  479. Promise.resolve().then(clickShowMore);
  480. } else {
  481. lastLShow = dateNow();
  482. }
  483. */
  484. // lastLShow = dateNow();
  485. } else if (!hasFirstShowMore) { // should more than one item being visible
  486. // implement inside visObserver to ensure there is sufficient delay
  487. hasFirstShowMore = true;
  488. requestAnimationFrame(() => {
  489. // foreground page
  490. activeDeferredAppendChild = true;
  491. // page visibly ready -> load the latest comments at initial loading
  492. const lcRenderer = lcRendererElm();
  493. lcRenderer.scrollToBottom_();
  494. });
  495. }
  496. }
  497. else if (target.getAttribute('wSr93') === 'visible') { // ignore target.getAttribute('wSr93') === '' to avoid wrong sizing
  498.  
  499. target.style.setProperty('--wsr94', entry.boundingClientRect.height + 'px');
  500. target.setAttribute('wSr93', 'hidden');
  501. } // note: might consider 0 < entry.intersectionRatio < 0.5 and target.getAttribute('wSr93') === '' <new last item>
  502.  
  503. }
  504.  
  505. }, {
  506. /*
  507. root: items,
  508. rootMargin: "0px",
  509. threshold: 1.0,
  510. */
  511. // root: HTMLElement.prototype.closest.call(m2, '#item-scroller.yt-live-chat-item-list-renderer'), // nullable
  512. rootMargin: "0px",
  513. threshold: [0.05, 0.95],
  514. });
  515.  
  516. //m2.style.visibility='';
  517.  
  518. const mutFn = (items) => {
  519. for (let node = nLastElem(items); node !== null; node = nPrevElem(node)) {
  520. if (node.hasAttribute('wSr93')) break;
  521. node.setAttribute('wSr93', '');
  522. visObserver.observe(node);
  523. }
  524. }
  525.  
  526. const mutObserver = new MutationObserver((mutations) => {
  527. const items = (mutations[0] || 0).target;
  528. if (!items) return;
  529. mutFn(items);
  530. });
  531.  
  532. const setupMutObserver = (m2) => {
  533. mutObserver.disconnect();
  534. mutObserver.takeRecords();
  535. if (m2) {
  536. mutObserver.observe(m2, {
  537. childList: true,
  538. subtree: false
  539. });
  540. mutFn(m2);
  541. }
  542. }
  543.  
  544. setupMutObserver(m2);
  545.  
  546.  
  547. const mclp = (customElements.get('yt-live-chat-item-list-renderer') || 0).prototype
  548. if (mclp) {
  549.  
  550. mclp.attached66 = mclp.attached;
  551. mclp.attached = function () {
  552. let m2 = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer');
  553. let m1 = nodeParent(m2);
  554. setupStyle(m1, m2);
  555. setupMutObserver(m2);
  556. return this.attached66();
  557. }
  558.  
  559. mclp.detached66 = mclp.detached;
  560. mclp.detached = function () {
  561. setupMutObserver();
  562. return this.detached66();
  563. }
  564.  
  565. mclp.canScrollToBottom_ = function () {
  566. return this.atBottom && this.allowScroll && !(dateNow() - lastWheel < 80)
  567. }
  568.  
  569. // mclp.scrollToBottom66_ = mclp.scrollToBottom_;
  570. // mclp.scrollToBottom_ = function () {
  571. // this.scrollToBottom66_();
  572. // }
  573. }
  574.  
  575.  
  576. let scrollCount = 0;
  577. document.addEventListener('scroll', (evt) => {
  578. if (!evt || !evt.isTrusted) return;
  579. // lastScroll = dateNow();
  580. if (++scrollCount > 1e9) scrollCount = 9;
  581. }, { passive: true, capture: true }) // support contain => support passive
  582.  
  583. // document.addEventListener('scroll', (evt) => {
  584.  
  585. // if (!evt || !evt.isTrusted) return;
  586. // if (!firstVisibleItemDetected) return;
  587. // const isUserAction = dateNow() - lastWheel < 80; // continuous wheel -> continuous scroll -> continuous wheel -> continuous scroll
  588. // if (!isUserAction) return;
  589. // // lastScroll = dateNow();
  590.  
  591. // }, { passive: true, capture: true }) // support contain => support passive
  592.  
  593.  
  594. let lastScrollCount = -1;
  595. document.addEventListener('wheel', (evt) => {
  596.  
  597. if (!evt || !evt.isTrusted) return;
  598. if (lastScrollCount === scrollCount) return;
  599. lastScrollCount = scrollCount;
  600. lastWheel = dateNow();
  601.  
  602. }, { passive: true, capture: true }) // support contain => support passive
  603.  
  604.  
  605. const fp = (renderer) => {
  606. const container = renderer.$.container;
  607. if (container) {
  608. container.setAttribute = tickerContainerSetAttribute;
  609. }
  610. }
  611. const tags = ["yt-live-chat-ticker-paid-message-item-renderer", "yt-live-chat-ticker-paid-sticker-item-renderer",
  612. "yt-live-chat-ticker-renderer", "yt-live-chat-ticker-sponsor-item-renderer"];
  613. for (const tag of tags) {
  614. const proto = customElements.get(tag).prototype;
  615. proto.attached77 = proto.attached
  616.  
  617. proto.attached = function () {
  618. fp(this);
  619. return this.attached77();
  620. }
  621.  
  622. for (const elm of document.getElementsByTagName(tag)) {
  623. fp(elm);
  624. }
  625. }
  626.  
  627. };
  628.  
  629.  
  630. function onReady() {
  631. let tmObserver = new MutationObserver(() => {
  632.  
  633. let p = document.getElementById('items'); // fast
  634. if (!p) return;
  635. let q = document.querySelector('#item-offset.style-scope.yt-live-chat-item-list-renderer > #items.style-scope.yt-live-chat-item-list-renderer'); // check
  636.  
  637. if (q) {
  638. tmObserver.disconnect();
  639. tmObserver.takeRecords();
  640. tmObserver = null;
  641. Promise.resolve(q).then((q) => {
  642. // confirm Promis.resolve() is resolveable
  643. // execute main without direct blocking
  644. main(q);
  645. })
  646. }
  647.  
  648. });
  649.  
  650. tmObserver.observe(document.body, {
  651. childList: true,
  652. subtree: true
  653. });
  654.  
  655. }
  656.  
  657. if (document.readyState != 'loading') {
  658. onReady();
  659. } else {
  660. window.addEventListener("DOMContentLoaded", onReady, false);
  661. }
  662.  
  663. })({ Promise, requestAnimationFrame, IntersectionObserver });