diff --git a/bun.lockb b/bun.lockb index 5b0c885..6d0a7e4 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index c9c04c1..7eeeb9d 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@solid-primitives/map": "^0.4.13", "@solid-primitives/page-visibility": "^2.0.17", "@solid-primitives/resize-observer": "^2.0.26", + "@solid-primitives/static-store": "^0.1.0", "@solidjs/router": "^0.15.2", "@suid/icons-material": "^0.8.1", "@suid/material": "^0.18.0", diff --git a/src/App.tsx b/src/App.tsx index 5e69ff3..9374b59 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,6 +33,7 @@ import { Service } from "./serviceworker/services.js"; import { makeEventListener } from "@solid-primitives/event-listener"; import { ServiceWorkerProvider } from "./platform/host.js"; import StackedRouter from "./platform/StackedRouter.js"; +import {ResizeObserverBoundary} from "~platform/resize-observer.jsx"; const AccountSignIn = lazy(() => import("./accounts/SignIn.js")); const AccountMastodonOAuth2Callback = lazy( @@ -157,6 +158,7 @@ const App: Component = () => { }} > + { + ); diff --git a/src/material/Scaffold.tsx b/src/material/Scaffold.tsx index a2c7b69..603c1e8 100644 --- a/src/material/Scaffold.tsx +++ b/src/material/Scaffold.tsx @@ -1,4 +1,4 @@ -import { createElementSize } from "@solid-primitives/resize-observer"; +import { createElementSize } from "~platform/resize-observer"; import { JSX, Show, diff --git a/src/platform/Masonry.tsx b/src/platform/Masonry.tsx index ab9f002..8176889 100644 --- a/src/platform/Masonry.tsx +++ b/src/platform/Masonry.tsx @@ -10,7 +10,7 @@ import { } from "solid-js"; import { Dynamic, type DynamicProps } from "solid-js/web"; import MasonryLayout from "masonry-layout"; -import { createElementSize } from "@solid-primitives/resize-observer"; +import { createElementSize } from "~platform/resize-observer"; import "./Masonry.css"; type MasonryContainer = diff --git a/src/platform/resize-observer.tsx b/src/platform/resize-observer.tsx new file mode 100644 index 0000000..fcc5808 --- /dev/null +++ b/src/platform/resize-observer.tsx @@ -0,0 +1,158 @@ +import { + createContext, + createEffect, + onCleanup, + sharedConfig, + useContext, + type JSX, +} from "solid-js"; +import { isDev, isServer } from "solid-js/web"; +import { createStaticStore } from "@solid-primitives/static-store"; + +export type ObserveCallback = ( + entry: ResizeObserverEntry & { readonly target: E }, +) => void; +export type ObserveElement = ( + element: E, + callback: ObserveCallback, +) => () => void; + +const ResizeObserverContext = /* @__PURE__ */ createContext(); + +export function useResizeObserver() { + const observe = useContext(ResizeObserverContext); + + if (isDev && !observe) { + throw new TypeError( + "ObserverElement is not found, this function must be called in ", + ); + } + + return observe!; +} + +function ResizeObserverBoundaryClient(props: { children: JSX.Element }) { + const map = new Map< + Element, + ObserveCallback | ObserveCallback[] + >(); + + const observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const callback = map.get(entry.target); + if (!callback) return; + if (Array.isArray(callback)) { + for (const f of callback) { + f(entry); + } + } else { + callback(entry); + } + } + }); + + onCleanup(() => { + observer.disconnect(); + }); + + const observe: ObserveElement = ( + element: Element, + callback: ObserveCallback, + ) => { + const callbacks = map.get(element); + if (!callbacks) { + map.set(element, callback); + observer.observe(element); + } else if (Array.isArray(callbacks)) { + callbacks.push(callback); + } else { + map.set(element, [callbacks, callback]); + } + + return () => { + const callbacks = map.get(element); + if (callbacks === null) { + observer.unobserve(element); + } else if (Array.isArray(callbacks)) { + const idx = callbacks.indexOf(callback); + if (idx !== -1) { + callbacks.splice(idx, 1); + } + + if (callbacks.length === 0) { + observer.unobserve(element); + map.delete(element); + } + } else { + observer.unobserve(element); + map.delete(element); + } + }; + }; + + return ( + + {props.children} + + ); +} + +function ResizeObserverBoundaryServer(props: { children: JSX.Element }) { + return ( + {}) as unknown as ObserveElement} + > + {props.children} + + ); +} + +export const ResizeObserverBoundary = isServer + ? ResizeObserverBoundaryServer + : ResizeObserverBoundaryClient; + +const ELEMENT_SIZE_FALLBACK = { width: null, height: null }; + +function getElementSize(target: Element) { + if (isServer || !target) { + return { ...ELEMENT_SIZE_FALLBACK }; + } + const { width, height } = target.getBoundingClientRect(); + return { width, height }; +} + +export type NullableSize = {width: number | null, height: number | null} + +export function createElementSize( + target: Element | (() => Element | undefined | null | false), +): Readonly { + if (isServer) { + return ELEMENT_SIZE_FALLBACK; + } + const isFn = typeof target === "function"; + const [size, setSize] = createStaticStore( + sharedConfig.context || isFn + ? ELEMENT_SIZE_FALLBACK + : getElementSize(target), + ); + const callback: ObserveCallback = (entry) => { + setSize(getElementSize(entry.target)); + }; + const observe = useResizeObserver(); + if (isFn) { + createEffect(() => { + const el = target(); + if (el) { + setSize(getElementSize(el)); + const unobserve = observe(el, callback); + onCleanup(unobserve); + } + }); + } else { + const unobserve = observe(target, callback); + onCleanup(unobserve); + } + return size; +} + +export { useWindowSize } from "@solid-primitives/resize-observer"; diff --git a/src/timelines/toots/MediaAttachmentGrid.tsx b/src/timelines/toots/MediaAttachmentGrid.tsx index 4b22564..be5f7d8 100644 --- a/src/timelines/toots/MediaAttachmentGrid.tsx +++ b/src/timelines/toots/MediaAttachmentGrid.tsx @@ -12,10 +12,7 @@ import { } from "solid-js"; import MediaViewer from "../MediaViewer"; import { render } from "solid-js/web"; -import { - createElementSize, - useWindowSize, -} from "@solid-primitives/resize-observer"; +import { createElementSize, useWindowSize } from "~platform/resize-observer"; import { useStore } from "@nanostores/solid"; import { $settings } from "../../settings/stores"; import { averageColorHex } from "~platform/blurhash";