chore: format

This commit is contained in:
2025-10-10 16:43:21 +02:00
parent f0aabd63b6
commit 75c29e0ba4
551 changed files with 433948 additions and 94145 deletions

View File

@@ -1,69 +1,86 @@
import { VELOCITY_THRESHOLD } from "./constants";
import { Coord } from './observables';
import { Coord } from "./observables";
// Using shorthands for common functions
const min = Math.min.bind(Math);
const max = Math.max.bind(Math);
export class CalcMixin {
side!: 'left' | 'right';
range!: [number, number];
side!: "left" | "right";
range!: [number, number];
calcIsInRange({ clientX }: Coord, opened: boolean) {
// console.log(this.range, this.align);
switch (this.side) {
case "left": {
const [lower, upper] = this.range;
return clientX > lower && (opened || clientX < upper);
}
case "right": {
const upper = window.innerWidth - this.range[0];
const lower = window.innerWidth - this.range[1];
return clientX < upper && (opened || clientX > lower);
}
default:
throw Error();
}
}
calcIsInRange({ clientX }: Coord, opened: boolean) {
// console.log(this.range, this.align);
switch (this.side) {
case "left": {
const [lower, upper] = this.range;
return clientX > lower && (opened || clientX < upper);
}
case "right": {
const upper = window.innerWidth - this.range[0];
const lower = window.innerWidth - this.range[1];
return clientX < upper && (opened || clientX > lower);
}
default:
throw Error();
}
}
calcIsSwipe({ clientX: startX }: Coord, { clientX: endX }: Coord, translateX: number, drawerWidth: number, _: number): boolean {
return endX !== startX || (translateX > 0 && translateX < drawerWidth);
}
calcIsSwipe(
{ clientX: startX }: Coord,
{ clientX: endX }: Coord,
translateX: number,
drawerWidth: number,
_: number,
): boolean {
return endX !== startX || (translateX > 0 && translateX < drawerWidth);
}
calcWillOpen(_: {}, __: {}, translateX: number, drawerWidth: number, velocity: number): boolean {
switch (this.side) {
case "left": {
if (velocity > VELOCITY_THRESHOLD) return true;
else if (velocity < -VELOCITY_THRESHOLD) return false;
else if (translateX >= drawerWidth / 2) return true;
else return false;
}
case "right": {
if (-velocity > VELOCITY_THRESHOLD) return true;
else if (-velocity < -VELOCITY_THRESHOLD) return false;
else if (translateX <= -drawerWidth / 2) return true;
else return false;
}
default:
throw Error();
}
}
calcWillOpen(
_: {},
__: {},
translateX: number,
drawerWidth: number,
velocity: number,
): boolean {
switch (this.side) {
case "left": {
if (velocity > VELOCITY_THRESHOLD) return true;
else if (velocity < -VELOCITY_THRESHOLD) return false;
else if (translateX >= drawerWidth / 2) return true;
else return false;
}
case "right": {
if (-velocity > VELOCITY_THRESHOLD) return true;
else if (-velocity < -VELOCITY_THRESHOLD) return false;
else if (translateX <= -drawerWidth / 2) return true;
else return false;
}
default:
throw Error();
}
}
calcTranslateX({ clientX: moveX }: Coord, { clientX: startX }: Coord, startTranslateX: number, drawerWidth: number): number {
switch (this.side) {
case "left": {
const deltaX = moveX - startX;
const translateX = startTranslateX + deltaX;
return max(0, min(drawerWidth, translateX));
}
case "right": {
const deltaX = moveX - startX;
const translateX = startTranslateX + deltaX;
return min(0, max(-drawerWidth, translateX));
}
default:
throw Error();
}
}
};
calcTranslateX(
{ clientX: moveX }: Coord,
{ clientX: startX }: Coord,
startTranslateX: number,
drawerWidth: number,
): number {
switch (this.side) {
case "left": {
const deltaX = moveX - startX;
const translateX = startTranslateX + deltaX;
return max(0, min(drawerWidth, translateX));
}
case "right": {
const deltaX = moveX - startX;
const translateX = startTranslateX + deltaX;
return min(0, max(-drawerWidth, translateX));
}
default:
throw Error();
}
}
}

View File

@@ -1,35 +1,42 @@
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { createResizeObservable } from '@hydecorp/component';
import { ComplexAttributeConverter } from 'lit';
import { Observable, of } from "rxjs";
import { map } from "rxjs/operators";
import { createResizeObservable } from "@hydecorp/component";
import { ComplexAttributeConverter } from "lit";
export { applyMixins, subscribeWhen, filterWhen, tween } from '@hydecorp/component';
export {
applyMixins,
subscribeWhen,
filterWhen,
tween,
} from "@hydecorp/component";
export function easeOutSine(t: number, b: number, c: number, d: number) {
return c * Math.sin((t / d) * (Math.PI / 2)) + b;
return c * Math.sin((t / d) * (Math.PI / 2)) + b;
}
export function observeWidth(el: HTMLElement) {
// This component should have at least basic support without `ResizeObserver` support,
// so we pass a one-time measurement when it's missing. Obviously this won't update, so BYO polyfill.
const resize$: Observable<{ contentRect: { width: number } }> =
'ResizeObserver' in window ? createResizeObservable(el) : of({ contentRect: { width: el.clientWidth } });
return resize$.pipe(map((rect) => rect.contentRect.width));
// This component should have at least basic support without `ResizeObserver` support,
// so we pass a one-time measurement when it's missing. Obviously this won't update, so BYO polyfill.
const resize$: Observable<{ contentRect: { width: number } }> =
"ResizeObserver" in window
? createResizeObservable(el)
: of({ contentRect: { width: el.clientWidth } });
return resize$.pipe(map((rect) => rect.contentRect.width));
}
export const rangeConverter: ComplexAttributeConverter<number[]> = {
fromAttribute(attr) {
return (attr ?? '')
.replace(/[\[\]]/g, '')
.split(',')
.map(Number);
},
fromAttribute(attr) {
return (attr ?? "")
.replace(/[\[\]]/g, "")
.split(",")
.map(Number);
},
toAttribute(range) {
return range.join(',');
},
toAttribute(range) {
return range.join(",");
},
};
export function rangeHasChanged(curr: number[], prev: number[] = []) {
return curr.length !== prev.length || curr.some((v, i) => v !== prev[i]);
return curr.length !== prev.length || curr.some((v, i) => v !== prev[i]);
}

