// Copyright (c) 2019 Florian Klampfer // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . import { fromEvent, merge, timer, zip } from 'rxjs'; import { tap, exhaustMap, map, mapTo, mergeMap, pairwise, share, startWith, switchMap, takeUntil, } from 'rxjs/operators'; import { animate, empty, importTemplate, webComponentsReady } from './common'; import { CrossFader } from './cross-fader'; import { setupFLIP } from './flip'; (async () => { await Promise.all([ ...('fetch' in window ? [] : [import(/* webpackChunkName: "fetch" */ './polyfills/fetch')]), ...('customElements' in window ? [] : [import(/* webpackChunkName: "webcomponents" */ './polyfills/webcomponents')]), ...('animate' in Element.prototype ? [] : [import(/* webpackChunkName: "webanimations" */ 'web-animations-js')]), ...('IntersectionObserver' in window ? [] : [import(/* webpackChunkName: "intersection-observer" */ 'intersection-observer')]), ]); await webComponentsReady; const NAVBAR_SEL = '#_navbar > .content > .nav-btn-bar'; const CROSS_FADE_DURATION = 2000; const FADE_OUT = [{ opacity: 1 }, { opacity: 0 }]; const FADE_IN = [ { opacity: 0, transform: 'translateY(-3rem)' }, { opacity: 1, transform: 'translateY(0)' }, ]; function setupAnimationMain(pushStateEl) { pushStateEl?.parentNode?.insertBefore(importTemplate('_animation-template'), pushStateEl); return pushStateEl?.previousElementSibling; } function setupLoading(navbarEl) { navbarEl?.appendChild(importTemplate('_loading-template')); return navbarEl?.lastElementChild; } function setupErrorPage(main, url) { const { pathname } = url; const error = importTemplate('_error-template'); const anchor = error?.querySelector('.this-link'); if (anchor) { anchor.href = pathname; anchor.textContent = pathname; } main?.appendChild(error); return main?.lastElementChild; } function getFlipType(el) { if (el?.classList.contains('flip-title')) return 'title'; if (el?.classList.contains('flip-project')) return 'project'; return el?.getAttribute?.('data-flip'); } const pushStateEl = document.querySelector('hy-push-state'); if (!pushStateEl) return; const duration = Number(pushStateEl.getAttribute('duration')) || 350; const settings = { duration: duration, easing: 'ease', }; const animateFadeOut = ({ main }) => animate(main, FADE_OUT, { ...settings, fill: 'forwards' }).pipe(mapTo({ main })); const animateFadeIn = ({ replaceEls: [main], flipType }) => animate(main, FADE_IN, settings).pipe(mapTo({ main, flipType })); const drawerEl = document.querySelector('hy-drawer'); const navbarEl = document.querySelector(NAVBAR_SEL); const animationMain = setupAnimationMain(pushStateEl); const loadingEl = setupLoading(navbarEl); const fromEventX = (eventName, mapFn) => fromEvent(pushStateEl, eventName).pipe( map(({ detail }) => detail), mapFn ? map(mapFn) : (_) => _, share(), ); const start$ = fromEventX('hy-push-state-start', (detail) => Object.assign(detail, { flipType: getFlipType(detail.anchor) }), ); const ready$ = fromEventX('hy-push-state-ready'); const after$ = fromEventX('hy-push-state-after'); const progress$ = fromEventX('hy-push-state-progress'); const error$ = fromEventX('hy-push-state-networkerror'); const fadeOut$ = start$.pipe( map((context) => Object.assign(context, { main: document.getElementById('_main') })), tap(({ main }) => { main.style.pointerEvents = 'none'; }), window._noDrawer && drawerEl?.classList.contains('cover') ? tap(() => { drawerEl.classList.remove('cover'); drawerEl.parentNode?.appendChild(drawerEl); }) : (_) => _, exhaustMap(animateFadeOut), tap(({ main }) => empty.call(main)), share(), ); if (loadingEl) { progress$.subscribe(() => { loadingEl.style.display = 'flex'; }); ready$.subscribe(() => { loadingEl.style.display = 'none'; }); } const fadeIn$ = after$.pipe( switchMap((context) => { const p = animateFadeIn(context).toPromise(); context.transitionUntil(p); return p; }), share(), ); const flip$ = setupFLIP(start$, ready$, merge(fadeIn$, error$), { animationMain, settings: settings, }).pipe(share()); start$ .pipe( switchMap((context) => { const promise = zip(timer(duration), fadeOut$, flip$).toPromise(); context.transitionUntil(promise); return promise; }), ) .subscribe(); // FIXME: Keeping permanent subscription? turn into hot observable? fadeOut$.subscribe(); flip$.subscribe(); const sidebarBg = document.querySelector('.sidebar-bg'); if (sidebarBg) { const crossFader = new CrossFader(CROSS_FADE_DURATION); after$ .pipe( switchMap(({ document }) => zip(crossFader.fetchImage(document), fadeIn$).pipe( map(([x]) => x), takeUntil(start$), ), ), startWith([sidebarBg]), pairwise(), mergeMap(([prev, curr]) => crossFader.fade(prev, curr)), ) .subscribe(); } error$ .pipe( switchMap(({ url }) => { if (loadingEl) loadingEl.style.display = 'none'; const main = document.getElementById('_main'); if (main) main.style.pointerEvents = ''; empty.call(animationMain?.querySelector('.page')); empty.call(main); setupErrorPage(main, url); return animate(main, FADE_IN, { ...settings, fill: 'forwards' }); }), ) .subscribe(); import(/* webpackMode: "eager" */ '@hydecorp/push-state'); window._pushState = pushStateEl; })();