@koine/react 2.0.0-beta.72 → 2.0.0-beta.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/FaviconTags.d.ts +9 -0
- package/FaviconTags.js +5 -0
- package/Meta.d.ts +5 -0
- package/Meta.js +5 -0
- package/NoJs.d.ts +3 -0
- package/NoJs.js +7 -0
- package/Polymorphic.d.ts +26 -0
- package/Polymorphic.js +1 -0
- package/calendar/CalendarDaygridCell.js +42 -0
- package/calendar/CalendarDaygridNav.js +22 -0
- package/calendar/CalendarDaygridTable.js +50 -0
- package/calendar/CalendarLegend.js +4 -0
- package/calendar/calendar-api-google.js +90 -0
- package/calendar/types.js +1 -0
- package/calendar/useCalendar.js +175 -0
- package/calendar/useDateLocale.js +16 -0
- package/calendar/utils.js +172 -0
- package/calendar.js +7 -0
- package/classed.d.ts +8 -0
- package/classed.js +41 -0
- package/components/FaviconTags.js +4 -0
- package/components/Meta.js +4 -0
- package/components/NoJs.js +6 -0
- package/createUseMediaQueryWidth.d.ts +6 -0
- package/createUseMediaQueryWidth.js +38 -0
- package/extendComponent.d.ts +16 -0
- package/extendComponent.js +9 -0
- package/forms/antispam.js +29 -0
- package/forms.js +1 -0
- package/hooks/index.js +19 -0
- package/hooks/useAsyncFn.js +25 -0
- package/hooks/useFirstMountState.js +9 -0
- package/hooks/useFixedOffset.js +41 -0
- package/hooks/useFocus.js +8 -0
- package/hooks/useInterval.js +19 -0
- package/hooks/useIsomorphicLayoutEffect.js +3 -0
- package/hooks/useKeyUp.js +15 -0
- package/hooks/useMeasure.js +118 -0
- package/hooks/useMountedState.js +12 -0
- package/hooks/useNavigateAway.js +24 -0
- package/hooks/usePrevious.js +8 -0
- package/hooks/usePreviousRef.js +8 -0
- package/hooks/useReveal.js +41 -0
- package/hooks/useScrollPosition.js +57 -0
- package/hooks/useScrollThreshold.js +25 -0
- package/hooks/useScrollTo.js +17 -0
- package/hooks/useSmoothScroll.js +31 -0
- package/hooks/useSpinDelay.js +35 -0
- package/hooks/useTraceUpdate.js +16 -0
- package/hooks/useUpdateEffect.js +10 -0
- package/hooks/useWindowSize.js +19 -0
- package/index.cjs.js +33 -23
- package/index.d.ts +28 -3
- package/index.esm.js +33 -26
- package/index.js +26 -0
- package/mergeRefs.d.ts +2 -0
- package/mergeRefs.js +13 -0
- package/package.json +3 -3
- package/types.js +1 -0
- package/useAsyncFn.d.ts +24 -0
- package/useAsyncFn.js +26 -0
- package/useFirstMountState.d.ts +2 -0
- package/useFirstMountState.js +10 -0
- package/useFixedOffset.d.ts +2 -0
- package/useFixedOffset.js +42 -0
- package/useFocus.d.ts +2 -0
- package/useFocus.js +9 -0
- package/useInterval.d.ts +2 -0
- package/useInterval.js +20 -0
- package/useIsomorphicLayoutEffect.d.ts +3 -0
- package/useIsomorphicLayoutEffect.js +4 -0
- package/useKeyUp.d.ts +2 -0
- package/useKeyUp.js +16 -0
- package/useMeasure.d.ts +22 -0
- package/useMeasure.js +119 -0
- package/useMountedState.d.ts +2 -0
- package/useMountedState.js +13 -0
- package/useNavigateAway.d.ts +3 -0
- package/useNavigateAway.js +25 -0
- package/usePrevious.d.ts +2 -0
- package/usePrevious.js +9 -0
- package/usePreviousRef.d.ts +2 -0
- package/usePreviousRef.js +9 -0
- package/useReveal.d.ts +13 -0
- package/useReveal.js +42 -0
- package/useScrollPosition.d.ts +7 -0
- package/useScrollPosition.js +58 -0
- package/useScrollThreshold.d.ts +2 -0
- package/useScrollThreshold.js +26 -0
- package/useScrollTo.d.ts +2 -0
- package/useScrollTo.js +18 -0
- package/useSmoothScroll.d.ts +2 -0
- package/useSmoothScroll.js +32 -0
- package/useSpinDelay.d.ts +2 -0
- package/useSpinDelay.js +36 -0
- package/useTraceUpdate.d.ts +2 -0
- package/useTraceUpdate.js +17 -0
- package/useUpdateEffect.d.ts +3 -0
- package/useUpdateEffect.js +11 -0
- package/useWindowSize.d.ts +3 -0
- package/useWindowSize.js +20 -0
- package/utils/Polymorphic.js +1 -0
- package/utils/classed.js +40 -0
- package/utils/createUseMediaQueryWidth.js +37 -0
- package/utils/extendComponent.js +8 -0
- package/utils/index.js +4 -0
- package/utils/mergeRefs.js +12 -0
package/classed.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
type ClassedAugmentedProps<Props> = Props & {
|
|
3
|
+
className?: string;
|
|
4
|
+
ref?: React.Ref<any>;
|
|
5
|
+
};
|
|
6
|
+
type ClassedFinalProps<Props, Component> = Component extends React.ReactHTML ? React.HTMLProps<Component> & ClassedAugmentedProps<Props> : ClassedAugmentedProps<Props>;
|
|
7
|
+
export declare let classed: <Props, Component extends React.ElementType<any, keyof React.JSX.IntrinsicElements> = any>(component: Component) => (strings: TemplateStringsArray, ...args: ((props: Props) => string)[] | string[]) => React.ForwardRefExoticComponent<React.PropsWithoutRef<ClassedFinalProps<Props, Component>> & React.RefAttributes<Component>>;
|
|
8
|
+
export default classed;
|
package/classed.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import React, { createElement, forwardRef } from "react";
|
|
2
|
+
export let classed = (component) => {
|
|
3
|
+
const type = component.type || component;
|
|
4
|
+
return function (strings, ...args) {
|
|
5
|
+
const WrappedComponent = forwardRef(function (props, ref) {
|
|
6
|
+
const argResolved = args
|
|
7
|
+
.map((arg, index) => {
|
|
8
|
+
let result = "";
|
|
9
|
+
if (typeof arg === "function") {
|
|
10
|
+
result = arg(props);
|
|
11
|
+
}
|
|
12
|
+
else if (typeof arg !== "undefined") {
|
|
13
|
+
result = arg.toString();
|
|
14
|
+
}
|
|
15
|
+
return strings[index] + result;
|
|
16
|
+
})
|
|
17
|
+
.join("");
|
|
18
|
+
const isNativeHtmlElement = typeof type === "string";
|
|
19
|
+
const propsToForward = isNativeHtmlElement
|
|
20
|
+
? {}
|
|
21
|
+
: props;
|
|
22
|
+
if (isNativeHtmlElement) {
|
|
23
|
+
for (const key in props) {
|
|
24
|
+
if (!key.startsWith("$")) {
|
|
25
|
+
propsToForward[key] = props[key];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
let className = argResolved || strings[0];
|
|
30
|
+
className = className.match(/class="([^"]*)/)?.[1] || className;
|
|
31
|
+
className += props?.className ? " " + props?.className : "";
|
|
32
|
+
return createElement(type, {
|
|
33
|
+
...propsToForward,
|
|
34
|
+
className: className || undefined,
|
|
35
|
+
ref,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
return WrappedComponent;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
export default classed;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
export let FaviconTags = ({ name, color, safariTabColor, tileColor, themeColor, }) => {
|
|
3
|
+
return (_jsxs(_Fragment, { children: [_jsx("link", { rel: "shortcut icon", href: "/favicon.ico", type: "image/x-icon" }), _jsx("link", { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png" }), _jsx("link", { rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32x32.png" }), _jsx("link", { rel: "icon", type: "image/png", sizes: "16x16", href: "/favicon-16x16.png" }), _jsx("link", { rel: "manifest", href: "/site.webmanifest" }), _jsx("link", { rel: "mask-icon", href: "/safari-pinned-tab.svg", color: safariTabColor || color }), _jsx("meta", { name: "apple-mobile-web-app-title", content: name }), _jsx("meta", { name: "application-name", content: name }), _jsx("meta", { name: "msapplication-TileColor", content: tileColor || color }), _jsx("meta", { name: "theme-color", content: themeColor || color })] }));
|
|
4
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
export let NoJs = (_props) => {
|
|
3
|
+
return (_jsx("script", { id: "no-js", dangerouslySetInnerHTML: {
|
|
4
|
+
__html: `document.querySelector("html").className=document.querySelector("html").className.replace(/no-js/,"") + "js";`,
|
|
5
|
+
} }));
|
|
6
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type GetMediaQueryWidthResolversBreakpoints } from "@koine/utils";
|
|
2
|
+
type _MediaQuerWidthDefExplicit<TBreakpoint extends string> = `min-${TBreakpoint}` | `max-${TBreakpoint}` | `up-${TBreakpoint}` | `down-${TBreakpoint}` | `between-${TBreakpoint}_${TBreakpoint}` | `only-${TBreakpoint}`;
|
|
3
|
+
export type MediaQuerWidthDef<TBreakpoint extends string> = `${TBreakpoint}` | _MediaQuerWidthDefExplicit<TBreakpoint>;
|
|
4
|
+
export type MediaQueryWidth<TBreakpoint extends string> = `@${MediaQuerWidthDef<TBreakpoint>}`;
|
|
5
|
+
export declare let createUseMediaQueryWidth: <TBreakpointsConfig extends GetMediaQueryWidthResolversBreakpoints>(customBreakpoints: TBreakpointsConfig) => <TBreakpoints extends Extract<keyof TBreakpointsConfig, string>>(media: `@${TBreakpoints}` | `@min-${TBreakpoints}` | `@max-${TBreakpoints}` | `@up-${TBreakpoints}` | `@down-${TBreakpoints}` | `@between-${TBreakpoints}_${TBreakpoints}` | `@only-${TBreakpoints}`, serverValue?: null | boolean) => boolean | null;
|
|
6
|
+
export default createUseMediaQueryWidth;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { getMediaQueryWidthResolvers, isUndefined, } from "@koine/utils";
|
|
3
|
+
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
4
|
+
export let createUseMediaQueryWidth = (customBreakpoints) => {
|
|
5
|
+
const queryResolvers = getMediaQueryWidthResolvers(customBreakpoints);
|
|
6
|
+
return function useMediaQueryWidth(media, serverValue) {
|
|
7
|
+
const definition = media.substring(1);
|
|
8
|
+
let [rule, ruleBreakpoint] = definition.split("-");
|
|
9
|
+
if (isUndefined(ruleBreakpoint)) {
|
|
10
|
+
ruleBreakpoint = rule;
|
|
11
|
+
}
|
|
12
|
+
if (isUndefined(rule)) {
|
|
13
|
+
rule = "min";
|
|
14
|
+
}
|
|
15
|
+
const [br1, br2] = ruleBreakpoint.split("_");
|
|
16
|
+
const query = queryResolvers[rule](br1, br2);
|
|
17
|
+
const [matches, setMatches] = useState(isUndefined(serverValue) ? null : serverValue);
|
|
18
|
+
useIsomorphicLayoutEffect(() => {
|
|
19
|
+
const mq = window.matchMedia(query);
|
|
20
|
+
const handleChange = (event) => {
|
|
21
|
+
setMatches(event.matches);
|
|
22
|
+
};
|
|
23
|
+
setMatches(mq.matches);
|
|
24
|
+
if (!mq.addEventListener) {
|
|
25
|
+
mq.addListener(handleChange);
|
|
26
|
+
return () => {
|
|
27
|
+
mq.removeListener(handleChange);
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
mq.addEventListener("change", handleChange);
|
|
31
|
+
return () => {
|
|
32
|
+
mq.removeEventListener("change", handleChange);
|
|
33
|
+
};
|
|
34
|
+
}, [query]);
|
|
35
|
+
return matches;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export default createUseMediaQueryWidth;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type ExtendableComponent<Props = any> = React.ForwardRefExoticComponent<Props> | React.ExoticComponent<Props> | React.FC<Props> | ((props: Props) => JSX.Element);
|
|
2
|
+
export declare let extendComponent: <Component extends ExtendableComponent<any>, DefaultProps extends {}>(component: Component, defaultProps: DefaultProps) => ((props: import("react").ComponentProps<Component>) => import("react").FunctionComponentElement<any>) & DefaultProps & {
|
|
3
|
+
defaultProps: DefaultProps;
|
|
4
|
+
};
|
|
5
|
+
export interface OverridableComponents {
|
|
6
|
+
[key: string]: {
|
|
7
|
+
type: React.ElementType;
|
|
8
|
+
props: any;
|
|
9
|
+
motionable?: boolean;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
export type WithComponents<Props, Components extends OverridableComponents> = Props & {
|
|
13
|
+
[Name in keyof Components]: NonNullable<Components[Name]["type"] extends keyof JSX.IntrinsicElements ? React.ElementType<Components[Name]["motionable"] extends true ? Omit<React.ComponentPropsWithoutRef<Components[Name]["type"]>, HtmlAttributesCollidingWithMotionProps> & Components[Name]["props"] : React.ComponentPropsWithoutRef<Components[Name]["type"]> & Components[Name]["props"]> : Components[Name]["type"]>;
|
|
14
|
+
};
|
|
15
|
+
type HtmlAttributesCollidingWithMotionProps = "style" | "onDrag" | "onDragStart" | "onDragEnd" | "onAnimationStart" | "onAnimationEnd";
|
|
16
|
+
export default extendComponent;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
export let extendComponent = (component, defaultProps) => {
|
|
3
|
+
const NewComponent = (props) => createElement(component, props);
|
|
4
|
+
return Object.assign(NewComponent, {
|
|
5
|
+
...defaultProps,
|
|
6
|
+
defaultProps,
|
|
7
|
+
});
|
|
8
|
+
};
|
|
9
|
+
export default extendComponent;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { object } from "@kuus/yup";
|
|
2
|
+
import { decode, encode, isUndefined } from "@koine/utils";
|
|
3
|
+
export let encodeForm = (validationRules) => {
|
|
4
|
+
const encoded = {};
|
|
5
|
+
const encodedNames = {};
|
|
6
|
+
for (const name in validationRules) {
|
|
7
|
+
if (!name.startsWith("_")) {
|
|
8
|
+
const encodedName = encode(name);
|
|
9
|
+
encoded[encodedName] = validationRules[name];
|
|
10
|
+
encodedNames[name] = encodedName;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
const encodedSchema = object(encoded).required();
|
|
14
|
+
return { encodedSchema, encodedNames };
|
|
15
|
+
};
|
|
16
|
+
export let decodeForm = (formData) => {
|
|
17
|
+
const json = {};
|
|
18
|
+
for (const encodedName in formData) {
|
|
19
|
+
const decodedName = decode(encodedName);
|
|
20
|
+
if (encodedName.startsWith("_")) {
|
|
21
|
+
json[encodedName.substring(1)] = formData[encodedName];
|
|
22
|
+
}
|
|
23
|
+
else if (!isUndefined(formData[encodedName]) &&
|
|
24
|
+
formData[decodedName] === "") {
|
|
25
|
+
json[decodedName] = formData[encodedName];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return json;
|
|
29
|
+
};
|
package/forms.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./forms/antispam";
|
package/hooks/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export { useAsyncFn, } from "./useAsyncFn";
|
|
2
|
+
export { useFirstMountState } from "./useFirstMountState";
|
|
3
|
+
export { useFixedOffset } from "./useFixedOffset";
|
|
4
|
+
export { useFocus } from "./useFocus";
|
|
5
|
+
export { useInterval } from "./useInterval";
|
|
6
|
+
export { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
7
|
+
export { useKeyUp } from "./useKeyUp";
|
|
8
|
+
export { useMeasure, } from "./useMeasure";
|
|
9
|
+
export { useMountedState } from "./useMountedState";
|
|
10
|
+
export { useNavigateAway, } from "./useNavigateAway";
|
|
11
|
+
export { usePrevious } from "./usePrevious";
|
|
12
|
+
export { usePreviousRef } from "./usePreviousRef";
|
|
13
|
+
export { useScrollPosition } from "./useScrollPosition";
|
|
14
|
+
export { useScrollThreshold } from "./useScrollThreshold";
|
|
15
|
+
export { useSmoothScroll } from "./useSmoothScroll";
|
|
16
|
+
export { useSpinDelay } from "./useSpinDelay";
|
|
17
|
+
export { useTraceUpdate } from "./useTraceUpdate";
|
|
18
|
+
export { useUpdateEffect } from "./useUpdateEffect";
|
|
19
|
+
export { useWindowSize } from "./useWindowSize";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { useCallback, useRef, useState } from "react";
|
|
2
|
+
import { useMountedState } from "./useMountedState";
|
|
3
|
+
export let useAsyncFn = (fn, deps = [], initialState = { loading: false }) => {
|
|
4
|
+
const lastCallId = useRef(0);
|
|
5
|
+
const isMounted = useMountedState();
|
|
6
|
+
const [state, set] = useState(initialState);
|
|
7
|
+
const callback = useCallback((...args) => {
|
|
8
|
+
const callId = ++lastCallId.current;
|
|
9
|
+
if (!state.loading) {
|
|
10
|
+
set((prevState) => ({ ...prevState, loading: true }));
|
|
11
|
+
}
|
|
12
|
+
return fn(...args).then((value) => {
|
|
13
|
+
isMounted() &&
|
|
14
|
+
callId === lastCallId.current &&
|
|
15
|
+
set({ value, loading: false });
|
|
16
|
+
return value;
|
|
17
|
+
}, (error) => {
|
|
18
|
+
isMounted() &&
|
|
19
|
+
callId === lastCallId.current &&
|
|
20
|
+
set({ error, loading: false });
|
|
21
|
+
return error;
|
|
22
|
+
});
|
|
23
|
+
}, deps);
|
|
24
|
+
return [state, callback];
|
|
25
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { debounce } from "@koine/utils";
|
|
3
|
+
import { $each, calculateFixedOffset, injectCss, listenResizeDebounced, } from "@koine/dom";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
5
|
+
const inject = (value) => {
|
|
6
|
+
injectCss("useFixedOffset", `html{scroll-padding-top: ${value}px}`);
|
|
7
|
+
};
|
|
8
|
+
export let useFixedOffset = (selector) => {
|
|
9
|
+
const fixedOffset = useRef(0);
|
|
10
|
+
useIsomorphicLayoutEffect(() => {
|
|
11
|
+
const update = () => {
|
|
12
|
+
const newFixedOffset = calculateFixedOffset();
|
|
13
|
+
fixedOffset.current = newFixedOffset;
|
|
14
|
+
inject(newFixedOffset);
|
|
15
|
+
};
|
|
16
|
+
update();
|
|
17
|
+
if (ResizeObserver) {
|
|
18
|
+
const observer = new ResizeObserver((entries) => {
|
|
19
|
+
let newFixedOffset = 0;
|
|
20
|
+
entries.forEach((entry) => {
|
|
21
|
+
newFixedOffset += entry.contentRect.height;
|
|
22
|
+
});
|
|
23
|
+
fixedOffset.current = newFixedOffset;
|
|
24
|
+
const updateOnResize = debounce(() => inject(newFixedOffset), 400, true);
|
|
25
|
+
updateOnResize();
|
|
26
|
+
});
|
|
27
|
+
$each(selector || "[data-fixed]", ($el) => {
|
|
28
|
+
if (observer)
|
|
29
|
+
observer.observe($el);
|
|
30
|
+
});
|
|
31
|
+
return () => {
|
|
32
|
+
observer?.disconnect();
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
const listener = listenResizeDebounced(0, update);
|
|
37
|
+
return listener;
|
|
38
|
+
}
|
|
39
|
+
}, [selector]);
|
|
40
|
+
return fixedOffset;
|
|
41
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { noop } from "@koine/utils";
|
|
3
|
+
export let useInterval = (callback, delay, deps = []) => {
|
|
4
|
+
const savedCallback = useRef();
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
savedCallback.current = callback;
|
|
7
|
+
}, [callback, ...deps]);
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
function tick() {
|
|
10
|
+
if (savedCallback.current)
|
|
11
|
+
savedCallback.current();
|
|
12
|
+
}
|
|
13
|
+
if (delay !== null) {
|
|
14
|
+
const id = setInterval(tick, delay);
|
|
15
|
+
return () => clearInterval(id);
|
|
16
|
+
}
|
|
17
|
+
return noop;
|
|
18
|
+
}, [delay]);
|
|
19
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { on } from "@koine/dom";
|
|
3
|
+
export let useKeyUp = (callback, deps = []) => {
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const listener = on(window, "keyup", (event) => {
|
|
6
|
+
if (!event.ctrlKey &&
|
|
7
|
+
!event.altKey &&
|
|
8
|
+
!event.shiftKey &&
|
|
9
|
+
!event.metaKey) {
|
|
10
|
+
callback(event);
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
return listener;
|
|
14
|
+
}, [callback, ...deps]);
|
|
15
|
+
};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { debounce, noop } from "@koine/utils";
|
|
3
|
+
import { listenResizeDebounced, listenScrollDebounced, off, on, } from "@koine/dom";
|
|
4
|
+
let observer;
|
|
5
|
+
let findScrollContainers = (element) => {
|
|
6
|
+
const result = [];
|
|
7
|
+
if (!element || element === document.body)
|
|
8
|
+
return result;
|
|
9
|
+
const { overflow, overflowX, overflowY } = window.getComputedStyle(element);
|
|
10
|
+
if ([overflow, overflowX, overflowY].some((prop) => prop === "auto" || prop === "scroll"))
|
|
11
|
+
result.push(element);
|
|
12
|
+
return [...result, ...findScrollContainers(element.parentElement)];
|
|
13
|
+
};
|
|
14
|
+
const keys = [
|
|
15
|
+
"x",
|
|
16
|
+
"y",
|
|
17
|
+
"top",
|
|
18
|
+
"bottom",
|
|
19
|
+
"left",
|
|
20
|
+
"right",
|
|
21
|
+
"width",
|
|
22
|
+
"height",
|
|
23
|
+
];
|
|
24
|
+
const areBoundsEqual = (a, b) => keys.every((key) => a[key] === b[key]);
|
|
25
|
+
export let useMeasure = (options) => {
|
|
26
|
+
const { scroll = false } = options || {};
|
|
27
|
+
const [bounds, setBounds] = useState({
|
|
28
|
+
left: 0,
|
|
29
|
+
top: 0,
|
|
30
|
+
width: 0,
|
|
31
|
+
height: 0,
|
|
32
|
+
bottom: 0,
|
|
33
|
+
right: 0,
|
|
34
|
+
x: 0,
|
|
35
|
+
y: 0,
|
|
36
|
+
});
|
|
37
|
+
const state = useRef([
|
|
38
|
+
null,
|
|
39
|
+
null,
|
|
40
|
+
null,
|
|
41
|
+
bounds,
|
|
42
|
+
]);
|
|
43
|
+
const mounted = useRef(false);
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
mounted.current = true;
|
|
46
|
+
return () => void (mounted.current = false);
|
|
47
|
+
}, []);
|
|
48
|
+
const [forceRefresh, , scrollChange] = useMemo(() => {
|
|
49
|
+
const callback = (..._args) => {
|
|
50
|
+
const [element, , , lastBounds] = state.current;
|
|
51
|
+
if (!element)
|
|
52
|
+
return;
|
|
53
|
+
const size = element.getBoundingClientRect();
|
|
54
|
+
Object.freeze(size);
|
|
55
|
+
if (mounted.current && !areBoundsEqual(lastBounds, size)) {
|
|
56
|
+
state.current[3] = size;
|
|
57
|
+
setBounds(size);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
const debouncedCallback = debounce(callback);
|
|
61
|
+
return [callback, debouncedCallback, debouncedCallback];
|
|
62
|
+
}, [setBounds]);
|
|
63
|
+
function removeListeners() {
|
|
64
|
+
const [, scrollContainers, resizeObserver] = state.current;
|
|
65
|
+
if (scrollContainers) {
|
|
66
|
+
scrollContainers.forEach((element) => off(element, "scroll", scrollChange));
|
|
67
|
+
state.current[1] = null;
|
|
68
|
+
}
|
|
69
|
+
if (resizeObserver) {
|
|
70
|
+
resizeObserver.disconnect();
|
|
71
|
+
state.current[2] = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function addListeners() {
|
|
75
|
+
const [element, scrollContainers] = state.current;
|
|
76
|
+
if (!element)
|
|
77
|
+
return;
|
|
78
|
+
if (!observer && ResizeObserver) {
|
|
79
|
+
observer = new ResizeObserver(scrollChange);
|
|
80
|
+
state.current[2] = observer;
|
|
81
|
+
observer.observe(element);
|
|
82
|
+
if (scroll && scrollContainers) {
|
|
83
|
+
scrollContainers.forEach((scrollContainer) => on(scrollContainer, "scroll", scrollChange, {
|
|
84
|
+
capture: true,
|
|
85
|
+
passive: true,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const ref = (node) => {
|
|
91
|
+
if (!node || node === state.current[0])
|
|
92
|
+
return;
|
|
93
|
+
removeListeners();
|
|
94
|
+
state.current[0] = node;
|
|
95
|
+
state.current[1] = findScrollContainers(node);
|
|
96
|
+
addListeners();
|
|
97
|
+
};
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
if (scroll) {
|
|
100
|
+
const listener = listenScrollDebounced(0, forceRefresh, 100);
|
|
101
|
+
return listener;
|
|
102
|
+
}
|
|
103
|
+
return noop;
|
|
104
|
+
}, [scroll, forceRefresh]);
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
const listener = listenResizeDebounced(0, forceRefresh, 100);
|
|
107
|
+
return listener;
|
|
108
|
+
}, [forceRefresh]);
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
removeListeners();
|
|
111
|
+
addListeners();
|
|
112
|
+
}, [scroll]);
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
forceRefresh();
|
|
115
|
+
return removeListeners;
|
|
116
|
+
}, []);
|
|
117
|
+
return [ref, bounds, forceRefresh];
|
|
118
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
export let useMountedState = () => {
|
|
3
|
+
const mountedRef = useRef(false);
|
|
4
|
+
const get = useCallback(() => mountedRef.current, []);
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
mountedRef.current = true;
|
|
7
|
+
return () => {
|
|
8
|
+
mountedRef.current = false;
|
|
9
|
+
};
|
|
10
|
+
}, []);
|
|
11
|
+
return get;
|
|
12
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from "react";
|
|
2
|
+
import { on } from "@koine/dom";
|
|
3
|
+
export let useNavigateAway = (handler) => {
|
|
4
|
+
const beforeUnloadHandlerRef = useRef();
|
|
5
|
+
useEffect(() => {
|
|
6
|
+
beforeUnloadHandlerRef.current = (event) => {
|
|
7
|
+
const customMessageOrCondition = handler(event);
|
|
8
|
+
if (customMessageOrCondition) {
|
|
9
|
+
event.preventDefault();
|
|
10
|
+
}
|
|
11
|
+
if (typeof customMessageOrCondition === "string") {
|
|
12
|
+
return (event.returnValue = customMessageOrCondition);
|
|
13
|
+
}
|
|
14
|
+
if (event.defaultPrevented) {
|
|
15
|
+
return (event.returnValue = "");
|
|
16
|
+
}
|
|
17
|
+
return;
|
|
18
|
+
};
|
|
19
|
+
}, [handler]);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const listenerBeforeunload = on(window, "beforeunload", (event) => beforeUnloadHandlerRef.current?.(event));
|
|
22
|
+
return listenerBeforeunload;
|
|
23
|
+
}, []);
|
|
24
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
export let useReveal = ({ direction = "left", offsetStartY = -2, offsetEndY = 0, offsetStartX = "all", }) => {
|
|
3
|
+
const ref = useRef(null);
|
|
4
|
+
const [startY, setStartY] = useState(0);
|
|
5
|
+
const [endY, setEndY] = useState(0);
|
|
6
|
+
const [startX, setStartX] = useState(0);
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
if (!ref.current) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
const rect = ref.current.getBoundingClientRect();
|
|
12
|
+
const scrollTop = window.scrollY || document.documentElement.scrollTop;
|
|
13
|
+
const elementHeight = rect.height;
|
|
14
|
+
const elementTop = rect.top;
|
|
15
|
+
const distanceTop = elementTop + scrollTop;
|
|
16
|
+
const offsetTop = offsetStartY ? elementHeight * offsetStartY : 0;
|
|
17
|
+
const offsetBottom = offsetEndY ? elementHeight * offsetEndY : offsetTop;
|
|
18
|
+
const startY = (distanceTop + offsetTop) / document.body.clientHeight;
|
|
19
|
+
const endY = (distanceTop + elementHeight + offsetBottom) / document.body.clientHeight;
|
|
20
|
+
let startX;
|
|
21
|
+
if (offsetStartX === "all") {
|
|
22
|
+
startX = direction === "left" ? -rect.right : rect.left;
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
startX = rect.width * offsetStartX;
|
|
26
|
+
startX = direction === "left" ? -startX : startX;
|
|
27
|
+
}
|
|
28
|
+
setStartY(startY);
|
|
29
|
+
setEndY(endY);
|
|
30
|
+
setStartX(startX);
|
|
31
|
+
}, [
|
|
32
|
+
setStartY,
|
|
33
|
+
setEndY,
|
|
34
|
+
setStartX,
|
|
35
|
+
offsetStartY,
|
|
36
|
+
offsetEndY,
|
|
37
|
+
offsetStartX,
|
|
38
|
+
direction,
|
|
39
|
+
]);
|
|
40
|
+
return { ref, startY, endY, startX };
|
|
41
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
import { isBrowser } from "@koine/utils";
|
|
3
|
+
import { listenScroll } from "@koine/dom";
|
|
4
|
+
import { useIsomorphicLayoutEffect } from "./useIsomorphicLayoutEffect";
|
|
5
|
+
const zeroPosition = { x: 0, y: 0 };
|
|
6
|
+
const getClientRect = (element) => element?.getBoundingClientRect();
|
|
7
|
+
const getScrollPosition = (element, boundingElement) => {
|
|
8
|
+
if (!isBrowser) {
|
|
9
|
+
return zeroPosition;
|
|
10
|
+
}
|
|
11
|
+
if (!boundingElement) {
|
|
12
|
+
return { x: window.scrollX, y: window.scrollY };
|
|
13
|
+
}
|
|
14
|
+
const targetPosition = getClientRect(element?.current || document.body);
|
|
15
|
+
const containerPosition = getClientRect(boundingElement.current);
|
|
16
|
+
if (!targetPosition) {
|
|
17
|
+
return zeroPosition;
|
|
18
|
+
}
|
|
19
|
+
return containerPosition
|
|
20
|
+
? {
|
|
21
|
+
x: (containerPosition.x || 0) - (targetPosition.x || 0),
|
|
22
|
+
y: (containerPosition.y || 0) - (targetPosition.y || 0),
|
|
23
|
+
}
|
|
24
|
+
: { x: targetPosition.left, y: targetPosition.top };
|
|
25
|
+
};
|
|
26
|
+
export let useScrollPosition = (effect, deps = [], element, boundingElement, wait) => {
|
|
27
|
+
const position = useRef(getScrollPosition(null, boundingElement));
|
|
28
|
+
let throttleTimeout = null;
|
|
29
|
+
const callBack = () => {
|
|
30
|
+
const current = getScrollPosition(element, boundingElement);
|
|
31
|
+
effect(current, position.current);
|
|
32
|
+
position.current = current;
|
|
33
|
+
throttleTimeout = null;
|
|
34
|
+
};
|
|
35
|
+
useIsomorphicLayoutEffect(() => {
|
|
36
|
+
if (!isBrowser) {
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
const handleScroll = () => {
|
|
40
|
+
if (wait) {
|
|
41
|
+
if (throttleTimeout === null) {
|
|
42
|
+
throttleTimeout = window.setTimeout(callBack, wait);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
callBack();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const listener = listenScroll(handleScroll, boundingElement?.current);
|
|
50
|
+
return () => {
|
|
51
|
+
listener();
|
|
52
|
+
if (throttleTimeout) {
|
|
53
|
+
clearTimeout(throttleTimeout);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}, deps);
|
|
57
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from "react";
|
|
2
|
+
import { noop } from "@koine/utils";
|
|
3
|
+
import { listenScroll } from "@koine/dom";
|
|
4
|
+
export let useScrollThreshold = (threshold, callback) => {
|
|
5
|
+
const [isBelow, setIsBelow] = useState(false);
|
|
6
|
+
const handler = useCallback(() => {
|
|
7
|
+
if (threshold) {
|
|
8
|
+
const posY = window.scrollY;
|
|
9
|
+
const isAbove = posY < threshold;
|
|
10
|
+
const isBelow = posY > threshold;
|
|
11
|
+
setIsBelow(isBelow);
|
|
12
|
+
if (callback)
|
|
13
|
+
callback(isAbove, isBelow);
|
|
14
|
+
}
|
|
15
|
+
}, [threshold, callback]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (threshold) {
|
|
18
|
+
const listener = listenScroll(handler);
|
|
19
|
+
handler();
|
|
20
|
+
return listener;
|
|
21
|
+
}
|
|
22
|
+
return noop;
|
|
23
|
+
}, [threshold, handler]);
|
|
24
|
+
return isBelow;
|
|
25
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { isBrowser } from "@koine/utils";
|
|
2
|
+
export let useScrollTo = (id = "", offset = 0) => {
|
|
3
|
+
if (!isBrowser) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
const headerOffset = 0;
|
|
7
|
+
let element = document.getElementById(id);
|
|
8
|
+
let top = 0;
|
|
9
|
+
if (element && element.offsetParent) {
|
|
10
|
+
do {
|
|
11
|
+
top += element.offsetTop;
|
|
12
|
+
} while ((element = element.offsetParent));
|
|
13
|
+
}
|
|
14
|
+
top -= offset;
|
|
15
|
+
top -= headerOffset;
|
|
16
|
+
window.scroll(0, top);
|
|
17
|
+
};
|