release: v0.4.0 — React 适配器、loading链式id稳定、dragThreshold、RTL适配、dedupe插件
- React 适配器子包:@metona-team/metona-toast/react — useToast() hook + 声明式 <Toast /> 组件,组件卸载自动清理;react 为 optional peerDependency,主包保持零依赖 - loading 链式转换改原地 update:id 稳定不重建 DOM,转换后 duration 恢复默认自动关闭;update() 支持 duration 变更动态重启计时器 - _emit 幽灵实例修复:beforeShow 拦截后的 toast 不再注册进 _toasts - dragThreshold 配置项(默认 120px,原硬编码);RTL 容器 dir 属性 + 进度条/side 条/色条镜像 - dedupe 预设插件:相同 type+message 自动去重,支持 uninstall - jest 环境隔离测试独立成 tests/ssr.test.ts(resetModules 不再污染共享模块状态);新增 tests/setup.ts 补 TextEncoder - 文档:README/docs.html 补 React 适配器、dedupe、dragThreshold;CHANGELOG 更新;328 测试全过
This commit is contained in:
+17
-7
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
@@ -78,7 +78,10 @@ const meToast: MeToast = {
|
||||
|
||||
const t = new Toast(merged);
|
||||
t.create();
|
||||
this._toasts.set(t.id, t);
|
||||
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||
if (t.el !== null) {
|
||||
this._toasts.set(t.id, t);
|
||||
}
|
||||
|
||||
return t;
|
||||
},
|
||||
@@ -159,15 +162,22 @@ const meToast: MeToast = {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||
*/
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
||||
const id = loadingToast.id;
|
||||
const old = this._toasts.get(id);
|
||||
const old = this._toasts.get(loadingToast.id);
|
||||
if (!old) return null;
|
||||
|
||||
const position = old.config.position;
|
||||
old.close();
|
||||
old.update({
|
||||
...opts,
|
||||
type,
|
||||
message,
|
||||
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||
} as ToastOptions);
|
||||
|
||||
return this._emit({ ...opts, type, message, position } as ToastOptions);
|
||||
return old;
|
||||
},
|
||||
|
||||
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import { ICONS } from './icons.js';
|
||||
@@ -12,7 +12,7 @@ export { ICONS, LOCALES };
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
export const VERSION = '0.3.0';
|
||||
export const VERSION = '0.4.0';
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
@@ -26,6 +26,7 @@ export const DEFAULTS = Object.freeze({
|
||||
pauseOnHover: true,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
dragThreshold: 120,
|
||||
showProgress: true,
|
||||
progressDirection: 'horizontal' as const,
|
||||
icon: true,
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import { LOCALES } from './constants.js';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
|
||||
|
||||
+33
-2
@@ -1,11 +1,12 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import type { Plugin, PluginManager as IPluginManager, PluginUtils } from './types.js';
|
||||
import { Toast } from './toast.js';
|
||||
import type { Plugin, PluginManager as IPluginManager, PluginUtils, ToastInstance } from './types.js';
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -176,6 +177,36 @@ const presetPlugins: Record<string, Plugin> = {
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
*/
|
||||
dedupe: {
|
||||
name: 'dedupe',
|
||||
version: '1.0.0',
|
||||
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||
_off: null as (() => void) | null,
|
||||
|
||||
install(this: Plugin) {
|
||||
const off = Toast.on('beforeShow', (toast: ToastInstance) => {
|
||||
const existing = Array.from(Toast._registry.values()).find(
|
||||
t => !t.closing && t.type === toast.type && t.message === toast.message
|
||||
);
|
||||
if (existing) {
|
||||
existing.update({ title: toast.title || existing.title });
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
(this as Record<string, unknown>)._off = off;
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
const off = (this as Record<string, unknown>)._off as (() => void) | null;
|
||||
if (off) off();
|
||||
(this as Record<string, unknown>)._off = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.4.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import MeToast from './index.js';
|
||||
import type { ToastInstance, ToastOptions } from './types.js';
|
||||
|
||||
/** 支持声明式渲染的 Toast 类型 */
|
||||
export type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||
|
||||
export interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||
message: string;
|
||||
type?: ReactToastType;
|
||||
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||
autoClose?: boolean;
|
||||
}
|
||||
|
||||
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'] as const;
|
||||
type ShowMethod = (typeof SHOW_METHODS)[number];
|
||||
|
||||
/** 有 id 的可追踪对象(ToastInstance / LoadingControl) */
|
||||
interface Tracked {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const showToast = (method: ShowMethod, message: string, opts: Omit<ToastProps, 'message' | 'type' | 'autoClose'>): ToastInstance => {
|
||||
switch (method) {
|
||||
case 'success': return MeToast.success(message, opts);
|
||||
case 'error': return MeToast.error(message, opts);
|
||||
case 'warning': return MeToast.warning(message, opts);
|
||||
case 'info': return MeToast.info(message, opts);
|
||||
default: return MeToast.show(message, opts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||
* 纯函数,便于独立测试与扩展。
|
||||
*/
|
||||
export const createBoundApi = (cleanup: Set<string>): Record<string, unknown> => {
|
||||
const track = <T extends Tracked | null>(fn: () => T): T => {
|
||||
const result = fn();
|
||||
if (result && result.id) cleanup.add(result.id);
|
||||
return result;
|
||||
};
|
||||
|
||||
const bound: Record<string, unknown> = {
|
||||
...MeToast,
|
||||
|
||||
success: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.success(messageOrOpts, opts)),
|
||||
error: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.error(messageOrOpts, opts)),
|
||||
warning: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.warning(messageOrOpts, opts)),
|
||||
info: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.info(messageOrOpts, opts)),
|
||||
show: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.show(messageOrOpts, opts)),
|
||||
loading: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.loading(messageOrOpts, opts)),
|
||||
|
||||
// 通过 id 关闭时同步清理集合,避免卸载时重复移除
|
||||
dismiss: (id?: string) => {
|
||||
if (id) cleanup.delete(id);
|
||||
return MeToast.dismiss(id);
|
||||
},
|
||||
removeToast: (id: string) => {
|
||||
cleanup.delete(id);
|
||||
return MeToast.removeToast(id);
|
||||
},
|
||||
};
|
||||
|
||||
return bound;
|
||||
};
|
||||
|
||||
/**
|
||||
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||
*/
|
||||
export const useToast = (): Record<string, unknown> => {
|
||||
const cleanupRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const set = cleanupRef.current;
|
||||
return () => {
|
||||
set.forEach(id => MeToast.removeToast(id));
|
||||
set.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(() => createBoundApi(cleanupRef.current), []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||
*/
|
||||
export const Toast = (props: ToastProps): null => {
|
||||
const { message, type, autoClose = true, ...rest } = props;
|
||||
const toastRef = useRef<ToastInstance | null>(null);
|
||||
const restKey = JSON.stringify(rest);
|
||||
|
||||
useEffect(() => {
|
||||
const method: ShowMethod = SHOW_METHODS.includes(type as ShowMethod) ? (type as ShowMethod) : 'show';
|
||||
toastRef.current = showToast(method, message, rest);
|
||||
|
||||
return () => {
|
||||
if (autoClose) toastRef.current?.remove();
|
||||
toastRef.current = null;
|
||||
};
|
||||
// autoClose 不参与重建依赖:它只影响卸载时的清理行为
|
||||
}, [message, type, restKey]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export { MeToast };
|
||||
+36
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import type { ThemeConfig } from './types.js';
|
||||
@@ -363,6 +363,41 @@ const generateCSS = (): string => {
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||
[dir="rtl"] .met-bar {
|
||||
transform-origin: right;
|
||||
}
|
||||
[dir="rtl"] .met-side {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
[dir="rtl"] .met-progress-v {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-success {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #10b981;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-error {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #ef4444;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-warning {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #f59e0b;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-info {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #3b82f6;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-loading {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #6366f1;
|
||||
}
|
||||
|
||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
||||
.met-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
|
||||
+17
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
import { generateId, escapeHTML } from './utils.js';
|
||||
@@ -204,6 +204,8 @@ export class Toast implements ToastInstance {
|
||||
el.className = `met-container ${position}`;
|
||||
el.setAttribute('aria-label', 'Notifications');
|
||||
el.setAttribute('role', 'region');
|
||||
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||
if (isRTL) el.setAttribute('dir', 'rtl');
|
||||
|
||||
const posStyles: Record<string, string> = {
|
||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||
@@ -417,7 +419,8 @@ export class Toast implements ToastInstance {
|
||||
el.releasePointerCapture(e.pointerId);
|
||||
el.style.transition = '';
|
||||
|
||||
if (Math.abs(dx) > 120) {
|
||||
const threshold = this.config.dragThreshold ?? 120;
|
||||
if (Math.abs(dx) > threshold) {
|
||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => this.close(true), 250);
|
||||
@@ -535,6 +538,18 @@ export class Toast implements ToastInstance {
|
||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||
}
|
||||
|
||||
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||
this.config.duration = partial.duration;
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
this.remaining = Math.max(0, partial.duration);
|
||||
if (partial.duration > 0 && !this.paused) {
|
||||
this.startedAt = Date.now();
|
||||
this._startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
|
||||
+2
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
// ========== 基础类型 ==========
|
||||
@@ -72,6 +72,7 @@ export interface ToastConfig {
|
||||
pauseOnHover?: boolean;
|
||||
closeOnClick?: boolean;
|
||||
draggable?: boolean;
|
||||
dragThreshold?: number;
|
||||
showProgress?: boolean;
|
||||
progressDirection?: ToastProgressDirection;
|
||||
icon?: boolean;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.3.0
|
||||
* @version 0.4.0
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user