View File

@@ -17,406 +17,469 @@
* @license
* @nocompile
*/
import { html, ReactiveElement } from 'lit';
import { property, customElement, query } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { styleMap } from 'lit/directives/style-map.js';
import { html, ReactiveElement } from "lit";
import { property, customElement, query } from "lit/decorators.js";
import { classMap } from "lit/directives/class-map.js";
import { styleMap } from "lit/directives/style-map.js";
import { Observable, Subject, BehaviorSubject, combineLatest, merge, NEVER, defer, fromEvent } from 'rxjs';
import {
startWith,
takeUntil,
map,
share,
withLatestFrom,
tap,
sample,
timestamp,
pairwise,
filter,
switchMap,
skip,
finalize,
} from 'rxjs/operators';
Observable,
Subject,
BehaviorSubject,
combineLatest,
merge,
NEVER,
defer,
fromEvent,
} from "rxjs";
import {
startWith,
takeUntil,
map,
share,
withLatestFrom,
tap,
sample,
timestamp,
pairwise,
filter,
switchMap,
skip,
finalize,
} from "rxjs/operators";
import { RxLitElement, createResolvablePromise } from '@hydecorp/component';
import { RxLitElement, createResolvablePromise } from "@hydecorp/component";
import { BASE_DURATION, WIDTH_CONTRIBUTION } from './constants';
import { applyMixins, filterWhen, easeOutSine, observeWidth, rangeConverter, rangeHasChanged, tween } from './common';
import { ObservablesMixin, Coord } from './observables';
import { CalcMixin } from './calc';
import { UpdateMixin, DOMUpdater } from './update';
import { styles } from './styles';
import { BASE_DURATION, WIDTH_CONTRIBUTION } from "./constants";
import {
applyMixins,
filterWhen,
easeOutSine,
observeWidth,
rangeConverter,
rangeHasChanged,
tween,
} from "./common";
import { ObservablesMixin, Coord } from "./observables";
import { CalcMixin } from "./calc";
import { UpdateMixin, DOMUpdater } from "./update";
import { styles } from "./styles";
@customElement('hy-drawer')
@customElement("hy-drawer")
export class HyDrawer
extends applyMixins(RxLitElement, [ObservablesMixin, UpdateMixin, CalcMixin])
implements ObservablesMixin, UpdateMixin, CalcMixin
extends applyMixins(RxLitElement, [ObservablesMixin, UpdateMixin, CalcMixin])
implements ObservablesMixin, UpdateMixin, CalcMixin
{
static styles = styles;
static styles = styles;
@query('.scrim') scrimEl!: HTMLElement;
@query('.wrapper') contentEl!: HTMLElement;
@query('.peek') peekEl!: HTMLElement;
@query(".scrim") scrimEl!: HTMLElement;
@query(".wrapper") contentEl!: HTMLElement;
@query(".peek") peekEl!: HTMLElement;
@property({ type: Boolean, reflect: true }) opened: boolean = false;
@property({ type: String, reflect: true }) side: 'left' | 'right' = 'left';
@property({ type: Boolean, reflect: true }) persistent: boolean = false;
@property({ type: Number, reflect: true }) threshold: number = 10;
@property({ type: Boolean, reflect: true }) noScroll: boolean = false;
@property({ type: Boolean, reflect: true }) mouseEvents: boolean = false;
// @property({ type: Boolean, reflect: true }) hashChange: boolean = false;
@property({ reflect: true, converter: rangeConverter, hasChanged: rangeHasChanged }) range: [number, number] = [
0, 100,
];
@property({ type: Boolean, reflect: true }) opened: boolean = false;
@property({ type: String, reflect: true }) side: "left" | "right" = "left";
@property({ type: Boolean, reflect: true }) persistent: boolean = false;
@property({ type: Number, reflect: true }) threshold: number = 10;
@property({ type: Boolean, reflect: true }) noScroll: boolean = false;
@property({ type: Boolean, reflect: true }) mouseEvents: boolean = false;
// @property({ type: Boolean, reflect: true }) hashChange: boolean = false;
@property({
reflect: true,
converter: rangeConverter,
hasChanged: rangeHasChanged,
})
range: [number, number] = [0, 100];
// State
@property() scrimClickable!: boolean;
@property() grabbing!: boolean;
@property() willChange: boolean = false;
// State
@property() scrimClickable!: boolean;
@property() grabbing!: boolean;
@property() willChange: boolean = false;
#initialized = createResolvablePromise();
get initialized() {
return this.#initialized;
}
#initialized = createResolvablePromise();
get initialized() {
return this.#initialized;
}
// get histId() { return this.id || this.tagName; }
// get hashId() { return `#${this.histId}--opened`; }
// get histId() { return this.id || this.tagName; }
// get hashId() { return `#${this.histId}--opened`; }
translateX!: number;
opacity!: number;
isSliding!: boolean;
translateX!: number;
opacity!: number;
isSliding!: boolean;
$!: {
opened: Subject<boolean>;
side: Subject<'left' | 'right'>;
persistent: Subject<boolean>;
preventDefault: Subject<boolean>;
mouseEvents: Subject<boolean>;
// hashChange: Subject<boolean>;
};
$!: {
opened: Subject<boolean>;
side: Subject<"left" | "right">;
persistent: Subject<boolean>;
preventDefault: Subject<boolean>;
mouseEvents: Subject<boolean>;
// hashChange: Subject<boolean>;
};
animateTo$!: Subject<boolean>;
animateTo$!: Subject<boolean>;
// TODO: Prefer composition to mixins...
// ObservablesMixin
getStartObservable!: () => Observable<Coord>;
getMoveObservable!: (start: Observable<Coord>, end: Observable<Coord>) => Observable<Coord>;
getEndObservable!: () => Observable<Coord>;
getIsSlidingObservable!: (
move: Observable<Coord>,
start: Observable<Coord>,
end: Observable<Coord>,
) => Observable<boolean>;
getIsSlidingObservableInner!: (move: Observable<Coord>, start: Observable<Coord>) => Observable<boolean>;
// TODO: Prefer composition to mixins...
// ObservablesMixin
getStartObservable!: () => Observable<Coord>;
getMoveObservable!: (
start: Observable<Coord>,
end: Observable<Coord>,
) => Observable<Coord>;
getEndObservable!: () => Observable<Coord>;
getIsSlidingObservable!: (
move: Observable<Coord>,
start: Observable<Coord>,
end: Observable<Coord>,
) => Observable<boolean>;
getIsSlidingObservableInner!: (
move: Observable<Coord>,
start: Observable<Coord>,
) => Observable<boolean>;
// CalcMixin
calcIsInRange!: (start: Coord, opened: boolean) => boolean;
calcIsSwipe!: (start: Coord, end: Coord, translateX: number, drawerWidth: number, _: number) => boolean;
calcWillOpen!: (start: {}, end: {}, translateX: number, drawerWidth: number, velocity: number) => boolean;
calcTranslateX!: (move: Coord, start: Coord, startTranslateX: number, drawerWidth: number) => number;
// CalcMixin
calcIsInRange!: (start: Coord, opened: boolean) => boolean;
calcIsSwipe!: (
start: Coord,
end: Coord,
translateX: number,
drawerWidth: number,
_: number,
) => boolean;
calcWillOpen!: (
start: {},
end: {},
translateX: number,
drawerWidth: number,
velocity: number,
) => boolean;
calcTranslateX!: (
move: Coord,
start: Coord,
startTranslateX: number,
drawerWidth: number,
) => number;
// UpdateMixin
updateDOM!: (translateX: number, drawerWidth: number) => void;
updater!: DOMUpdater;
// UpdateMixin
updateDOM!: (translateX: number, drawerWidth: number) => void;
updater!: DOMUpdater;
// HyDrawer
getDrawerWidth(): Observable<number> {
const content$ = observeWidth(this.contentEl).pipe(
tap((px) => this.fireEvent('content-width-change', { detail: px })),
);
const peek$ = observeWidth(this.peekEl).pipe(tap((px) => this.fireEvent('peek-width-change', { detail: px })));
// HyDrawer
getDrawerWidth(): Observable<number> {
const content$ = observeWidth(this.contentEl).pipe(
tap((px) => this.fireEvent("content-width-change", { detail: px })),
);
const peek$ = observeWidth(this.peekEl).pipe(
tap((px) => this.fireEvent("peek-width-change", { detail: px })),
);
return combineLatest([content$, peek$]).pipe(
// takeUntil(this.subjects.disconnect),
map(([contentWidth, peekWidth]) => contentWidth - peekWidth),
share(),
);
}
return combineLatest([content$, peek$]).pipe(
// takeUntil(this.subjects.disconnect),
map(([contentWidth, peekWidth]) => contentWidth - peekWidth),
share(),
);
}
// private consolidateState() {
// const hashOpened = location.hash === this.hashId;
// const isReload = history.state && history.state[this.histId];
// if (isReload) {
// if (hashOpened !== this.opened) {
// this.opened = hashOpened;
// }
// } else {
// const url = new URL(location.href);
// const newState = { ...history.state, [this.histId]: { backable: false } };
// if (hashOpened && !this.opened) {
// url.hash = '';
// history.replaceState(newState, document.title, url.href);
// private consolidateState() {
// const hashOpened = location.hash === this.hashId;
// const isReload = history.state && history.state[this.histId];
// if (isReload) {
// if (hashOpened !== this.opened) {
// this.opened = hashOpened;
// }
// } else {
// const url = new URL(location.href);
// const newState = { ...history.state, [this.histId]: { backable: false } };
// if (hashOpened && !this.opened) {
// url.hash = '';
// history.replaceState(newState, document.title, url.href);
// url.hash = this.hashId;
// history.pushState({ [this.histId]: { backable: true } }, document.title, url.href);
// url.hash = this.hashId;
// history.pushState({ [this.histId]: { backable: true } }, document.title, url.href);
// this.opened = true;
// }
// else if (!hashOpened && this.opened) {
// history.replaceState(newState, document.title, url.href);
// this.opened = true;
// }
// else if (!hashOpened && this.opened) {
// history.replaceState(newState, document.title, url.href);
// url.hash = this.hashId;
// history.pushState({ [this.histId]: { backable: true } }, document.title, url.href);
// }
// else {
// history.replaceState(newState, document.title, url.href);
// }
// }
// }
// url.hash = this.hashId;
// history.pushState({ [this.histId]: { backable: true } }, document.title, url.href);
// }
// else {
// history.replaceState(newState, document.title, url.href);
// }
// }
// }
connectedCallback() {
super.connectedCallback();
connectedCallback() {
super.connectedCallback();
// if (this.hashChange) this.consolidateState()
// if (this.hashChange) this.consolidateState()
this.$ = {
opened: new BehaviorSubject(this.opened),
side: new BehaviorSubject(this.side),
persistent: new BehaviorSubject(this.persistent),
preventDefault: new BehaviorSubject(this.noScroll),
mouseEvents: new BehaviorSubject(this.mouseEvents),
// hashChange: new BehaviorSubject(this.hashChange),
};
this.$ = {
opened: new BehaviorSubject(this.opened),
side: new BehaviorSubject(this.side),
persistent: new BehaviorSubject(this.persistent),
preventDefault: new BehaviorSubject(this.noScroll),
mouseEvents: new BehaviorSubject(this.mouseEvents),
// hashChange: new BehaviorSubject(this.hashChange),
};
this.scrimClickable = this.opened;
this.animateTo$ = new Subject<boolean>();
this.updater = DOMUpdater.getUpdaterForPlatform(this);
this.updateComplete.then(this.upgrade);
}
this.scrimClickable = this.opened;
this.animateTo$ = new Subject<boolean>();
this.updater = DOMUpdater.getUpdaterForPlatform(this);
this.updateComplete.then(this.upgrade);
}
#translateX$!: Observable<number>;
#startTranslateX$!: Observable<number>;
#tweenTranslateX$!: Observable<number>;
#translateX$!: Observable<number>;
#startTranslateX$!: Observable<number>;
#tweenTranslateX$!: Observable<number>;
upgrade = () => {
const drawerWidth$ = this.getDrawerWidth();
const active$ = this.$.persistent.pipe(map((_) => !_));
upgrade = () => {
const drawerWidth$ = this.getDrawerWidth();
const active$ = this.$.persistent.pipe(map((_) => !_));
const start$ = this.getStartObservable().pipe(
// takeUntil(this.subjects.disconnect),
filterWhen(active$),
share(),
);
const start$ = this.getStartObservable().pipe(
// takeUntil(this.subjects.disconnect),
filterWhen(active$),
share(),
);
const isScrimVisible$ = defer(() => {
return this.#translateX$.pipe(map((translateX) => translateX !== 0));
});
const isScrimVisible$ = defer(() => {
return this.#translateX$.pipe(map((translateX) => translateX !== 0));
});
const isInRange$ = start$.pipe(
withLatestFrom(isScrimVisible$),
map((args) => this.calcIsInRange(...args)),
tap((inRange) => {
if (inRange) {
this.willChange = true;
this.fireEvent('prepare');
}
}),
share(),
);
const isInRange$ = start$.pipe(
withLatestFrom(isScrimVisible$),
map((args) => this.calcIsInRange(...args)),
tap((inRange) => {
if (inRange) {
this.willChange = true;
this.fireEvent("prepare");
}
}),
share(),
);
const end$ = this.getEndObservable().pipe(
// takeUntil(this.subjects.disconnect),
filterWhen(active$, isInRange$),
tap(() => {
if (this.mouseEvents) this.grabbing = false;
}),
share(),
);
const end$ = this.getEndObservable().pipe(
// takeUntil(this.subjects.disconnect),
filterWhen(active$, isInRange$),
tap(() => {
if (this.mouseEvents) this.grabbing = false;
}),
share(),
);
const move$ = this.getMoveObservable(start$, end$).pipe(
// takeUntil(this.subjects.disconnect),
filterWhen(active$, isInRange$),
share(),
);
const move$ = this.getMoveObservable(start$, end$).pipe(
// takeUntil(this.subjects.disconnect),
filterWhen(active$, isInRange$),
share(),
);
const isSliding$ = this.getIsSlidingObservable(move$, start$, end$).pipe(
tap((isSliding) => {
this.isSliding = isSliding;
if (isSliding && this.mouseEvents) this.grabbing = true;
// if (isSliding) this.fireEvent('slidestart', { detail: this.opened });
}),
);
const isSliding$ = this.getIsSlidingObservable(move$, start$, end$).pipe(
tap((isSliding) => {
this.isSliding = isSliding;
if (isSliding && this.mouseEvents) this.grabbing = true;
// if (isSliding) this.fireEvent('slidestart', { detail: this.opened });
}),
);
const translateX$ = (this.#translateX$ = defer(() => {
const jumpTranslateX$ = combineLatest([this.$.opened, this.$.side, drawerWidth$]).pipe(
map(([opened, side, drawerWidth]) => {
return !opened ? 0 : drawerWidth * (side === 'left' ? 1 : -1);
}),
);
const translateX$ = (this.#translateX$ = defer(() => {
const jumpTranslateX$ = combineLatest([
this.$.opened,
this.$.side,
drawerWidth$,
]).pipe(
map(([opened, side, drawerWidth]) => {
return !opened ? 0 : drawerWidth * (side === "left" ? 1 : -1);
}),
);
const moveTranslateX$ = move$.pipe(
filterWhen(isSliding$),
tap(() => (this.scrimClickable = false)),
tap(({ event }) => event && this.noScroll && event.preventDefault()),
withLatestFrom(start$, this.#startTranslateX$, drawerWidth$),
map((args) => this.calcTranslateX(...args)),
);
const moveTranslateX$ = move$.pipe(
filterWhen(isSliding$),
tap(() => (this.scrimClickable = false)),
tap(({ event }) => event && this.noScroll && event.preventDefault()),
withLatestFrom(start$, this.#startTranslateX$, drawerWidth$),
map((args) => this.calcTranslateX(...args)),
);
return merge(this.#tweenTranslateX$, jumpTranslateX$, moveTranslateX$);
}).pipe(share()));
return merge(this.#tweenTranslateX$, jumpTranslateX$, moveTranslateX$);
}).pipe(share()));
this.#startTranslateX$ = translateX$.pipe(sample(start$));
this.#startTranslateX$ = translateX$.pipe(sample(start$));
const velocity$ = translateX$.pipe(
timestamp(),
pairwise(),
filter(([{ timestamp: prevTime }, { timestamp: time }]) => time - prevTime > 0),
map(([{ value: prevX, timestamp: prevTime }, { value: x, timestamp: time }]) => (x - prevX) / (time - prevTime)),
startWith(0),
);
const velocity$ = translateX$.pipe(
timestamp(),
pairwise(),
filter(
([{ timestamp: prevTime }, { timestamp: time }]) => time - prevTime > 0,
),
map(
([
{ value: prevX, timestamp: prevTime },
{ value: x, timestamp: time },
]) => (x - prevX) / (time - prevTime),
),
startWith(0),
);
const willOpen$ = end$.pipe(
withLatestFrom(start$, translateX$, drawerWidth$, velocity$),
filter((args) => this.calcIsSwipe(...args)),
map((args) => this.calcWillOpen(...args)),
// TODO: only fire `slideend` event when slidestart fired as well?
// tap(willOpen => this.fireEvent('slideend', { detail: willOpen })),
);
const willOpen$ = end$.pipe(
withLatestFrom(start$, translateX$, drawerWidth$, velocity$),
filter((args) => this.calcIsSwipe(...args)),
map((args) => this.calcWillOpen(...args)),
// TODO: only fire `slideend` event when slidestart fired as well?
// tap(willOpen => this.fireEvent('slideend', { detail: willOpen })),
);
const animateTo$ = this.animateTo$.pipe(
tap(() => {
this.willChange = true;
this.fireEvent('prepare');
}),
);
const animateTo$ = this.animateTo$.pipe(
tap(() => {
this.willChange = true;
this.fireEvent("prepare");
}),
);
this.#tweenTranslateX$ = merge(willOpen$, animateTo$).pipe(
withLatestFrom(translateX$, drawerWidth$),
switchMap(([willOpen, translateX, drawerWidth]) => {
const inv = this.side === 'left' ? 1 : -1;
const endTranslateX = willOpen ? drawerWidth * inv : 0;
const diffTranslateX = endTranslateX - translateX;
const duration = Math.ceil(BASE_DURATION + drawerWidth * WIDTH_CONTRIBUTION);
this.#tweenTranslateX$ = merge(willOpen$, animateTo$).pipe(
withLatestFrom(translateX$, drawerWidth$),
switchMap(([willOpen, translateX, drawerWidth]) => {
const inv = this.side === "left" ? 1 : -1;
const endTranslateX = willOpen ? drawerWidth * inv : 0;
const diffTranslateX = endTranslateX - translateX;
const duration = Math.ceil(
BASE_DURATION + drawerWidth * WIDTH_CONTRIBUTION,
);
return tween(easeOutSine, translateX, diffTranslateX, duration).pipe(
finalize(() => {
this.transitioned(willOpen);
}),
takeUntil(start$),
takeUntil(this.$.side.pipe(skip(1))),
share(),
);
}),
);
return tween(easeOutSine, translateX, diffTranslateX, duration).pipe(
finalize(() => {
this.transitioned(willOpen);
}),
takeUntil(start$),
takeUntil(this.$.side.pipe(skip(1))),
share(),
);
}),
);
translateX$
.pipe(
withLatestFrom(drawerWidth$),
tap((args) => {
this.updateDOM(...args);
const { translateX, opacity } = this;
this.fireEvent('move', { detail: { translateX, opacity }, bubbles: false });
}),
)
.subscribe();
translateX$
.pipe(
withLatestFrom(drawerWidth$),
tap((args) => {
this.updateDOM(...args);
const { translateX, opacity } = this;
this.fireEvent("move", {
detail: { translateX, opacity },
bubbles: false,
});
}),
)
.subscribe();
fromEvent(this.scrimEl, 'click')
.pipe(
// takeUntil(this.subjects.disconnect),
tap(() => this.close()),
)
.subscribe();
fromEvent(this.scrimEl, "click")
.pipe(
// takeUntil(this.subjects.disconnect),
tap(() => this.close()),
)
.subscribe();
active$
.pipe(
// takeUntil(this.subjects.disconnect),
tap((active) => {
this.scrimEl.style.display = active ? 'block' : 'none';
}),
)
.subscribe();
active$
.pipe(
// takeUntil(this.subjects.disconnect),
tap((active) => {
this.scrimEl.style.display = active ? "block" : "none";
}),
)
.subscribe();
this.$.mouseEvents
.pipe(
// takeUntil(this.subjects.disconnect),
switchMap((mouseEvents) => {
return mouseEvents ? start$.pipe(withLatestFrom(isInRange$)) : NEVER;
}),
filter(([coord, isInRange]) => isInRange && coord.event != null),
tap(([{ event }]) => event && event.preventDefault()),
)
.subscribe();
this.$.mouseEvents
.pipe(
// takeUntil(this.subjects.disconnect),
switchMap((mouseEvents) => {
return mouseEvents ? start$.pipe(withLatestFrom(isInRange$)) : NEVER;
}),
filter(([coord, isInRange]) => isInRange && coord.event != null),
tap(([{ event }]) => event && event.preventDefault()),
)
.subscribe();
// fromEvent(window, 'hashchange').pipe(
// // takeUntil(this.subjects.disconnect),
// subscribeWhen(this.$.hashChange),
// tap(() => {
// const opened = location.hash === this.hashId;
// if (!history.state && opened) {
// history.replaceState({ [this.histId]: { backable: true } }, document.title)
// }
// fromEvent(window, 'hashchange').pipe(
// // takeUntil(this.subjects.disconnect),
// subscribeWhen(this.$.hashChange),
// tap(() => {
// const opened = location.hash === this.hashId;
// if (!history.state && opened) {
// history.replaceState({ [this.histId]: { backable: true } }, document.title)
// }
// // If the state doesn't match open/close the drawer
// if (opened !== this.opened) this.animateTo$.next(opened);
// }),
// ).subscribe();
// // If the state doesn't match open/close the drawer
// if (opened !== this.opened) this.animateTo$.next(opened);
// }),
// ).subscribe();
this.fireEvent('init', { detail: this.opened });
this.#initialized.resolve(this);
};
this.fireEvent("init", { detail: this.opened });
this.#initialized.resolve(this);
};
private transitioned = (hasOpened: boolean) => {
this.opened = this.scrimClickable = hasOpened;
this.willChange = false;
private transitioned = (hasOpened: boolean) => {
this.opened = this.scrimClickable = hasOpened;
this.willChange = false;
// if (this.hashChange) this.transitionedHash(hasOpened)
// if (this.hashChange) this.transitionedHash(hasOpened)
this.fireEvent('transitioned', { detail: hasOpened });
};
this.fireEvent("transitioned", { detail: hasOpened });
};
// private transitionedHash(hasOpened: boolean) {
// const hasClosed = !hasOpened;
// const { backable } = history.state && history.state[this.histId] || { backable: false }
// if (hasClosed && backable) {
// history.back()
// }
// if (hasOpened && location.hash !== this.hashId) {
// location.hash = this.hashId;
// }
// }
// private transitionedHash(hasOpened: boolean) {
// const hasClosed = !hasOpened;
// const { backable } = history.state && history.state[this.histId] || { backable: false }
// if (hasClosed && backable) {
// history.back()
// }
// if (hasOpened && location.hash !== this.hashId) {
// location.hash = this.hashId;
// }
// }
render() {
return html`
render() {
return html`
<div class="peek full-height"></div>
<div
class="scrim"
style=${styleMap({
willChange: this.willChange ? 'opacity' : '',
pointerEvents: this.scrimClickable ? 'all' : '',
})}
willChange: this.willChange ? "opacity" : "",
pointerEvents: this.scrimClickable ? "all" : "",
})}
></div>
${this.mouseEvents && this.grabbing && !this.scrimClickable
? html`<div class="grabbing-screen full-screen"></div>`
: null}
${
this.mouseEvents && this.grabbing && !this.scrimClickable
? html`<div class="grabbing-screen full-screen"></div>`
: null
}
<div
class=${classMap({
wrapper: true,
'full-height': true,
[this.side]: true,
grab: this.mouseEvents,
grabbing: this.mouseEvents && this.grabbing,
})}
wrapper: true,
"full-height": true,
[this.side]: true,
grab: this.mouseEvents,
grabbing: this.mouseEvents && this.grabbing,
})}
style=${styleMap({
willChange: this.willChange ? 'transform' : '',
})}
willChange: this.willChange ? "transform" : "",
})}
>
<div class="overflow">
<slot></slot>
</div>
</div>
`;
}
}
open() {
this.animateTo$.next(true);
}
open() {
this.animateTo$.next(true);
}
close() {
this.animateTo$.next(false);
}
close() {
this.animateTo$.next(false);
}
toggle() {
this.animateTo$.next(!this.opened);
}
toggle() {
this.animateTo$.next(!this.opened);
}
}

View File

@@ -1,16 +1,23 @@
import { Observable, combineLatest, fromEvent, merge, NEVER, ObservedValueOf } from "rxjs";
import {
Observable,
combineLatest,
fromEvent,
merge,
NEVER,
ObservedValueOf,
} from "rxjs";
import {
// tap,
filter,
map,
mapTo,
repeatWhen,
skipWhile,
startWith,
switchMap,
take,
withLatestFrom,
// tap,
filter,
map,
mapTo,
repeatWhen,
skipWhile,
startWith,
switchMap,
take,
withLatestFrom,
} from "rxjs/operators";
import { subscribeWhen } from "./common";
@@ -18,112 +25,137 @@ import { subscribeWhen } from "./common";
const abs = Math.abs.bind(Math);
export type Coord = {
readonly target: EventTarget;
readonly clientX: number;
readonly clientY: number;
readonly pageX: number;
readonly pageY: number;
readonly screenX: number;
readonly screenY: number;
event?: Event;
}
readonly target: EventTarget;
readonly clientX: number;
readonly clientY: number;
readonly pageX: number;
readonly pageY: number;
readonly screenX: number;
readonly screenY: number;
event?: Event;
};
export class ObservablesMixin {
$!: {
mouseEvents: Observable<boolean>;
preventDefault: Observable<boolean>;
}
$!: {
mouseEvents: Observable<boolean>;
preventDefault: Observable<boolean>;
};
threshold!: number;
noScroll!: boolean;
threshold!: number;
noScroll!: boolean;
getStartObservable() {
return combineLatest([this.$.mouseEvents]).pipe(
switchMap(([mouseEvents]) => {
const touchstart$ = (<Observable<TouchEvent>>fromEvent(document, "touchstart", { passive: true })).pipe(
filter(({ touches }) => touches.length === 1),
map(({ touches }) => touches[0] as Coord)
);
getStartObservable() {
return combineLatest([this.$.mouseEvents]).pipe(
switchMap(([mouseEvents]) => {
const touchstart$ = (<Observable<TouchEvent>>(
fromEvent(document, "touchstart", { passive: true })
)).pipe(
filter(({ touches }) => touches.length === 1),
map(({ touches }) => touches[0] as Coord),
);
const mousedown$ = !mouseEvents
? NEVER
: (<Observable<MouseEvent>>fromEvent(document, "mousedown")).pipe(
map((e) => (((e as Coord).event = e), e as Coord)),
);
const mousedown$ = !mouseEvents
? NEVER
: (<Observable<MouseEvent>>fromEvent(document, "mousedown")).pipe(
map((e) => (((e as Coord).event = e), e as Coord)),
);
return merge(touchstart$, mousedown$);
})
);
}
return merge(touchstart$, mousedown$);
}),
);
}
getMoveObservable(start$: Observable<Coord>, end$: Observable<Coord>) {
return combineLatest([this.$.mouseEvents, this.$.preventDefault]).pipe(
switchMap(([mouseEvents, preventDefault]) => {
const touchmove$ = (<Observable<TouchEvent>>fromEvent(document, "touchmove", { passive: !preventDefault })).pipe(
map(e => (((e.touches[0] as Coord).event = e), e.touches[0] as Coord))
);
getMoveObservable(start$: Observable<Coord>, end$: Observable<Coord>) {
return combineLatest([this.$.mouseEvents, this.$.preventDefault]).pipe(
switchMap(([mouseEvents, preventDefault]) => {
const touchmove$ = (<Observable<TouchEvent>>(
fromEvent(document, "touchmove", { passive: !preventDefault })
)).pipe(
map(
(e) => (((e.touches[0] as Coord).event = e), e.touches[0] as Coord),
),
);
const mousemove$ = !mouseEvents
? NEVER
: (<Observable<MouseEvent>>fromEvent(document, "mousemove", { passive: !preventDefault })).pipe(
subscribeWhen(merge(start$.pipe(mapTo(true)), end$.pipe(mapTo(false)))),
map(e => (((e as Coord).event = e), e as Coord))
);
const mousemove$ = !mouseEvents
? NEVER
: (<Observable<MouseEvent>>(
fromEvent(document, "mousemove", { passive: !preventDefault })
)).pipe(
subscribeWhen(
merge(start$.pipe(mapTo(true)), end$.pipe(mapTo(false))),
),
map((e) => (((e as Coord).event = e), e as Coord)),
);
return merge(touchmove$, mousemove$);
})
);
}
return merge(touchmove$, mousemove$);
}),
);
}
getEndObservable() {
return combineLatest([this.$.mouseEvents]).pipe(
switchMap(([mouseEvents]) => {
const touchend$ = (<Observable<TouchEvent>>fromEvent(document, "touchend", { passive: true })).pipe(
filter(({ touches }) => touches.length === 0),
map(event => event.changedTouches[0] as Coord)
);
getEndObservable() {
return combineLatest([this.$.mouseEvents]).pipe(
switchMap(([mouseEvents]) => {
const touchend$ = (<Observable<TouchEvent>>(
fromEvent(document, "touchend", { passive: true })
)).pipe(
filter(({ touches }) => touches.length === 0),
map((event) => event.changedTouches[0] as Coord),
);
const mouseup$ = !mouseEvents
? NEVER
: (<Observable<MouseEvent>>fromEvent(document, "mouseup", { passive: true })).pipe(
map(e => (((e as Coord).event = e), e as Coord)),
);
const mouseup$ = !mouseEvents
? NEVER
: (<Observable<MouseEvent>>(
fromEvent(document, "mouseup", { passive: true })
)).pipe(map((e) => (((e as Coord).event = e), e as Coord)));
return merge(touchend$, mouseup$);
})
);
}
return merge(touchend$, mouseup$);
}),
);
}
getIsSlidingObservable(move$: Observable<Coord>, start$: Observable<Coord>, end$: Observable<Coord>) {
return this.getIsSlidingObservableInner(move$, start$).pipe(
take(1),
startWith(undefined),
repeatWhen(() => end$)
);
}
getIsSlidingObservable(
move$: Observable<Coord>,
start$: Observable<Coord>,
end$: Observable<Coord>,
) {
return this.getIsSlidingObservableInner(move$, start$).pipe(
take(1),
startWith(undefined),
repeatWhen(() => end$),
);
}
getIsSlidingObservableInner(move$: Observable<Coord>, start$: Observable<Coord>) {
if (this.threshold) {
return move$.pipe(
withLatestFrom(start$),
skipWhile(
([{ clientX, clientY }, { clientX: startX, clientY: startY }]) =>
abs(startY - clientY) < this.threshold && abs(startX - clientX) < this.threshold
),
map(
([{ clientX, clientY }, { clientX: startX, clientY: startY }]) =>
abs(startX - clientX) >= abs(startY - clientY)
)
);
} else {
return move$.pipe(
withLatestFrom(start$),
map(([{ clientX, clientY, event }, { clientX: startX, clientY: startY }]) => {
const isSliding = abs(startX - clientX) >= abs(startY - clientY);
if (this.noScroll && isSliding && event) event.preventDefault();
return isSliding;
})
);
}
}
getIsSlidingObservableInner(
move$: Observable<Coord>,
start$: Observable<Coord>,
) {
if (this.threshold) {
return move$.pipe(
withLatestFrom(start$),
skipWhile(
([{ clientX, clientY }, { clientX: startX, clientY: startY }]) =>
abs(startY - clientY) < this.threshold &&
abs(startX - clientX) < this.threshold,
),
map(
([{ clientX, clientY }, { clientX: startX, clientY: startY }]) =>
abs(startX - clientX) >= abs(startY - clientY),
),
);
} else {
return move$.pipe(
withLatestFrom(start$),
map(
([
{ clientX, clientY, event },
{ clientX: startX, clientY: startY },
]) => {
const isSliding = abs(startX - clientX) >= abs(startY - clientY);
if (this.noScroll && isSliding && event) event.preventDefault();
return isSliding;
},
),
);
}
}
}

View File

@@ -1,4 +1,4 @@
import { css } from 'lit';
import { css } from "lit";
export const styles = css`
@media screen {

View File

@@ -1,68 +1,79 @@
export type CallbackValue = { translateX: number, opacity: number };
export type CallbackValue = { translateX: number; opacity: number };
export class UpdateMixin {
contentEl!: HTMLElement;
scrimEl!: HTMLElement;
contentEl!: HTMLElement;
scrimEl!: HTMLElement;
translateX!: number;
side!: string;
opacity!: number;
translateX!: number;
side!: string;
opacity!: number;
updater!: DOMUpdater;
updater!: DOMUpdater;
updateDOM(translateX: number, drawerWidth: number) {
const inv = this.side === "left" ? 1 : -1;
const opacity = (translateX / drawerWidth) * inv || 0;
updateDOM(translateX: number, drawerWidth: number) {
const inv = this.side === "left" ? 1 : -1;
const opacity = (translateX / drawerWidth) * inv || 0;
this.translateX = translateX;
this.opacity = opacity;
this.translateX = translateX;
this.opacity = opacity;
this.updater.updateDOM(translateX, opacity);
}
this.updater.updateDOM(translateX, opacity);
}
}
export abstract class DOMUpdater {
static getUpdaterForPlatform(parent: UpdateMixin) {
const hasCSSOM = "attributeStyleMap" in Element.prototype && "CSS" in window && "number" in CSS;
return hasCSSOM
? new AttributeStyleMapUpdater(parent)
: new StyleUpdater(parent);
}
static getUpdaterForPlatform(parent: UpdateMixin) {
const hasCSSOM =
"attributeStyleMap" in Element.prototype &&
"CSS" in window &&
"number" in CSS;
return hasCSSOM
? new AttributeStyleMapUpdater(parent)
: new StyleUpdater(parent);
}
private parent: UpdateMixin;
constructor(parent: UpdateMixin) {
this.parent = parent;
}
get contentEl() { return this.parent.contentEl }
get scrimEl() { return this.parent.scrimEl }
private parent: UpdateMixin;
constructor(parent: UpdateMixin) {
this.parent = parent;
}
get contentEl() {
return this.parent.contentEl;
}
get scrimEl() {
return this.parent.scrimEl;
}
abstract updateDOM(translateX: number, opacity: number): void;
abstract updateDOM(translateX: number, opacity: number): void;
}
export class StyleUpdater extends DOMUpdater {
constructor(parent: UpdateMixin) { super(parent); }
constructor(parent: UpdateMixin) {
super(parent);
}
updateDOM(translateX: number, opacity: number) {
this.contentEl.style.transform = `translate(${translateX}px, 0px)`;
this.scrimEl.style.opacity = `${opacity}`;
}
updateDOM(translateX: number, opacity: number) {
this.contentEl.style.transform = `translate(${translateX}px, 0px)`;
this.scrimEl.style.opacity = `${opacity}`;
}
}
export class AttributeStyleMapUpdater extends DOMUpdater {
private tvalue: CSSTransformValue;
private ovalue: CSSUnitValue;
private tvalue: CSSTransformValue;
private ovalue: CSSUnitValue;
constructor(parent: UpdateMixin) {
super(parent);
this.tvalue = new CSSTransformValue([new CSSTranslate(CSS.px(0), CSS.px(0))]);
this.ovalue = CSS.number(1);
}
constructor(parent: UpdateMixin) {
super(parent);
this.tvalue = new CSSTransformValue([
new CSSTranslate(CSS.px(0), CSS.px(0)),
]);
this.ovalue = CSS.number(1);
}
updateDOM(translateX: number, opacity: number) {
((this.tvalue[0] as CSSTranslate).x as CSSUnitValue).value = translateX;
this.ovalue.value = opacity;
updateDOM(translateX: number, opacity: number) {
((this.tvalue[0] as CSSTranslate).x as CSSUnitValue).value = translateX;
this.ovalue.value = opacity;
this.contentEl.attributeStyleMap.set("transform", this.tvalue);
this.scrimEl.attributeStyleMap.set("opacity", this.ovalue);
}
}
this.contentEl.attributeStyleMap.set("transform", this.tvalue);
this.scrimEl.attributeStyleMap.set("opacity", this.ovalue);
}
}