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:
tianhao
2026-08-08 14:45:19 +08:00
parent 2937ce5e2d
commit bbff631f4f
41 changed files with 10761 additions and 161 deletions
+108 -20
View File
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
/**
* MetonaToast Utils — 工具函数
* @module utils
* @version 0.3.0
* @version 0.4.0
*/
/**
* 生成唯一ID
@@ -42,7 +42,7 @@ const prefersDark = () => {
/**
* MetonaToast Icons — 图标SVG定义
* @module icons
* @version 0.3.0
* @version 0.4.0
* @description 107 个内置 SVG 图标
*/
const ICONS = {
@@ -539,7 +539,7 @@ const ICONS = {
/**
* MetonaToast Locales — 国际化翻译数据
* @module locales
* @version 0.3.0
* @version 0.4.0
* @description 内置 zh-CN / en-US 完整翻译
*/
const LOCALES = {
@@ -1026,12 +1026,12 @@ const LOCALES = {
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.3.0
* @version 0.4.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
const VERSION = '0.3.0';
const VERSION = '0.4.0';
/**
* 默认配置
*/
@@ -1044,6 +1044,7 @@ const DEFAULTS = Object.freeze({
pauseOnHover: true,
closeOnClick: true,
draggable: true,
dragThreshold: 120,
showProgress: true,
progressDirection: 'horizontal',
icon: true,
@@ -1297,7 +1298,7 @@ const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', '
/**
* MetonaToast Styles — 样式管理
* @module styles
* @version 0.3.0
* @version 0.4.0
*/
// 样式缓存
let styleElement = null;
@@ -1655,6 +1656,41 @@ const generateCSS = () => {
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; }
@@ -1751,7 +1787,7 @@ const injectStyles = () => {
/**
* MetonaToast Themes — 主题管理
* @module themes
* @version 0.3.0
* @version 0.4.0
*/
// 当前主题状态
let currentTheme = 'auto';
@@ -2095,7 +2131,7 @@ const themeUtils = {
/**
* MetonaToast i18n — 国际化管理
* @module i18n
* @version 0.3.0
* @version 0.4.0
*/
// 当前语言状态
let currentLocale = 'zh-CN';
@@ -2770,7 +2806,7 @@ const animationUtils = {
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.3.0
* @version 0.4.0
*/
/**
* Toast 类 — 核心通知组件
@@ -2931,6 +2967,9 @@ class Toast {
el.className = `met-container ${position}`;
el.setAttribute('aria-label', 'Notifications');
el.setAttribute('role', 'region');
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
if (isRTL)
el.setAttribute('dir', 'rtl');
const posStyles = {
'top-left': 'top:0;left:0;align-items:flex-start',
'top-center': 'top:0;left:0;right:0;align-items:center',
@@ -3122,7 +3161,8 @@ class Toast {
dragging = false;
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);
@@ -3240,6 +3280,18 @@ class Toast {
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
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)
@@ -3386,7 +3438,7 @@ const _containerCache = new Map();
/**
* MetonaToast Templates — HTML 模板辅助函数
* @module templates
* @version 0.3.0
* @version 0.4.0
* @description confirm / prompt / progress / action 的 DOM 模板
*/
/**
@@ -3458,7 +3510,7 @@ const actionHTML = (actions) => {
/**
* MetonaToast Plugins — 插件系统
* @module plugins
* @version 0.3.0
* @version 0.4.0
*/
/**
* 插件管理器
@@ -3636,6 +3688,32 @@ const presetPlugins = {
el.parentNode.removeChild(el); }, 3000);
},
},
/**
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
*/
dedupe: {
name: 'dedupe',
version: '1.0.0',
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
_off: null,
install() {
const off = Toast.on('beforeShow', (toast) => {
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._off = off;
},
uninstall() {
const off = this._off;
if (off)
off();
this._off = null;
},
},
};
/**
* 插件工具
@@ -3678,7 +3756,7 @@ const defaultPluginManager = new PluginManager();
/**
* MetonaToast API — meToast 核心 API 对象
* @module api
* @version 0.3.0
* @version 0.4.0
*/
/**
* 参数标准化工具
@@ -3729,7 +3807,10 @@ const meToast = {
merged.message = merged.content;
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;
},
_remove(id) {
@@ -3795,14 +3876,21 @@ const meToast = {
throw err;
});
},
/**
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
*/
_resolve(loadingToast, type, message, opts) {
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();
return this._emit({ ...opts, type, message, position });
old.update({
...opts,
type,
message,
duration: opts.duration ?? (old.config.duration || this._config.duration),
});
return old;
},
confirm(message, opts = {}) {
if (typeof message !== 'string') {
@@ -4340,7 +4428,7 @@ Toast.setCallbacks({
/**
* MetonaToast — 轻量级Toast通知库
* @module metona-toast
* @version 0.3.0
* @version 0.4.0
* @author thzxx
* @license MIT
*/
+1 -1
View File
File diff suppressed because one or more lines are too long
+5 -4
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast — 核心类型定义
* @module types
* @version 0.3.0
* @version 0.4.0
*/
/** Toast 通知类型(107种内置图标类型) */
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | 'check' | 'x' | 'alert' | 'question' | 'star' | 'heart' | 'bell' | 'mail' | 'settings' | 'user' | 'home' | 'search' | 'plus' | 'minus' | 'edit' | 'trash' | 'download' | 'upload' | 'share' | 'link' | 'external' | 'clock' | 'calendar' | 'map' | 'compass' | 'globe' | 'wifi' | 'cloud' | 'sun' | 'moon' | 'zap' | 'activity' | 'cpu' | 'database' | 'server' | 'terminal' | 'code' | 'git' | 'package' | 'layers' | 'grid' | 'list' | 'filter' | 'sort' | 'refresh' | 'sync' | 'power' | 'battery' | 'bluetooth' | 'volume' | 'mic' | 'camera' | 'image' | 'video' | 'music' | 'file' | 'folder' | 'clipboard' | 'save' | 'print' | 'eye' | 'eyeOff' | 'lock' | 'unlock' | 'shield' | 'key' | 'flag' | 'bookmark' | 'tag' | 'gift' | 'award' | 'target' | 'crosshair' | 'move' | 'maximize' | 'minimize' | 'copy' | 'cut' | 'paste' | 'rotateCw' | 'rotateCcw' | 'zoomIn' | 'zoomOut' | 'crop' | 'sliders' | 'toggleLeft' | 'toggleRight' | 'checkCircle' | 'xCircle' | 'alertCircle' | 'infoCircle' | 'helpCircle' | 'alertTriangle' | 'checkSquare' | 'square' | 'circle' | 'triangle' | 'hexagon' | 'octagon' | 'pentagon' | 'diamond';
@@ -26,6 +26,7 @@ interface ToastConfig {
pauseOnHover?: boolean;
closeOnClick?: boolean;
draggable?: boolean;
dragThreshold?: number;
showProgress?: boolean;
progressDirection?: ToastProgressDirection;
icon?: boolean;
@@ -417,7 +418,7 @@ declare global {
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.3.0
* @version 0.4.0
*/
/**
@@ -491,13 +492,13 @@ declare class Toast implements ToastInstance {
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.3.0
* @version 0.4.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
declare const VERSION = "0.3.0";
declare const VERSION = "0.4.0";
/**
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
+108 -20
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Utils — 工具函数
* @module utils
* @version 0.3.0
* @version 0.4.0
*/
/**
* 生成唯一ID
@@ -38,7 +38,7 @@ const prefersDark = () => {
/**
* MetonaToast Icons — 图标SVG定义
* @module icons
* @version 0.3.0
* @version 0.4.0
* @description 107 个内置 SVG 图标
*/
const ICONS = {
@@ -535,7 +535,7 @@ const ICONS = {
/**
* MetonaToast Locales — 国际化翻译数据
* @module locales
* @version 0.3.0
* @version 0.4.0
* @description 内置 zh-CN / en-US 完整翻译
*/
const LOCALES = {
@@ -1022,12 +1022,12 @@ const LOCALES = {
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.3.0
* @version 0.4.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
const VERSION = '0.3.0';
const VERSION = '0.4.0';
/**
* 默认配置
*/
@@ -1040,6 +1040,7 @@ const DEFAULTS = Object.freeze({
pauseOnHover: true,
closeOnClick: true,
draggable: true,
dragThreshold: 120,
showProgress: true,
progressDirection: 'horizontal',
icon: true,
@@ -1293,7 +1294,7 @@ const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', '
/**
* MetonaToast Styles — 样式管理
* @module styles
* @version 0.3.0
* @version 0.4.0
*/
// 样式缓存
let styleElement = null;
@@ -1651,6 +1652,41 @@ const generateCSS = () => {
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; }
@@ -1747,7 +1783,7 @@ const injectStyles = () => {
/**
* MetonaToast Themes — 主题管理
* @module themes
* @version 0.3.0
* @version 0.4.0
*/
// 当前主题状态
let currentTheme = 'auto';
@@ -2091,7 +2127,7 @@ const themeUtils = {
/**
* MetonaToast i18n — 国际化管理
* @module i18n
* @version 0.3.0
* @version 0.4.0
*/
// 当前语言状态
let currentLocale = 'zh-CN';
@@ -2766,7 +2802,7 @@ const animationUtils = {
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.3.0
* @version 0.4.0
*/
/**
* Toast 类 — 核心通知组件
@@ -2927,6 +2963,9 @@ class Toast {
el.className = `met-container ${position}`;
el.setAttribute('aria-label', 'Notifications');
el.setAttribute('role', 'region');
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
if (isRTL)
el.setAttribute('dir', 'rtl');
const posStyles = {
'top-left': 'top:0;left:0;align-items:flex-start',
'top-center': 'top:0;left:0;right:0;align-items:center',
@@ -3118,7 +3157,8 @@ class Toast {
dragging = false;
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);
@@ -3236,6 +3276,18 @@ class Toast {
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
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)
@@ -3382,7 +3434,7 @@ const _containerCache = new Map();
/**
* MetonaToast Templates — HTML 模板辅助函数
* @module templates
* @version 0.3.0
* @version 0.4.0
* @description confirm / prompt / progress / action 的 DOM 模板
*/
/**
@@ -3454,7 +3506,7 @@ const actionHTML = (actions) => {
/**
* MetonaToast Plugins — 插件系统
* @module plugins
* @version 0.3.0
* @version 0.4.0
*/
/**
* 插件管理器
@@ -3632,6 +3684,32 @@ const presetPlugins = {
el.parentNode.removeChild(el); }, 3000);
},
},
/**
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
*/
dedupe: {
name: 'dedupe',
version: '1.0.0',
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
_off: null,
install() {
const off = Toast.on('beforeShow', (toast) => {
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._off = off;
},
uninstall() {
const off = this._off;
if (off)
off();
this._off = null;
},
},
};
/**
* 插件工具
@@ -3674,7 +3752,7 @@ const defaultPluginManager = new PluginManager();
/**
* MetonaToast API — meToast 核心 API 对象
* @module api
* @version 0.3.0
* @version 0.4.0
*/
/**
* 参数标准化工具
@@ -3725,7 +3803,10 @@ const meToast = {
merged.message = merged.content;
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;
},
_remove(id) {
@@ -3791,14 +3872,21 @@ const meToast = {
throw err;
});
},
/**
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
*/
_resolve(loadingToast, type, message, opts) {
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();
return this._emit({ ...opts, type, message, position });
old.update({
...opts,
type,
message,
duration: opts.duration ?? (old.config.duration || this._config.duration),
});
return old;
},
confirm(message, opts = {}) {
if (typeof message !== 'string') {
@@ -4336,7 +4424,7 @@ Toast.setCallbacks({
/**
* MetonaToast — 轻量级Toast通知库
* @module metona-toast
* @version 0.3.0
* @version 0.4.0
* @author thzxx
* @license MIT
*/
+1 -1
View File
File diff suppressed because one or more lines are too long
+108 -20
View File
@@ -7,7 +7,7 @@
/**
* MetonaToast Utils — 工具函数
* @module utils
* @version 0.3.0
* @version 0.4.0
*/
/**
* 生成唯一ID
@@ -44,7 +44,7 @@
/**
* MetonaToast Icons — 图标SVG定义
* @module icons
* @version 0.3.0
* @version 0.4.0
* @description 107 个内置 SVG 图标
*/
const ICONS = {
@@ -541,7 +541,7 @@
/**
* MetonaToast Locales — 国际化翻译数据
* @module locales
* @version 0.3.0
* @version 0.4.0
* @description 内置 zh-CN / en-US 完整翻译
*/
const LOCALES = {
@@ -1028,12 +1028,12 @@
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.3.0
* @version 0.4.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
const VERSION = '0.3.0';
const VERSION = '0.4.0';
/**
* 默认配置
*/
@@ -1046,6 +1046,7 @@
pauseOnHover: true,
closeOnClick: true,
draggable: true,
dragThreshold: 120,
showProgress: true,
progressDirection: 'horizontal',
icon: true,
@@ -1299,7 +1300,7 @@
/**
* MetonaToast Styles — 样式管理
* @module styles
* @version 0.3.0
* @version 0.4.0
*/
// 样式缓存
let styleElement = null;
@@ -1657,6 +1658,41 @@
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; }
@@ -1753,7 +1789,7 @@
/**
* MetonaToast Themes — 主题管理
* @module themes
* @version 0.3.0
* @version 0.4.0
*/
// 当前主题状态
let currentTheme = 'auto';
@@ -2097,7 +2133,7 @@
/**
* MetonaToast i18n — 国际化管理
* @module i18n
* @version 0.3.0
* @version 0.4.0
*/
// 当前语言状态
let currentLocale = 'zh-CN';
@@ -2772,7 +2808,7 @@
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.3.0
* @version 0.4.0
*/
/**
* Toast 类 — 核心通知组件
@@ -2933,6 +2969,9 @@
el.className = `met-container ${position}`;
el.setAttribute('aria-label', 'Notifications');
el.setAttribute('role', 'region');
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
if (isRTL)
el.setAttribute('dir', 'rtl');
const posStyles = {
'top-left': 'top:0;left:0;align-items:flex-start',
'top-center': 'top:0;left:0;right:0;align-items:center',
@@ -3124,7 +3163,8 @@
dragging = false;
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);
@@ -3242,6 +3282,18 @@
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
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)
@@ -3388,7 +3440,7 @@
/**
* MetonaToast Templates — HTML 模板辅助函数
* @module templates
* @version 0.3.0
* @version 0.4.0
* @description confirm / prompt / progress / action 的 DOM 模板
*/
/**
@@ -3460,7 +3512,7 @@
/**
* MetonaToast Plugins — 插件系统
* @module plugins
* @version 0.3.0
* @version 0.4.0
*/
/**
* 插件管理器
@@ -3638,6 +3690,32 @@
el.parentNode.removeChild(el); }, 3000);
},
},
/**
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
*/
dedupe: {
name: 'dedupe',
version: '1.0.0',
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
_off: null,
install() {
const off = Toast.on('beforeShow', (toast) => {
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._off = off;
},
uninstall() {
const off = this._off;
if (off)
off();
this._off = null;
},
},
};
/**
* 插件工具
@@ -3680,7 +3758,7 @@
/**
* MetonaToast API — meToast 核心 API 对象
* @module api
* @version 0.3.0
* @version 0.4.0
*/
/**
* 参数标准化工具
@@ -3731,7 +3809,10 @@
merged.message = merged.content;
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;
},
_remove(id) {
@@ -3797,14 +3878,21 @@
throw err;
});
},
/**
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
*/
_resolve(loadingToast, type, message, opts) {
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();
return this._emit({ ...opts, type, message, position });
old.update({
...opts,
type,
message,
duration: opts.duration ?? (old.config.duration || this._config.duration),
});
return old;
},
confirm(message, opts = {}) {
if (typeof message !== 'string') {
@@ -4342,7 +4430,7 @@
/**
* MetonaToast — 轻量级Toast通知库
* @module metona-toast
* @version 0.3.0
* @version 0.4.0
* @author thzxx
* @license MIT
*/
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long
+4582
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
+509
View File
@@ -0,0 +1,509 @@
/**
* MetonaToast
* @module types
* @version 0.4.0
*/
/** Toast 通知类型(107种内置图标类型) */
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | 'check' | 'x' | 'alert' | 'question' | 'star' | 'heart' | 'bell' | 'mail' | 'settings' | 'user' | 'home' | 'search' | 'plus' | 'minus' | 'edit' | 'trash' | 'download' | 'upload' | 'share' | 'link' | 'external' | 'clock' | 'calendar' | 'map' | 'compass' | 'globe' | 'wifi' | 'cloud' | 'sun' | 'moon' | 'zap' | 'activity' | 'cpu' | 'database' | 'server' | 'terminal' | 'code' | 'git' | 'package' | 'layers' | 'grid' | 'list' | 'filter' | 'sort' | 'refresh' | 'sync' | 'power' | 'battery' | 'bluetooth' | 'volume' | 'mic' | 'camera' | 'image' | 'video' | 'music' | 'file' | 'folder' | 'clipboard' | 'save' | 'print' | 'eye' | 'eyeOff' | 'lock' | 'unlock' | 'shield' | 'key' | 'flag' | 'bookmark' | 'tag' | 'gift' | 'award' | 'target' | 'crosshair' | 'move' | 'maximize' | 'minimize' | 'copy' | 'cut' | 'paste' | 'rotateCw' | 'rotateCcw' | 'zoomIn' | 'zoomOut' | 'crop' | 'sliders' | 'toggleLeft' | 'toggleRight' | 'checkCircle' | 'xCircle' | 'alertCircle' | 'infoCircle' | 'helpCircle' | 'alertTriangle' | 'checkSquare' | 'square' | 'circle' | 'triangle' | 'hexagon' | 'octagon' | 'pentagon' | 'diamond';
/** Toast 显示位置 */
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
/** Toast 主题 */
type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
/** Toast 动画类型 */
type ToastAnimation = 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | (string & {});
/** 进度条方向 */
type ToastProgressDirection = 'horizontal' | 'vertical';
interface TypeColor {
fg: string;
bg: string;
}
interface ToastConfig {
position?: ToastPosition;
duration?: number;
max?: number;
gap?: number;
offset?: number;
pauseOnHover?: boolean;
closeOnClick?: boolean;
draggable?: boolean;
dragThreshold?: number;
showProgress?: boolean;
progressDirection?: ToastProgressDirection;
icon?: boolean;
closeButton?: boolean;
theme?: ToastTheme;
animation?: ToastAnimation;
zIndex?: number;
width?: number | string;
className?: string;
style?: Record<string, string>;
onShow?: ((toast: ToastInstance) => void) | null;
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
onClose?: ((toast: ToastInstance) => void) | null;
onClick?: ((toast: ToastInstance) => void) | null;
onUpdate?: ((toast: ToastInstance) => void) | null;
render?: ((toast: ToastInstance) => string) | null;
resetTimerOnUpdate?: boolean;
notifyWhenHidden?: boolean;
onError?: ((errorInfo: ErrorInfo) => void) | null;
group?: string | null;
locale?: string;
plugins?: Array<string | Plugin>;
}
interface ErrorInfo {
hook?: string;
source?: string;
error: Error;
toast?: ToastInstance;
}
interface ToastOptions extends Partial<ToastConfig> {
type?: string;
title?: string;
message?: string;
content?: string;
html?: string;
iconHTML?: string;
id?: string;
}
interface ToastInstance {
id: string;
type: string;
title: string;
message: string;
html: string;
iconHTML: string;
config: ToastConfig;
el: HTMLElement | null;
barEl: HTMLElement | null;
rafId: number | null;
remaining: number;
startedAt: number;
paused: boolean;
closing: boolean;
group: string | null;
_cleanups: Array<() => void>;
_palette(): {
theme: string;
c: TypeColor;
t: Partial<ThemeConfig>;
};
create(): this;
update(partial: Partial<ToastOptions>): this;
updatePosition(position: string): this;
remove(): void;
close(immediate?: boolean): void;
_pause(): void;
_resume(): void;
_destroy(): void;
}
interface LoadingControl {
id: string;
success(message?: string, opts?: ToastOptions): ToastInstance | null;
error(message?: string, opts?: ToastOptions): ToastInstance | null;
info(message?: string, opts?: ToastOptions): ToastInstance | null;
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
update(partial: Partial<ToastOptions>): unknown;
dismiss(): void;
}
interface ProgressControl {
id: string;
setProgress(percent: number): void;
complete(message?: string): void;
error(message?: string): void;
dismiss(): void;
}
interface CountdownControl {
id: string;
cancel(): void;
pause(): void;
resume(): void;
}
interface ActionControl {
id: string;
toast: ToastInstance;
dismiss(): void;
}
interface QueueControl {
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
cancel(): void;
}
interface GroupAPI {
_group: string;
show: (...args: unknown[]) => ToastInstance;
success: (...args: unknown[]) => ToastInstance;
error: (...args: unknown[]) => ToastInstance;
warning: (...args: unknown[]) => ToastInstance;
info: (...args: unknown[]) => ToastInstance;
loading: (...args: unknown[]) => LoadingControl;
action: (...args: unknown[]) => ActionControl;
dismiss(): void;
count(): number;
}
interface ActionButton {
text: string;
onClick: (toast: ToastInstance) => void;
color?: string;
style?: Record<string, string>;
close?: boolean;
}
interface PromiseOptions extends ToastOptions {
loading?: string;
success?: string;
error?: string;
}
interface ConfirmOptions extends ToastOptions {
confirmText?: string;
confirmColor?: string;
cancelText?: string;
cancelColor?: string;
}
interface PromptOptions extends ToastOptions {
inputType?: string;
placeholder?: string;
defaultValue?: string;
submitText?: string;
submitColor?: string;
cancelText?: string;
cancelColor?: string;
}
interface ProgressOptions extends ToastOptions {
progressColor?: string;
}
interface CountdownOptions extends ToastOptions {
onComplete?: (() => void) | null;
}
interface QueueOptions extends ToastOptions {
delay?: number;
}
interface StackOptions extends ToastOptions {
stagger?: number;
}
interface InitOptions {
config?: Partial<ToastConfig>;
theme?: ToastTheme;
locale?: string;
plugins?: Array<string | Plugin>;
}
interface StatusInfo {
version: string;
toasts: number;
theme: string;
locale: string;
plugins: string[];
animations: number;
}
interface AnimationConfig {
enter: Record<string, string | number>;
leave: Record<string, string | number>;
duration: number;
easing: string;
name?: string;
delay?: number;
iterations?: number;
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
}
interface AnimationUtils {
register(name: string, config: Partial<AnimationConfig>): void;
unregister(name: string): void;
get(name: string): AnimationConfig | null;
getAnimationNames(): string[];
getActiveCount(): number;
cancelAll(): void;
reset(): void;
destroy(): void;
}
interface ThemeConfig {
bg: string;
text: string;
border: string;
shadow: string;
hoverShadow: string;
progressBg: string;
closeHoverBg: string;
backdropFilter?: string;
}
interface ThemePreview {
name: string;
resolved: string;
colors: {
background: string;
text: string;
border: string;
};
isDark: boolean;
isLight: boolean;
isAuto: boolean;
}
interface ThemeUtils {
getSystemTheme(): 'light' | 'dark';
resolveTheme(theme: string): string;
getThemeConfig(theme: string): ThemeConfig;
applyTheme(theme: string): void;
getCurrentTheme(): string;
getResolvedTheme(): string;
switchTheme(theme: string): void;
toggleTheme(): void;
resetToAuto(): void;
initTheme(): void;
watchSystemTheme(): void;
unwatchSystemTheme(): void;
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
clearThemeListeners(): void;
registerTheme(name: string, config: ThemeConfig): void;
unregisterTheme(name: string): void;
getAllThemes(): Record<string, ThemeConfig>;
getThemeNames(): string[];
hasTheme(name: string): boolean;
getThemePreview(theme: string): ThemePreview;
generateThemeCSS(theme: string): string;
applyThemeCSS(theme: string): void;
removeThemeCSS(): void;
saveTheme(theme: string): void;
loadTheme(): string;
}
interface LocaleInfo {
code: string;
name: string;
direction: 'ltr' | 'rtl';
isSupported: boolean;
isCurrent: boolean;
isFallback: boolean;
}
interface I18nUtils {
t(key: string, params?: Record<string, string | number>): string;
plural(key: string, count: number, params?: Record<string, string | number>): string;
getCurrentLocale(): string;
setCurrentLocale(locale: string): void;
switchLocale(locale: string): void;
getFallbackLocale(): string;
setFallbackLocale(locale: string): void;
hasTranslation(key: string): boolean;
getTranslations(locale: string): Record<string, unknown>;
addTranslations(locale: string, translations: Record<string, unknown>): void;
removeTranslation(locale: string, key: string): void;
clearTranslations(locale: string): void;
getSupportedLocales(): string[];
isLocaleSupported(locale: string): boolean;
getLocaleName(locale: string): string;
getLocaleDirection(locale: string): 'ltr' | 'rtl';
getLocaleInfo(locale: string): LocaleInfo;
getAllLocaleInfo(): LocaleInfo[];
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
formatList(list: string[], options?: Record<string, unknown>): string;
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
addLocaleListener(listener: (locale: string) => void): () => void;
removeLocaleListener(listener: (locale: string) => void): void;
clearLocaleListeners(): void;
initI18n(): void;
saveLocale(locale: string): void;
loadLocale(): string;
getDefaultLocale(): string;
}
interface Plugin {
name: string;
version?: string;
description?: string;
hooks?: Record<string, (...args: unknown[]) => void>;
middleware?: (...args: unknown[]) => unknown;
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
uninstall?: (manager: PluginManager) => void;
init?: (manager: PluginManager) => void;
destroy?: (manager: PluginManager) => void;
installed?: boolean;
enabled?: boolean;
[key: string]: unknown;
}
interface PluginManager {
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
destroy(): void;
}
interface PluginUtils {
createManager(): PluginManager;
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
getPreset(name: string): Plugin | null;
getAllPresets(): Record<string, Plugin>;
createPlugin(config: Partial<Plugin>): Plugin;
validatePlugin(plugin: Partial<Plugin>): {
valid: boolean;
errors: string[];
};
}
interface MeToast {
version: string;
_toasts: Map<string, ToastInstance>;
_config: ToastConfig;
_destroyed?: boolean;
_emit(opts: ToastOptions): ToastInstance;
_remove(id: string): void;
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
_groupCount(name: string): number;
configure(opts: Partial<ToastConfig>): MeToast;
init(options?: InitOptions): MeToast;
destroy(): void;
getStatus(): StatusInfo;
getConfig(): ToastConfig;
updateConfig(config: Partial<ToastConfig>): MeToast;
resetConfig(): MeToast;
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
group(name: string): GroupAPI;
dismissGroup(name: string): void;
find(id: string): ToastInstance | undefined;
dismiss(id?: string): void;
removeToast(id: string): void;
clear(position?: ToastPosition): void;
getToasts(): ToastInstance[];
count(): number;
hasToasts(): boolean;
getToast(id: string): ToastInstance | null;
closeAll(): void;
clearAll(): void;
pauseAll(): void;
resumeAll(): void;
updateAll(partial: Partial<ToastOptions>): void;
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
findByType(type: ToastType): ToastInstance[];
findByPosition(position: ToastPosition): ToastInstance[];
getAll(): Map<string, ToastInstance>;
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
animations: AnimationUtils;
themes: ThemeUtils;
i18n: I18nUtils;
plugins: PluginUtils;
presetPlugins: Record<string, Plugin>;
}
declare global {
interface Window {
MeToast: MeToast;
Met: MeToast;
}
}
/**
* MeToast meToast
*/
declare const enhancedMeToast: {
version: string;
animations: AnimationUtils;
themes: ThemeUtils;
i18n: I18nUtils;
plugins: PluginUtils;
presetPlugins: Record<string, Plugin>;
_toasts: Map<string, ToastInstance>;
_config: ToastConfig;
_destroyed?: boolean;
_emit(opts: ToastOptions): ToastInstance;
_remove(id: string): void;
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
_groupCount(name: string): number;
configure(opts: Partial<ToastConfig>): MeToast;
init(options?: InitOptions): MeToast;
destroy(): void;
getStatus(): StatusInfo;
getConfig(): ToastConfig;
updateConfig(config: Partial<ToastConfig>): MeToast;
resetConfig(): MeToast;
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
group(name: string): GroupAPI;
dismissGroup(name: string): void;
find(id: string): ToastInstance | undefined;
dismiss(id?: string): void;
removeToast(id: string): void;
clear(position?: ToastPosition): void;
getToasts(): ToastInstance[];
count(): number;
hasToasts(): boolean;
getToast(id: string): ToastInstance | null;
closeAll(): void;
clearAll(): void;
pauseAll(): void;
resumeAll(): void;
updateAll(partial: Partial<ToastOptions>): void;
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
findByType(type: ToastType): ToastInstance[];
findByPosition(position: ToastPosition): ToastInstance[];
getAll(): Map<string, ToastInstance>;
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
};
/**
* MetonaToast React React
* @module react
* @version 0.4.0
* @description useToast hook + <Toast /> React optional peerDependency
*/
/** 支持声明式渲染的 Toast 类型 */
type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
message: string;
type?: ReactToastType;
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
autoClose?: boolean;
}
/**
* API Toast
* 便
*/
declare const createBoundApi: (cleanup: Set<string>) => Record<string, unknown>;
/**
* useToast MeToast API Toast
* "通知随组件生命周期"
*/
declare const useToast: () => Record<string, unknown>;
/**
* Toast Toast props
* autoClose true
*/
declare const Toast: (props: ToastProps) => null;
export { enhancedMeToast as MeToast, ReactToastType, Toast, ToastProps, createBoundApi, useToast };
+4577
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long