254 lines
7.7 KiB
JavaScript
254 lines
7.7 KiB
JavaScript
// Copyright (c) 2019 Florian Klampfer <https://qwtel.com/>
|
|
//
|
|
// 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 <http://www.gnu.org/licenses/>.
|
|
|
|
import { fromEvent, merge, NEVER, combineLatest } from 'rxjs';
|
|
import {
|
|
distinctUntilChanged,
|
|
map,
|
|
filter,
|
|
startWith,
|
|
switchMap,
|
|
tap,
|
|
throttleTime,
|
|
withLatestFrom,
|
|
} from 'rxjs/operators';
|
|
|
|
import {
|
|
BREAK_POINT_3,
|
|
BREAK_POINT_DYNAMIC,
|
|
isSafari,
|
|
isMobile,
|
|
isMobileSafari,
|
|
hasCSSOM,
|
|
webComponentsReady,
|
|
stylesheetReady,
|
|
getScrollTop,
|
|
getViewWidth,
|
|
fromMediaQuery,
|
|
} from './common';
|
|
|
|
(async () => {
|
|
await Promise.all([
|
|
...('customElements' in window
|
|
? []
|
|
: [
|
|
import(/* webpackChunkName: "webcomponents" */ './polyfills/webcomponents').then(() =>
|
|
import(/* webpackChunkName: "shadydom" */ './polyfills/shadydom'),
|
|
),
|
|
]),
|
|
...('ResizeObserver' in window
|
|
? []
|
|
: [import(/* webpackChunkName: "resize-observer" */ './polyfills/resize-observer')]),
|
|
]);
|
|
|
|
await webComponentsReady;
|
|
await stylesheetReady;
|
|
|
|
const MOBILE = 1;
|
|
const DESKTOP = 2;
|
|
const LARGE_DESKTOP = 3;
|
|
|
|
const subscribeWhen = (p$) => (source) => {
|
|
if (process.env.DEBUG && !p$) throw Error();
|
|
return p$.pipe(switchMap((p) => (p ? source : NEVER)));
|
|
};
|
|
|
|
// Determines the range from which to draw the drawer in pixels, counted from the left edge.
|
|
// It depends on the browser, e.g. Safari has a native gesture when sliding form the side,
|
|
// so we ignore the first 35 pixels (roughly the range for the native gesture),
|
|
// to avoid triggering both gestures.
|
|
function getRange(drawerWidth, size) {
|
|
if (size >= DESKTOP) return [0, drawerWidth];
|
|
if (isMobileSafari) return [35, 150];
|
|
return [0, 150];
|
|
}
|
|
|
|
// The functions below add an svg graphic to the sidebar
|
|
// that indicate that the sidebar can be drawn using touch gestures.
|
|
function setupIcon(drawerEl) {
|
|
const img = document.getElementById('_hrefSwipeSVG');
|
|
if (img) {
|
|
const svg = document.createElement('img');
|
|
svg.id = '_swipe';
|
|
svg.src = img.href;
|
|
svg.alt = 'Swipe image';
|
|
svg.addEventListener('click', () => drawerEl.close());
|
|
document.getElementById('_sidebar')?.appendChild(svg);
|
|
}
|
|
}
|
|
|
|
function removeIcon() {
|
|
const svg = document.getElementById('_swipe');
|
|
svg?.parentNode?.removeChild(svg);
|
|
}
|
|
|
|
const detectSize = () =>
|
|
window.matchMedia(BREAK_POINT_DYNAMIC).matches
|
|
? LARGE_DESKTOP
|
|
: window.matchMedia(BREAK_POINT_3).matches
|
|
? DESKTOP
|
|
: MOBILE;
|
|
|
|
// First we get hold of some DOM elements.
|
|
const drawerEl = document.getElementById('_drawer');
|
|
const sidebarEl = document.getElementById('_sidebar');
|
|
const contentEl = sidebarEl?.querySelector('.sidebar-sticky');
|
|
if (!drawerEl || !sidebarEl || !contentEl) return;
|
|
|
|
document.getElementById('_menu')?.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
drawerEl.toggle();
|
|
});
|
|
|
|
sidebarEl
|
|
.querySelectorAll('a[href^="/"]:not(.external)')
|
|
.forEach((el) => el.addEventListener('click', () => drawerEl.close()));
|
|
|
|
if (isSafari) drawerEl.setAttribute('threshold', '0');
|
|
if (!isMobile) drawerEl.setAttribute('mouseevents', '');
|
|
|
|
const [tValue, oValue] = hasCSSOM
|
|
? [new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))]), CSS.number(1)]
|
|
: [null, null];
|
|
|
|
const updateSidebar = (t, size, distance) => {
|
|
const value = distance * t;
|
|
const opacity = size >= DESKTOP ? 1 : 1 - t;
|
|
if (hasCSSOM) {
|
|
tValue[0].x.value = value;
|
|
oValue.value = opacity;
|
|
sidebarEl.attributeStyleMap.set('transform', tValue);
|
|
contentEl.attributeStyleMap.set('opacity', oValue);
|
|
} else {
|
|
sidebarEl.style.transform = `translateX(${value}px)`;
|
|
contentEl.style.opacity = opacity;
|
|
}
|
|
};
|
|
|
|
// A flag for the 3 major viewport sizes we support
|
|
const size$ = merge(
|
|
fromMediaQuery(window.matchMedia(BREAK_POINT_3)),
|
|
fromMediaQuery(window.matchMedia(BREAK_POINT_DYNAMIC)),
|
|
).pipe(startWith({}), map(detectSize));
|
|
|
|
// An observable keeping track of the drawer (peek) width.
|
|
const peekWidth$ = fromEvent(drawerEl, 'peek-width-change').pipe(map((e) => e.detail));
|
|
|
|
// An observable keeping track the viewport width
|
|
const viewWidth$ = fromEvent(window, 'resize', { passive: true }).pipe(startWith({}), map(getViewWidth));
|
|
|
|
// An observable keeping track of the distance between
|
|
// the middle point of the screen and the middle point of the drawer.
|
|
const distance$ = combineLatest(peekWidth$, viewWidth$).pipe(
|
|
map(([drawerWidth, viewWidth]) => viewWidth / 2 - drawerWidth / 2),
|
|
);
|
|
|
|
const t$ = merge(
|
|
distance$.pipe(map(() => (drawerEl.opacity !== undefined ? 1 - drawerEl.opacity : opened ? 0 : 1))),
|
|
fromEvent(drawerEl, 'hy-drawer-move').pipe(
|
|
map(({ detail: { opacity } }) => {
|
|
return 1 - opacity;
|
|
}),
|
|
),
|
|
);
|
|
|
|
drawerEl.addEventListener('hy-drawer-prepare', () => {
|
|
sidebarEl.style.willChange = 'transform';
|
|
contentEl.style.willChange = 'opacity';
|
|
});
|
|
|
|
drawerEl.addEventListener('hy-drawer-transitioned', () => {
|
|
sidebarEl.style.willChange = '';
|
|
contentEl.style.willChange = '';
|
|
});
|
|
|
|
// Save scroll position before the drawer gets initialized.
|
|
const scrollTop = getScrollTop();
|
|
|
|
// Start the drawer in `opened` state when the cover class is present,
|
|
// and the user hasn't started scrolling already.
|
|
const opened = drawerEl.classList.contains('cover') && scrollTop <= 0 && !(history.state && history.state.closedOnce);
|
|
|
|
if (!opened) {
|
|
if (!history.state) history.replaceState({}, document.title);
|
|
history.state.closedOnce = true;
|
|
drawerEl.removeAttribute('opened');
|
|
}
|
|
|
|
const opened$ = fromEvent(drawerEl, 'hy-drawer-transitioned').pipe(
|
|
map((e) => e.detail),
|
|
distinctUntilChanged(),
|
|
tap((opened) => {
|
|
if (!opened) {
|
|
removeIcon();
|
|
if (!history.state) history.replaceState({}, document.title);
|
|
history.state.closedOnce = true;
|
|
}
|
|
}),
|
|
startWith(opened),
|
|
);
|
|
|
|
// We need the height of the drawer in case we need to reset the scroll position
|
|
const drawerHeight = opened ? null : drawerEl.getBoundingClientRect().height;
|
|
|
|
drawerEl.addEventListener(
|
|
'hy-drawer-init',
|
|
() => {
|
|
drawerEl.classList.add('loaded');
|
|
|
|
setupIcon(drawerEl);
|
|
|
|
if (drawerHeight && scrollTop >= drawerHeight) {
|
|
window.scrollTo(0, scrollTop - drawerHeight);
|
|
}
|
|
},
|
|
{ once: true },
|
|
);
|
|
|
|
await import(/* webpackMode: "eager" */ '@hydecorp/drawer');
|
|
|
|
window._drawer = drawerEl;
|
|
|
|
t$.pipe(
|
|
withLatestFrom(size$, distance$),
|
|
tap((args) => updateSidebar(...args)),
|
|
).subscribe();
|
|
|
|
// Keeping the drawer updated.
|
|
peekWidth$
|
|
.pipe(
|
|
withLatestFrom(size$),
|
|
map((args) => getRange(...args)),
|
|
tap((range) => {
|
|
drawerEl.range = range;
|
|
}),
|
|
)
|
|
.subscribe();
|
|
|
|
// Hacky way of letting the cover page close when scrolling
|
|
fromEvent(document, 'wheel', { passive: false })
|
|
.pipe(
|
|
subscribeWhen(opened$),
|
|
filter((e) => e.deltaY > 0),
|
|
tap((e) => {
|
|
if (drawerEl.translateX > 0) e.preventDefault();
|
|
}),
|
|
throttleTime(500),
|
|
tap(() => drawerEl.close()),
|
|
)
|
|
.subscribe();
|
|
})();
|