page.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. var IV = {
  2. notify: function(message) {
  3. if (window.external && window.external.invoke) {
  4. window.external.invoke(JSON.stringify(message));
  5. }
  6. },
  7. frameClickHandler: function(e) {
  8. var target = e.target;
  9. var context = '';
  10. while (target) {
  11. if (target.id == 'menu_page_blocker') {
  12. IV.notify({ event: 'menu_page_blocker_click' });
  13. IV.menuShown(false);
  14. return;
  15. }
  16. if (target.tagName == 'AUDIO' || target.tagName == 'VIDEO') {
  17. return;
  18. }
  19. if (context === ''
  20. && target.hasAttribute
  21. && target.hasAttribute('data-context')) {
  22. context = String(target.getAttribute('data-context'));
  23. }
  24. if (target.tagName == 'A') {
  25. break;
  26. }
  27. target = target.parentNode;
  28. }
  29. if (!target || (context === '' && !target.hasAttribute('href'))) {
  30. return;
  31. }
  32. var base = document.createElement('A');
  33. base.href = window.location.href;
  34. if (base.origin != target.origin
  35. || base.pathname != target.pathname
  36. || base.search != target.search) {
  37. IV.notify({
  38. event: 'link_click',
  39. url: target.href,
  40. context: context,
  41. });
  42. } else if (target.hash.length < 2) {
  43. IV.jumpToHash('');
  44. } else {
  45. IV.jumpToHash(decodeURIComponent(target.hash.substr(1)));
  46. }
  47. e.preventDefault();
  48. },
  49. getElementTop: function (element) {
  50. var top = 0;
  51. while (element && !element.classList.contains('page-scroll')) {
  52. top += element.offsetTop;
  53. element = element.offsetParent;
  54. }
  55. return top;
  56. },
  57. jumpToHash: function (hash, instant) {
  58. var current = IV.computeCurrentState();
  59. current.hash = hash;
  60. window.history.replaceState(
  61. current,
  62. '',
  63. 'page' + IV.index + '.html');
  64. if (hash == '') {
  65. IV.scrollTo(0, instant);
  66. return;
  67. }
  68. var element = document.getElementsByName(hash)[0];
  69. if (element) {
  70. IV.scrollTo(IV.getElementTop(element), instant);
  71. }
  72. },
  73. frameKeyDown: function (e) {
  74. const key0 = (e.key === '0')
  75. || (e.code === 'Key0')
  76. || (e.keyCode === 48);
  77. const keyW = (e.key === 'w')
  78. || (e.code === 'KeyW')
  79. || (e.keyCode === 87);
  80. const keyQ = (e.key === 'q')
  81. || (e.code === 'KeyQ')
  82. || (e.keyCode === 81);
  83. const keyM = (e.key === 'm')
  84. || (e.code === 'KeyM')
  85. || (e.keyCode === 77);
  86. if ((e.metaKey || e.ctrlKey) && (keyW || keyQ || keyM || key0)) {
  87. e.preventDefault();
  88. IV.notify({
  89. event: 'keydown',
  90. modifier: e.ctrlKey ? 'ctrl' : 'cmd',
  91. key: key0 ? '0' : keyW ? 'w' : keyQ ? 'q' : 'm',
  92. });
  93. } else if (e.key === 'Escape' || e.keyCode === 27) {
  94. e.preventDefault();
  95. if (IV.position) {
  96. window.history.back();
  97. } else {
  98. IV.notify({
  99. event: 'keydown',
  100. key: 'escape',
  101. });
  102. }
  103. }
  104. },
  105. frameMouseEnter: function (e) {
  106. IV.notify({ event: 'mouseenter' });
  107. },
  108. frameMouseUp: function (e) {
  109. IV.notify({ event: 'mouseup' });
  110. },
  111. lastScrollTop: 0,
  112. frameScrolled: function (e) {
  113. const was = IV.lastScrollTop;
  114. IV.lastScrollTop = IV.findPageScroll().scrollTop;
  115. IV.updateJumpToTop(was < IV.lastScrollTop);
  116. IV.checkVideos();
  117. },
  118. updateJumpToTop: function (scrolledDown) {
  119. if (IV.lastScrollTop < 100) {
  120. document.getElementById('bottom_up').classList.add('hidden');
  121. } else if (scrolledDown && IV.lastScrollTop > 200) {
  122. document.getElementById('bottom_up').classList.remove('hidden');
  123. }
  124. },
  125. updateStyles: function (styles) {
  126. if (IV.styles !== styles) {
  127. IV.styles = styles;
  128. document.getElementsByTagName('html')[0].style = styles;
  129. }
  130. },
  131. toggleChannelJoined: function (id, joined) {
  132. IV.channelsJoined['channel' + id] = joined;
  133. IV.checkChannelButtons();
  134. },
  135. checkChannelButtons: function() {
  136. const channels = document.getElementsByClassName('channel');
  137. for (var i = 0; i < channels.length; ++i) {
  138. const channel = channels[i];
  139. const full = String(channel.getAttribute('data-context'));
  140. const value = IV.channelsJoined[full];
  141. if (value !== undefined) {
  142. channel.classList.toggle('joined', value);
  143. }
  144. }
  145. },
  146. slideshowSlide: function(el, delta) {
  147. var dir = window.getComputedStyle(el, null).direction || 'ltr';
  148. var marginProp = dir == 'rtl' ? 'marginRight' : 'marginLeft';
  149. if (delta) {
  150. var form = el.parentNode.firstChild;
  151. var s = form.s;
  152. const next = +s.value + delta;
  153. s.value = (next == s.length) ? 0 : (next == -1) ? (s.length - 1) : next;
  154. form.nextSibling.firstChild.style[marginProp] = (-100 * s.value) + '%';
  155. } else {
  156. el.form.nextSibling.firstChild.style[marginProp] = (-100 * el.value) + '%';
  157. }
  158. return false;
  159. },
  160. initPreBlocks: function() {
  161. if (!hljs) {
  162. return;
  163. }
  164. var pres = document.getElementsByTagName('pre');
  165. for (var i = 0; i < pres.length; i++) {
  166. if (pres[i].hasAttribute('data-language')) {
  167. hljs.highlightBlock(pres[i]);
  168. }
  169. }
  170. },
  171. initEmbedBlocks: function() {
  172. var iframes = document.getElementsByTagName('iframe');
  173. for (var i = 0; i < iframes.length; i++) {
  174. (function(iframe) {
  175. window.addEventListener('message', function(event) {
  176. if (event.source !== iframe.contentWindow ||
  177. event.origin != window.origin) {
  178. return;
  179. }
  180. try {
  181. var data = JSON.parse(event.data);
  182. } catch(e) {
  183. var data = {};
  184. }
  185. if (data.eventType == 'resize_frame') {
  186. if (data.eventData.height) {
  187. iframe.style.height = data.eventData.height + 'px';
  188. }
  189. }
  190. }, false);
  191. })(iframes[i]);
  192. }
  193. },
  194. addRipple: function (button, x, y) {
  195. const ripple = document.createElement('span');
  196. ripple.classList.add('ripple');
  197. const inner = document.createElement('span');
  198. inner.classList.add('inner');
  199. x -= button.offsetLeft;
  200. y -= button.offsetTop;
  201. const mx = button.clientWidth - x;
  202. const my = button.clientHeight - y;
  203. const sq1 = x * x + y * y;
  204. const sq2 = mx * mx + y * y;
  205. const sq3 = x * x + my * my;
  206. const sq4 = mx * mx + my * my;
  207. const radius = Math.sqrt(Math.max(sq1, sq2, sq3, sq4));
  208. inner.style.width = inner.style.height = `${2 * radius}px`;
  209. inner.style.left = `${x - radius}px`;
  210. inner.style.top = `${y - radius}px`;
  211. inner.classList.add('inner');
  212. ripple.addEventListener('animationend', function (e) {
  213. if (e.animationName === 'fadeOut') {
  214. ripple.remove();
  215. }
  216. });
  217. ripple.appendChild(inner);
  218. button.appendChild(ripple);
  219. },
  220. stopRipples: function (button) {
  221. const id = button.id ? button.id : button;
  222. button = document.getElementById(id);
  223. const ripples = button.getElementsByClassName('ripple');
  224. for (var i = 0; i < ripples.length; ++i) {
  225. const ripple = ripples[i];
  226. if (!ripple.classList.contains('hiding')) {
  227. ripple.classList.add('hiding');
  228. }
  229. }
  230. },
  231. init: function () {
  232. var current = IV.computeCurrentState();
  233. window.history.replaceState(current, '', IV.pageUrl(0));
  234. IV.jumpToHash(current.hash, true);
  235. IV.lastScrollTop = window.history.state.scroll;
  236. IV.findPageScroll().onscroll = IV.frameScrolled;
  237. const buttons = document.getElementsByClassName('fixed_button');
  238. for (let i = 0; i < buttons.length; ++i) {
  239. const button = buttons[i];
  240. button.addEventListener('mousedown', function (e) {
  241. IV.addRipple(e.currentTarget, e.clientX, e.clientY);
  242. });
  243. button.addEventListener('mouseup', function (e) {
  244. const id = e.currentTarget.id;
  245. setTimeout(function () {
  246. IV.stopRipples(id);
  247. }, 0);
  248. });
  249. button.addEventListener('mouseleave', function (e) {
  250. IV.stopRipples(e.currentTarget);
  251. });
  252. }
  253. IV.initMedia();
  254. IV.notify({ event: 'ready' });
  255. IV.forceScrollFocus();
  256. IV.frameScrolled();
  257. },
  258. initMedia: function () {
  259. var scroll = IV.findPageScroll();
  260. const photos = scroll.getElementsByClassName('photo');
  261. for (let i = 0; i < photos.length; ++i) {
  262. const photo = photos[i];
  263. if (photo.classList.contains('loaded')) {
  264. continue;
  265. }
  266. const url = photo.style.backgroundImage;
  267. if (!url || url.length < 7) {
  268. continue;
  269. }
  270. var img = new Image();
  271. img.onload = function () {
  272. photo.classList.add('loaded');
  273. }
  274. img.src = url.substr(5, url.length - 7);
  275. if (img.complete) {
  276. photo.classList.add('loaded');
  277. IV.stopAnimations(photo);
  278. }
  279. }
  280. IV.videos = [];
  281. const videos = scroll.getElementsByClassName('video');
  282. for (let i = 0; i < videos.length; ++i) {
  283. const element = videos[i];
  284. IV.videos.push({
  285. element: element,
  286. src: String(element.getAttribute('data-src')),
  287. autoplay: (element.getAttribute('data-autoplay') == '1'),
  288. loop: (element.getAttribute('data-loop') == '1'),
  289. small: (element.getAttribute('data-small') == '1'),
  290. filled: (element.firstChild
  291. && element.firstChild.tagName == 'VIDEO'),
  292. });
  293. }
  294. },
  295. checkVideos: function () {
  296. const visibleTop = IV.lastScrollTop;
  297. const visibleBottom = visibleTop + IV.findPageScroll().offsetHeight;
  298. const videos = IV.videos;
  299. for (let i = 0; i < videos.length; ++i) {
  300. const video = videos[i];
  301. const element = video.element;
  302. const wrap = element.offsetParent; // video-wrap
  303. const top = IV.getElementTop(wrap);
  304. const bottom = top + wrap.offsetHeight;
  305. if (top < visibleBottom && bottom > visibleTop) {
  306. if (!video.created) {
  307. video.created = new Date();
  308. video.loaded = false;
  309. element.innerHTML = '<video muted class="'
  310. + (video.small ? 'video-small' : '')
  311. + '"'
  312. + (video.autoplay
  313. ? ' preload="auto" autoplay'
  314. : (video.small
  315. ? ''
  316. : ' controls'))
  317. + (video.loop ? ' loop' : '')
  318. + ' oncanplay="IV.checkVideos();"'
  319. + ' onloadeddata="IV.checkVideos();">'
  320. + '<source src="'
  321. + video.src
  322. + '" type="video/mp4" />'
  323. + '</video>';
  324. var media = element.firstChild;
  325. media.oncontextmenu = function () { return false; };
  326. media.oncanplay = IV.checkVideos;
  327. media.onloadeddata = IV.checkVideos;
  328. }
  329. } else if (video.created && video.autoplay) {
  330. video.created = false;
  331. element.innerHTML = '';
  332. }
  333. if (video.created && !video.loaded) {
  334. var media = element.firstChild;
  335. const HAVE_CURRENT_DATA = 2;
  336. if (media && media.readyState >= HAVE_CURRENT_DATA) {
  337. video.loaded = true;
  338. media.classList.add('loaded');
  339. if ((new Date() - video.created) < 100) {
  340. IV.stopAnimations(media);
  341. }
  342. }
  343. }
  344. }
  345. },
  346. showTooltip: function (text) {
  347. var toast = document.createElement('div');
  348. toast.classList.add('toast');
  349. toast.textContent = text;
  350. document.body.appendChild(toast);
  351. setTimeout(function () {
  352. toast.classList.add('hiding');
  353. }, 2000);
  354. setTimeout(function () {
  355. document.body.removeChild(toast);
  356. }, 3000);
  357. },
  358. scrollTo: function (y, instant) {
  359. if (y < 200) {
  360. document.getElementById('bottom_up').classList.add('hidden');
  361. }
  362. IV.findPageScroll().scrollTo({
  363. top: y || 0,
  364. behavior: instant ? 'instant' : 'smooth'
  365. });
  366. },
  367. computeCurrentState: function () {
  368. var now = IV.findPageScroll();
  369. return {
  370. position: IV.position,
  371. index: IV.index,
  372. hash: ((!window.history.state
  373. || window.history.state.hash === undefined)
  374. ? window.location.hash.substr(1)
  375. : window.history.state.hash),
  376. scroll: now ? now.scrollTop : 0
  377. };
  378. },
  379. pageUrl: function (index, hash) {
  380. var result = 'page' + index + '.html';
  381. if (hash) {
  382. result += '#' + hash;
  383. }
  384. return result;
  385. },
  386. navigateTo: function (index, hash) {
  387. if (!index && !IV.index) {
  388. IV.navigateToDOM(IV.index, hash);
  389. return;
  390. }
  391. IV.pending = [index, hash];
  392. if (!IV.cache[index]) {
  393. IV.loadPage(index);
  394. } else if (IV.cache[index].dom) {
  395. IV.navigateToDOM(index, hash);
  396. } else if (IV.cache[index].content) {
  397. IV.navigateToLoaded(index, hash);
  398. }
  399. },
  400. applyUpdatedContent: function (index) {
  401. if (IV.index != index) {
  402. IV.cache[index].contentUpdated = (IV.cache[index].dom !== undefined);
  403. return;
  404. }
  405. var data = JSON.parse(IV.cache[index].content);
  406. var article = function (el) {
  407. return el.getElementsByTagName('article')[0];
  408. };
  409. var footer = function (el) {
  410. return el.getElementsByClassName('page-footer')[0];
  411. };
  412. var from = IV.findPageScroll();
  413. var to = IV.makeScrolledContent(data.html);
  414. morphdom(article(from), article(to), {
  415. onBeforeElUpdated: function (fromEl, toEl) {
  416. if (fromEl.classList.contains('video')
  417. && toEl.classList.contains('video')
  418. && fromEl.hasAttribute('data-src')
  419. && toEl.hasAttribute('data-src')
  420. && (fromEl.getAttribute('data-src')
  421. == toEl.getAttribute('data-src'))) {
  422. return false;
  423. } else if (fromEl.tagName == 'SECTION'
  424. && fromEl.classList.contains('channel')
  425. && fromEl.hasAttribute('data-context')
  426. && toEl.tagName == 'SECTION'
  427. && toEl.classList.contains('channel')
  428. && toEl.hasAttribute('data-context')
  429. && (String(fromEl.getAttribute('data-context'))
  430. == String(toEl.getAttribute('data-context')))) {
  431. return false;
  432. } else if (fromEl.classList.contains('loaded')) {
  433. toEl.classList.add('loaded');
  434. }
  435. return !fromEl.isEqualNode(toEl);
  436. }
  437. });
  438. morphdom(footer(from), footer(to));
  439. IV.initMedia();
  440. eval(data.js);
  441. },
  442. loadPage: function (index) {
  443. if (!IV.cache[index]) {
  444. IV.cache[index] = {};
  445. }
  446. IV.cache[index].loading = true;
  447. let xhr = new XMLHttpRequest();
  448. xhr.onload = function () {
  449. IV.cache[index].loading = false;
  450. IV.cache[index].content = xhr.responseText;
  451. IV.applyUpdatedContent(index);
  452. if (IV.pending && IV.pending[0] == index) {
  453. IV.navigateToLoaded(index, IV.pending[1]);
  454. }
  455. if (IV.cache[index].reloadPending) {
  456. IV.cache[index].reloadPending = false;
  457. IV.reloadPage(index);
  458. }
  459. }
  460. xhr.open('GET', 'page' + index + '.json');
  461. xhr.send();
  462. },
  463. reloadPage: function (index) {
  464. if (IV.cache[index] && IV.cache[index].loading) {
  465. IV.cache[index].reloadPending = true;
  466. return;
  467. }
  468. IV.loadPage(index);
  469. },
  470. makeScrolledContent: function (html) {
  471. var result = document.createElement('div');
  472. result.className = 'page-scroll';
  473. result.tabIndex = '-1';
  474. result.innerHTML = html.trim();
  475. result.onscroll = IV.frameScrolled;
  476. return result;
  477. },
  478. navigateToLoaded: function (index, hash) {
  479. if (IV.cache[index].dom) {
  480. IV.navigateToDOM(index, hash);
  481. } else {
  482. var data = JSON.parse(IV.cache[index].content);
  483. IV.cache[index].dom = IV.makeScrolledContent(data.html);
  484. IV.navigateToDOM(index, hash);
  485. eval(data.js);
  486. }
  487. },
  488. navigateToDOM: function (index, hash) {
  489. IV.pending = null;
  490. if (IV.index == index) {
  491. IV.jumpToHash(hash);
  492. IV.forceScrollFocus();
  493. return;
  494. }
  495. window.history.replaceState(
  496. IV.computeCurrentState(),
  497. '',
  498. IV.pageUrl(IV.index));
  499. IV.position = IV.position + 1;
  500. window.history.pushState(
  501. { position: IV.position, index: index, hash: hash },
  502. '',
  503. IV.pageUrl(index));
  504. IV.showDOM(index, hash);
  505. },
  506. findPageScroll: function () {
  507. var all = document.getElementsByClassName('page-scroll');
  508. for (i = 0; i < all.length; ++i) {
  509. if (!all[i].classList.contains('hidden-left')
  510. && !all[i].classList.contains('hidden-right')) {
  511. return all[i];
  512. }
  513. }
  514. return null;
  515. },
  516. showDOM: function (index, hash, scroll) {
  517. IV.pending = null;
  518. if (IV.index != index) {
  519. var initial = !window.history.state
  520. || window.history.state.position === undefined;
  521. var back = initial
  522. || IV.position > window.history.state.position;
  523. IV.position = initial ? 0 : window.history.state.position;
  524. var now = IV.cache[index].dom;
  525. var was = IV.findPageScroll();
  526. if (!IV.cache[IV.index]) {
  527. IV.cache[IV.index] = {};
  528. }
  529. IV.cache[IV.index].dom = was;
  530. was.parentNode.appendChild(now);
  531. if (scroll !== undefined) {
  532. now.scrollTop = scroll;
  533. setTimeout(function () {
  534. // When returning by history.back to an URL with a hash
  535. // for the first time browser forces the scroll to the
  536. // hash instead of the saved scroll position.
  537. //
  538. // This workaround prevents incorrect scroll position.
  539. now.scrollTop = scroll;
  540. }, 0);
  541. }
  542. now.classList.add(back ? 'hidden-left' : 'hidden-right');
  543. now.classList.remove(back ? 'hidden-right' : 'hidden-left');
  544. IV.stopAnimations(now.firstChild);
  545. if (!was.listening) {
  546. was.listening = true;
  547. was.firstChild.addEventListener('transitionend', function (e) {
  548. if (was.classList.contains('hidden-left')
  549. || was.classList.contains('hidden-right')) {
  550. if (was.parentNode) {
  551. was.parentNode.removeChild(was);
  552. var videos = was.getElementsByClassName('video');
  553. for (var i = 0; i < videos.length; ++i) {
  554. videos[i].innerHTML = '';
  555. }
  556. }
  557. }
  558. });
  559. }
  560. was.classList.add(back ? 'hidden-right' : 'hidden-left');
  561. now.classList.remove(back ? 'hidden-left' : 'hidden-right');
  562. IV.index = index;
  563. IV.notify({
  564. event: 'location_change',
  565. index: IV.index,
  566. position: IV.position,
  567. hash: IV.computeCurrentState().hash,
  568. });
  569. if (IV.cache[index].contentUpdated) {
  570. IV.cache[index].contentUpdated = false;
  571. IV.applyUpdatedContent(index);
  572. } else {
  573. IV.initMedia();
  574. }
  575. IV.checkChannelButtons();
  576. if (scroll === undefined) {
  577. IV.jumpToHash(hash, true);
  578. } else {
  579. IV.lastScrollTop = scroll;
  580. IV.updateJumpToTop(true);
  581. }
  582. } else if (scroll !== undefined) {
  583. IV.scrollTo(scroll);
  584. IV.lastScrollTop = scroll;
  585. IV.updateJumpToTop(true);
  586. } else {
  587. IV.jumpToHash(hash);
  588. }
  589. IV.forceScrollFocus();
  590. IV.frameScrolled();
  591. },
  592. forceScrollFocus: function () {
  593. IV.findPageScroll().focus();
  594. setTimeout(function () {
  595. // Doesn't work on #hash-ed pages in Windows WebView2 otherwise.
  596. IV.findPageScroll().focus();
  597. }, 100);
  598. },
  599. stopAnimations: function (element) {
  600. element.getAnimations().forEach(
  601. (animation) => animation.finish());
  602. },
  603. menuShown: function (shown) {
  604. var already = document.getElementById('menu_page_blocker');
  605. if (already && shown) {
  606. return;
  607. } else if (already) {
  608. document.body.removeChild(already);
  609. return;
  610. } else if (!shown) {
  611. return;
  612. }
  613. var blocker = document.createElement('div');
  614. blocker.id = 'menu_page_blocker';
  615. document.body.appendChild(blocker);
  616. },
  617. videos: {},
  618. videosPlaying: {},
  619. cache: {},
  620. channelsJoined: {},
  621. index: 0,
  622. position: 0
  623. };
  624. document.onclick = IV.frameClickHandler;
  625. document.onkeydown = IV.frameKeyDown;
  626. document.onmouseenter = IV.frameMouseEnter;
  627. document.onmouseup = IV.frameMouseUp;
  628. document.onresize = IV.checkVideos;
  629. window.onmessage = IV.postMessageHandler;
  630. window.addEventListener('popstate', function (e) {
  631. if (e.state) {
  632. IV.showDOM(e.state.index, e.state.hash, e.state.scroll);
  633. }
  634. });
  635. document.addEventListener("DOMContentLoaded", IV.forceScrollFocus);