react-atom-toast
Version:
Tiny & Headless toast for React
486 lines (476 loc) • 14.5 kB
JavaScript
var react = require('react');
var reactTransitionPreset = require('react-transition-preset');
var jsxRuntime = require('react/jsx-runtime');
var client = require('react-dom/client');
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
function useMemoizedFn(fn) {
const fnRef = react.useRef(fn);
fnRef.current = react.useMemo(() => fn, [fn]);
const memoizedFn = react.useRef();
if (!memoizedFn.current) {
memoizedFn.current = function(...args) {
return fnRef.current.apply(this, args);
};
}
return memoizedFn.current;
}
// src/utils.ts
function isBrowser() {
return typeof window !== "undefined" && typeof document !== "undefined";
}
function isFunction(value) {
return typeof value === "function";
}
function omit(obj, keys) {
const copy = __spreadValues({}, obj);
keys.forEach((key) => delete copy[key]);
return copy;
}
function classnames(...args) {
return args.filter(Boolean).join(" ");
}
function defaults(options, defaultOptions) {
const result = __spreadValues({}, defaultOptions);
for (const key in options) {
if (options[key] !== void 0) {
result[key] = options[key];
}
}
return result;
}
function secToMs(ms) {
return (ms || 0) * 1e3;
}
var defaultShouldUpdate = (a, b) => !Object.is(a, b);
function usePrevious(state, shouldUpdate = defaultShouldUpdate) {
const prevRef = react.useRef();
const curRef = react.useRef();
if (shouldUpdate(curRef.current, state)) {
prevRef.current = curRef.current;
curRef.current = state;
}
return prevRef.current;
}
var useUpdate = () => {
const [, setState] = react.useState({});
return react.useCallback(() => setState({}), []);
};
// src/hooks/use-controlled-state.ts
function useControlledState(option) {
const { defaultValue, value, onChange, beforeValue, postValue, onInit } = option;
const isControlled = Object.prototype.hasOwnProperty.call(option, "value") && typeof value !== "undefined";
const initialValue = react.useMemo(() => {
let init = value;
if (isControlled) {
init = value;
} else if (defaultValue !== void 0) {
init = isFunction(defaultValue) ? defaultValue() : defaultValue;
}
return init;
}, []);
react.useEffect(() => {
onInit == null ? void 0 : onInit(initialValue);
}, [initialValue]);
const stateRef = react.useRef(initialValue);
if (isControlled) {
stateRef.current = value;
}
const previousState = usePrevious(stateRef.current);
if (postValue) {
const post = postValue(stateRef.current, previousState);
if (post) {
stateRef.current = post;
}
}
const update = useUpdate();
function triggerChange(newValue) {
let r = isFunction(newValue) ? newValue(stateRef.current) : newValue;
if (beforeValue) {
const before = beforeValue(r, stateRef.current);
if (before) {
r = before;
}
}
if (onChange) {
onChange(r, stateRef.current);
}
if (!isControlled) {
stateRef.current = r;
update();
}
}
return [stateRef.current, useMemoizedFn(triggerChange), previousState];
}
var useIsoLayoutEffect = isBrowser() ? react.useLayoutEffect : react.useEffect;
var createUpdateEffect = (hook) => (effect, deps) => {
const isMounted = react.useRef(false);
hook(() => {
return () => {
isMounted.current = false;
};
}, []);
hook(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
}, deps);
};
var create_update_effect_default = createUpdateEffect;
// src/hooks/use-update-iso-layout-effect.ts
var useUpdateIsoLayoutEffect = create_update_effect_default(useIsoLayoutEffect);
function Toast(props) {
const {
content,
className,
style,
duration,
onOpenChange,
onClosed,
pauseOnHover,
hover,
updateFlag,
transition: _transition,
open: _open,
onEnter: _onEnter,
onUpdate: _onUpdate,
onExited: _onExited,
offsetHeight
} = props;
const didMount = react.useRef(false);
const transition = useMemoizedFn(() => {
if (typeof _transition === "string") {
return { transition: _transition };
}
return {
transition: _transition == null ? void 0 : _transition.name,
duration: _transition == null ? void 0 : _transition.duration,
exitDuration: _transition == null ? void 0 : _transition.exitDuration
};
});
const timer = react.useRef();
const ref = react.useRef(null);
const [open, setOpen] = useControlledState({
defaultValue: !!_open,
value: _open,
onChange: (value) => {
onOpenChange(value);
}
});
const delayClear = useMemoizedFn(() => {
if (duration) {
timer.current && clearTimeout(timer.current);
timer.current = window.setTimeout(() => {
setOpen(false);
timer.current && clearTimeout(timer.current);
}, secToMs(duration));
}
});
useUpdateIsoLayoutEffect(() => {
if (!open) return;
if (pauseOnHover) {
if (hover) {
timer.current && clearTimeout(timer.current);
delayClear();
}
}
}, [hover]);
const onEnter = useMemoizedFn(() => {
if (!ref.current) return;
const height = ref.current.clientHeight;
_onEnter(height);
});
const onUpdate = useMemoizedFn(() => {
var _a;
const height = (_a = ref.current) == null ? void 0 : _a.clientHeight;
_onUpdate(height || 0);
});
const onExited = useMemoizedFn(() => {
var _a;
const height = (_a = ref.current) == null ? void 0 : _a.clientHeight;
_onExited(height || 0);
onClosed == null ? void 0 : onClosed();
});
const resolveTransform = useMemoizedFn((transform, offsetHeight2) => {
const internalTransform = `translate(-50%, calc(-50% - ${offsetHeight2}px))`;
return [internalTransform, transform].filter(Boolean).join(" ");
});
const resolveTransformProperty = useMemoizedFn((transform) => {
return [.../* @__PURE__ */ new Set(["transform", transform])].filter(Boolean).join(", ");
});
useIsoLayoutEffect(() => {
if (!open) return;
if (!didMount.current) {
didMount.current = true;
} else {
onUpdate();
}
delayClear();
return () => {
timer.current && clearTimeout(timer.current);
};
}, [updateFlag]);
useIsoLayoutEffect(() => {
if (!content) {
onExited();
}
}, [content]);
return /* @__PURE__ */ jsxRuntime.jsx(reactTransitionPreset.Transition, __spreadProps(__spreadValues({ mounted: open }, transition()), { onEnter, onExited, initial: true, children: (styles) => /* @__PURE__ */ jsxRuntime.jsx(
"div",
{
style: __spreadProps(__spreadValues(__spreadValues({
position: "fixed",
zIndex: 9999,
top: "50%",
left: "50%"
}, style), styles), {
transitionProperty: resolveTransformProperty(styles.transitionProperty),
transform: resolveTransform(styles.transform, offsetHeight)
}),
ref,
className: classnames("toast__content", className),
children: content
}
) }));
}
var toast_default = Toast;
function ToastContainer(props) {
const { toasts, onClosed, onOpenChange } = props;
const [hoverState, setHoverState] = react.useState(false);
const [heightMap, setHeightMap] = react.useState(/* @__PURE__ */ new Map());
const onEnter = useMemoizedFn((key, height) => {
setHeightMap((prev) => {
prev.set(key, height);
return new Map(prev);
});
});
const onUpdate = useMemoizedFn(onEnter);
const onExited = useMemoizedFn((key) => {
setHeightMap((prev) => {
prev.delete(key);
return new Map(prev);
});
});
const offsetHeight = useMemoizedFn((toast2) => {
const index = toasts.findIndex((t) => t.key === toast2.key);
let offset = 0;
const start = toasts.length - 1;
for (let i = start; i > index; i--) {
const currentToastHeight = heightMap.get(toasts[i].key) || 0;
const nextToastHeight = heightMap.get(toasts[i - 1].key) || 0;
offset += nextToastHeight / 2 + currentToastHeight / 2 + toasts[index].gap;
}
return offset;
});
return /* @__PURE__ */ jsxRuntime.jsx(
"div",
{
onMouseEnter: () => {
setHoverState(true);
},
onMouseLeave: () => {
setHoverState(false);
},
className: "toast__container",
children: toasts.map((toast2) => /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: toast2.render(
/* @__PURE__ */ jsxRuntime.jsx(
toast_default,
__spreadProps(__spreadValues({}, omit(toast2, ["key"])), {
onOpenChange: (open) => onOpenChange(toast2.key, open),
onEnter: (height) => {
onEnter(toast2.key, height);
},
onUpdate: (height) => {
onUpdate(toast2.key, height);
},
onExited: () => {
onExited(toast2.key);
},
onClosed: () => {
var _a;
(_a = toast2.onClosed) == null ? void 0 : _a.call(toast2);
onClosed(toast2.key);
},
hover: hoverState,
offsetHeight: offsetHeight(toast2)
})
)
) }, toast2.key))
}
);
}
var toast_container_default = ToastContainer;
var MARK = "__react_root__";
function mount(node, container) {
const root = container[MARK] || client.createRoot(container);
root.render(node);
container[MARK] = root;
}
var Renderer = class {
constructor(queue) {
__publicField(this, "containerID", "react-atom-toast");
__publicField(this, "container", null);
__publicField(this, "queue");
this.queue = queue;
}
createContainer() {
if (isBrowser()) {
let container = document.getElementById(this.containerID);
if (!container) {
container = document.createElement("div");
container.id = this.containerID;
document.body.appendChild(container);
}
this.container = container;
}
}
render(toasts) {
if (!toasts.length && this.container) {
this.container.remove();
this.container = null;
return;
}
this.createContainer();
if (!this.container) return;
this.reactMount(toasts, this.container);
}
reactMount(toasts, container) {
mount(
/* @__PURE__ */ jsxRuntime.jsx(
toast_container_default,
{
toasts,
onClosed: (key) => this.queue.remove(key),
onOpenChange: (key, open) => {
this.queue.update(key, { open });
}
}
),
container
);
}
};
// src/toast-queue.ts
var ToastQueue = class {
constructor() {
__publicField(this, "renderer");
__publicField(this, "toasts", []);
this.renderer = new Renderer(this);
}
render() {
this.renderer.render(this.toasts);
}
add(options) {
var _a;
const { maxCount, key } = options;
if (key && this.toasts.some((t) => t.key === key)) {
this.update(key, options);
return;
}
const visibleToasts = this.toasts.filter((t) => t.open === true);
if (maxCount && visibleToasts.length >= maxCount) {
if (maxCount === 1) {
for (let i = 0; i < visibleToasts.length - 1; i++) {
this.remove(visibleToasts[i].key);
}
this.update(visibleToasts[visibleToasts.length - 1].key, __spreadProps(__spreadValues({}, options), { open: true }));
return;
} else {
this.close((_a = visibleToasts[0]) == null ? void 0 : _a.key);
}
}
const toastOptions = __spreadProps(__spreadValues({}, options), {
key: options.key || Math.random().toString(36),
open: true
});
this.toasts.push(toastOptions);
this.render();
}
close(key) {
if (!key) return;
this.update(key, { open: false });
}
closeAll() {
this.toasts = this.toasts.map((toastOptions) => __spreadProps(__spreadValues({}, toastOptions), {
open: false
}));
this.render();
}
remove(key) {
this.toasts = this.toasts.filter((toastOptions) => toastOptions.key !== key);
this.render();
}
removeAll() {
this.toasts = [];
this.render();
}
update(key, options) {
const index = this.toasts.findIndex((toastOptions) => toastOptions.key === key);
if (index !== -1) {
this.toasts[index] = __spreadProps(__spreadValues(__spreadValues({}, this.toasts[index]), options), {
updateFlag: !this.toasts[index].updateFlag
});
this.render();
}
}
};
// src/Toast.ts
var Toast2 = class {
constructor() {
__publicField(this, "defaultOptions", {
duration: 2,
pauseOnHover: true,
transition: "fade",
maxCount: 3,
gap: 16,
render: (children) => children
});
__publicField(this, "toastQueue");
__publicField(this, "open", (options) => {
if (!(options && typeof options === "object" && "content" in options)) {
options = {
content: options
};
}
this.toastQueue.add(defaults(options, this.defaultOptions));
});
__publicField(this, "close", (key) => {
this.toastQueue.close(key);
});
__publicField(this, "closeAll", () => {
this.toastQueue.closeAll();
});
__publicField(this, "update", (key, options) => {
this.toastQueue.update(key, __spreadValues({}, options));
});
__publicField(this, "setDefaultOptions", (options) => {
this.defaultOptions = defaults(options, this.defaultOptions);
});
this.toastQueue = new ToastQueue();
}
};
// src/index.ts
var toast = new Toast2();
exports.Toast = Toast2;
exports.toast = toast;
;