release: v0.5.0 — 覆盖率96.42%达标、lint零警告、覆盖率门禁、CI强化、发布自动化
- 覆盖率 96.42%(行):新增 59 测试共 385 个,覆盖 DOM 交互事件、对话框按钮交互、Intl/localStorage 异常路径、React 渲染生命周期 - 测试环境重构:SSR 分支用 defineProperty 真正覆盖 document/window;新增 tests/dom.test.ts、tests/setup.ts - jest 覆盖率门禁 lines >= 95%;CI lint 改必过;新增 publish.yml(v* tag 自动发布到 Gitea registry) - 修复:Toast.off 空数组残留、Toast.on 取消函数 this 绑定、action close:false 冒泡关闭、use() 重复注册钩子、i18n catch 引用错误 - 删除死代码 autoApplySystemTheme;lint 零警告(no-console 策略 + caughtErrorsIgnorePattern)
This commit is contained in:
+2
-2
@@ -14,9 +14,9 @@
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended"],
|
||||
"rules": {
|
||||
"no-console": "warn",
|
||||
"no-console": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-undef": "off",
|
||||
|
||||
@@ -31,10 +31,9 @@ jobs:
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
continue-on-error: true
|
||||
|
||||
- name: Run tests
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache
|
||||
- name: Run tests (coverage gate: lines >= 95%)
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: debian-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: https://git.metona.cn/api/packages/MetonaTeam/npm/
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Run tests
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to Gitea npm registry
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
||||
@@ -2,6 +2,28 @@
|
||||
|
||||
All notable changes to MetonaToast will be documented in this file.
|
||||
|
||||
## [0.5.0] - 2026-08-08
|
||||
|
||||
### Changed
|
||||
- **覆盖率 96.42%(行)**:新增 59 个测试用例(共 385),覆盖 DOM 交互事件(click/drag/hover/animation)、confirm/prompt/action 按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
|
||||
- **测试环境重构**:SSR 分支测试用 `Object.defineProperty` 真正覆盖 document/window(jsdom getter 使此前赋值无效);新增 `tests/dom.test.ts`(jsdom 真实 DOM 事件驱动)、`tests/setup.ts`(TextEncoder 补丁)
|
||||
- **jest 覆盖率门禁**:`lines >= 95%`,未达标测试即失败
|
||||
|
||||
### Fixed
|
||||
- **`Toast.off` 空数组残留**:移除全部钩子后删除 Map key(此前空数组残留导致 `_hooks.has()` 误报 true)
|
||||
- **`Toast.on` 返回的取消函数 this 绑定**:改用类名引用,任意调用上下文可用
|
||||
- **action 按钮 `close:false` 失效**:按钮 click 冒泡到 toast 的 closeOnClick 处理器导致总是关闭,已 stopPropagation 修复
|
||||
- **`use()` 重复注册钩子**:accessibility/persistence 预设的钩子注册改为防重复(先卸载旧钩子),并在插件 `uninstall` 中一并清理
|
||||
- **i18n catch 引用错误**:`catch {}` 中残留 `e` 引用导致的 ReferenceError(saveLocale/loadLocale/notifyLocaleListeners)
|
||||
- **删除残缺死代码**:`styles.autoApplySystemTheme`(无实际效果的占位实现)
|
||||
|
||||
### Removed
|
||||
- **lint 零警告**:`no-console` 规则关闭(库的错误报告依赖 console,生产构建已由 terser `drop_console` 剥离);catch 变量通过 `caughtErrorsIgnorePattern` 豁免
|
||||
|
||||
### Added
|
||||
- **Gitea Actions 发布自动化**:`publish.yml` — 推送 `v*` tag 时自动 测试+构建+发布到 Gitea npm registry(需配置 `PACKAGES_TOKEN` secret)
|
||||
- **CI 强化**:lint 改为必过(移除 continue-on-error);测试命令带覆盖率门禁
|
||||
|
||||
## [0.4.0] - 2026-08-08
|
||||
|
||||
### Added
|
||||
|
||||
Vendored
+71
-36
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 生成唯一ID
|
||||
@@ -42,7 +42,7 @@ const prefersDark = () => {
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
const ICONS = {
|
||||
@@ -539,7 +539,7 @@ const ICONS = {
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
const LOCALES = {
|
||||
@@ -1026,12 +1026,12 @@ const LOCALES = {
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
const VERSION = '0.4.0';
|
||||
const VERSION = '0.5.0';
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@@ -1298,7 +1298,7 @@ const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', '
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
@@ -1787,7 +1787,7 @@ const injectStyles = () => {
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
@@ -2131,7 +2131,7 @@ const themeUtils = {
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
@@ -2499,7 +2499,7 @@ const formatNumber = (number, options = {}) => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -2514,7 +2514,7 @@ const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
...options,
|
||||
}).format(amount);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -2530,7 +2530,7 @@ const formatPercent = (value, options = {}) => {
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -2542,7 +2542,7 @@ const formatDate = (date, options = {}) => {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2586,7 +2586,7 @@ const formatRelativeTime = (date, options = {}) => {
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2613,7 +2613,7 @@ const formatPlural = (count, options = {}) => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
@@ -2806,7 +2806,7 @@ const animationUtils = {
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
@@ -2825,12 +2825,19 @@ class Toast {
|
||||
if (!this._hooks.has(name))
|
||||
this._hooks.set(name, []);
|
||||
this._hooks.get(name).push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
static off(name, fn) {
|
||||
const list = this._hooks.get(name);
|
||||
if (list)
|
||||
this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0)
|
||||
this._hooks.delete(name);
|
||||
else
|
||||
this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
@@ -3438,7 +3445,7 @@ const _containerCache = new Map();
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
/**
|
||||
@@ -3510,7 +3517,7 @@ const actionHTML = (actions) => {
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -3665,6 +3672,12 @@ const presetPlugins = {
|
||||
}
|
||||
catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
meta._saveOff();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
@@ -3687,6 +3700,14 @@ const presetPlugins = {
|
||||
setTimeout(() => { if (el.parentNode)
|
||||
el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
uninstall() {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this;
|
||||
if (typeof meta._off === 'function') {
|
||||
meta._off();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
@@ -3756,7 +3777,7 @@ const defaultPluginManager = new PluginManager();
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 参数标准化工具
|
||||
@@ -4092,12 +4113,14 @@ const meToast = {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try {
|
||||
a.onClick(toast);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Action onClick error:', e);
|
||||
catch (err) {
|
||||
console.error('Action onClick error:', err);
|
||||
}
|
||||
if (a.close !== false)
|
||||
toast.close();
|
||||
@@ -4334,25 +4357,37 @@ const meToast = {
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin);
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast) => {
|
||||
const acc = preset;
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') {
|
||||
stored._off();
|
||||
}
|
||||
const acc = preset;
|
||||
if (stored) {
|
||||
stored._off = Toast.on('afterShow', (toast) => {
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') {
|
||||
stored._saveOff();
|
||||
}
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins)
|
||||
: null;
|
||||
if (saved)
|
||||
this.configure(saved);
|
||||
Toast.on('afterClose', () => {
|
||||
const p = preset;
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
const p = preset;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast.on('afterClose', () => {
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (plugin && typeof plugin === 'object') {
|
||||
@@ -4428,7 +4463,7 @@ Toast.setCallbacks({
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+4
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.4.0
|
||||
* @version 0.5.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';
|
||||
@@ -418,7 +418,7 @@ declare global {
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -492,13 +492,13 @@ declare class Toast implements ToastInstance {
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
declare const VERSION = "0.4.0";
|
||||
declare const VERSION = "0.5.0";
|
||||
|
||||
/**
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
|
||||
Vendored
+71
-36
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 生成唯一ID
|
||||
@@ -38,7 +38,7 @@ const prefersDark = () => {
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
const ICONS = {
|
||||
@@ -535,7 +535,7 @@ const ICONS = {
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
const LOCALES = {
|
||||
@@ -1022,12 +1022,12 @@ const LOCALES = {
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
const VERSION = '0.4.0';
|
||||
const VERSION = '0.5.0';
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@@ -1294,7 +1294,7 @@ const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', '
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
@@ -1783,7 +1783,7 @@ const injectStyles = () => {
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
@@ -2127,7 +2127,7 @@ const themeUtils = {
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
@@ -2495,7 +2495,7 @@ const formatNumber = (number, options = {}) => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -2510,7 +2510,7 @@ const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
...options,
|
||||
}).format(amount);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -2526,7 +2526,7 @@ const formatPercent = (value, options = {}) => {
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -2538,7 +2538,7 @@ const formatDate = (date, options = {}) => {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2582,7 +2582,7 @@ const formatRelativeTime = (date, options = {}) => {
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2609,7 +2609,7 @@ const formatPlural = (count, options = {}) => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
@@ -2802,7 +2802,7 @@ const animationUtils = {
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
@@ -2821,12 +2821,19 @@ class Toast {
|
||||
if (!this._hooks.has(name))
|
||||
this._hooks.set(name, []);
|
||||
this._hooks.get(name).push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
static off(name, fn) {
|
||||
const list = this._hooks.get(name);
|
||||
if (list)
|
||||
this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0)
|
||||
this._hooks.delete(name);
|
||||
else
|
||||
this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
@@ -3434,7 +3441,7 @@ const _containerCache = new Map();
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
/**
|
||||
@@ -3506,7 +3513,7 @@ const actionHTML = (actions) => {
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -3661,6 +3668,12 @@ const presetPlugins = {
|
||||
}
|
||||
catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
meta._saveOff();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
@@ -3683,6 +3696,14 @@ const presetPlugins = {
|
||||
setTimeout(() => { if (el.parentNode)
|
||||
el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
uninstall() {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this;
|
||||
if (typeof meta._off === 'function') {
|
||||
meta._off();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
@@ -3752,7 +3773,7 @@ const defaultPluginManager = new PluginManager();
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 参数标准化工具
|
||||
@@ -4088,12 +4109,14 @@ const meToast = {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try {
|
||||
a.onClick(toast);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Action onClick error:', e);
|
||||
catch (err) {
|
||||
console.error('Action onClick error:', err);
|
||||
}
|
||||
if (a.close !== false)
|
||||
toast.close();
|
||||
@@ -4330,25 +4353,37 @@ const meToast = {
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin);
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast) => {
|
||||
const acc = preset;
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') {
|
||||
stored._off();
|
||||
}
|
||||
const acc = preset;
|
||||
if (stored) {
|
||||
stored._off = Toast.on('afterShow', (toast) => {
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') {
|
||||
stored._saveOff();
|
||||
}
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins)
|
||||
: null;
|
||||
if (saved)
|
||||
this.configure(saved);
|
||||
Toast.on('afterClose', () => {
|
||||
const p = preset;
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
const p = preset;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast.on('afterClose', () => {
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (plugin && typeof plugin === 'object') {
|
||||
@@ -4424,7 +4459,7 @@ Toast.setCallbacks({
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+71
-36
@@ -7,7 +7,7 @@
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 生成唯一ID
|
||||
@@ -44,7 +44,7 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
const ICONS = {
|
||||
@@ -541,7 +541,7 @@
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
const LOCALES = {
|
||||
@@ -1028,12 +1028,12 @@
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
const VERSION = '0.4.0';
|
||||
const VERSION = '0.5.0';
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@@ -1300,7 +1300,7 @@
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
@@ -1789,7 +1789,7 @@
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
@@ -2133,7 +2133,7 @@
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
@@ -2501,7 +2501,7 @@
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -2516,7 +2516,7 @@
|
||||
...options,
|
||||
}).format(amount);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -2532,7 +2532,7 @@
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -2544,7 +2544,7 @@
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2588,7 +2588,7 @@
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2615,7 +2615,7 @@
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
@@ -2808,7 +2808,7 @@
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
@@ -2827,12 +2827,19 @@
|
||||
if (!this._hooks.has(name))
|
||||
this._hooks.set(name, []);
|
||||
this._hooks.get(name).push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
static off(name, fn) {
|
||||
const list = this._hooks.get(name);
|
||||
if (list)
|
||||
this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0)
|
||||
this._hooks.delete(name);
|
||||
else
|
||||
this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
@@ -3440,7 +3447,7 @@
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
/**
|
||||
@@ -3512,7 +3519,7 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -3667,6 +3674,12 @@
|
||||
}
|
||||
catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
meta._saveOff();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
@@ -3689,6 +3702,14 @@
|
||||
setTimeout(() => { if (el.parentNode)
|
||||
el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
uninstall() {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this;
|
||||
if (typeof meta._off === 'function') {
|
||||
meta._off();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
@@ -3758,7 +3779,7 @@
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 参数标准化工具
|
||||
@@ -4094,12 +4115,14 @@
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try {
|
||||
a.onClick(toast);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Action onClick error:', e);
|
||||
catch (err) {
|
||||
console.error('Action onClick error:', err);
|
||||
}
|
||||
if (a.close !== false)
|
||||
toast.close();
|
||||
@@ -4336,25 +4359,37 @@
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin);
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast) => {
|
||||
const acc = preset;
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') {
|
||||
stored._off();
|
||||
}
|
||||
const acc = preset;
|
||||
if (stored) {
|
||||
stored._off = Toast.on('afterShow', (toast) => {
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') {
|
||||
stored._saveOff();
|
||||
}
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins)
|
||||
: null;
|
||||
if (saved)
|
||||
this.configure(saved);
|
||||
Toast.on('afterClose', () => {
|
||||
const p = preset;
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
const p = preset;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast.on('afterClose', () => {
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (plugin && typeof plugin === 'object') {
|
||||
@@ -4430,7 +4465,7 @@
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+72
-37
@@ -5,7 +5,7 @@ var react = require('react');
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 生成唯一ID
|
||||
@@ -42,7 +42,7 @@ const prefersDark = () => {
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
const ICONS = {
|
||||
@@ -539,7 +539,7 @@ const ICONS = {
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
const LOCALES = {
|
||||
@@ -1026,12 +1026,12 @@ const LOCALES = {
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
const VERSION = '0.4.0';
|
||||
const VERSION = '0.5.0';
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@@ -1298,7 +1298,7 @@ const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', '
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
@@ -1787,7 +1787,7 @@ const injectStyles = () => {
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
@@ -2131,7 +2131,7 @@ const themeUtils = {
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
@@ -2499,7 +2499,7 @@ const formatNumber = (number, options = {}) => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -2514,7 +2514,7 @@ const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
...options,
|
||||
}).format(amount);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -2530,7 +2530,7 @@ const formatPercent = (value, options = {}) => {
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -2542,7 +2542,7 @@ const formatDate = (date, options = {}) => {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2586,7 +2586,7 @@ const formatRelativeTime = (date, options = {}) => {
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2613,7 +2613,7 @@ const formatPlural = (count, options = {}) => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
@@ -2806,7 +2806,7 @@ const animationUtils = {
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
@@ -2825,12 +2825,19 @@ let Toast$1 = class Toast {
|
||||
if (!this._hooks.has(name))
|
||||
this._hooks.set(name, []);
|
||||
this._hooks.get(name).push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
static off(name, fn) {
|
||||
const list = this._hooks.get(name);
|
||||
if (list)
|
||||
this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0)
|
||||
this._hooks.delete(name);
|
||||
else
|
||||
this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
@@ -3438,7 +3445,7 @@ const _containerCache = new Map();
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
/**
|
||||
@@ -3510,7 +3517,7 @@ const actionHTML = (actions) => {
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -3665,6 +3672,12 @@ const presetPlugins = {
|
||||
}
|
||||
catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
meta._saveOff();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
@@ -3687,6 +3700,14 @@ const presetPlugins = {
|
||||
setTimeout(() => { if (el.parentNode)
|
||||
el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
uninstall() {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this;
|
||||
if (typeof meta._off === 'function') {
|
||||
meta._off();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
@@ -3756,7 +3777,7 @@ const defaultPluginManager = new PluginManager();
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 参数标准化工具
|
||||
@@ -4092,12 +4113,14 @@ const meToast = {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try {
|
||||
a.onClick(toast);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Action onClick error:', e);
|
||||
catch (err) {
|
||||
console.error('Action onClick error:', err);
|
||||
}
|
||||
if (a.close !== false)
|
||||
toast.close();
|
||||
@@ -4334,25 +4357,37 @@ const meToast = {
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin);
|
||||
if (plugin === 'accessibility') {
|
||||
Toast$1.on('afterShow', (toast) => {
|
||||
const acc = preset;
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') {
|
||||
stored._off();
|
||||
}
|
||||
const acc = preset;
|
||||
if (stored) {
|
||||
stored._off = Toast$1.on('afterShow', (toast) => {
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') {
|
||||
stored._saveOff();
|
||||
}
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins)
|
||||
: null;
|
||||
if (saved)
|
||||
this.configure(saved);
|
||||
Toast$1.on('afterClose', () => {
|
||||
const p = preset;
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
const p = preset;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast$1.on('afterClose', () => {
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (plugin && typeof plugin === 'object') {
|
||||
@@ -4428,7 +4463,7 @@ Toast$1.setCallbacks({
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
@@ -4494,7 +4529,7 @@ if (typeof window !== 'undefined') {
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'];
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+2
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.4.0
|
||||
* @version 0.5.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';
|
||||
@@ -478,7 +478,7 @@ declare const enhancedMeToast: {
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
|
||||
Vendored
+72
-37
@@ -3,7 +3,7 @@ import { useRef, useEffect, useMemo } from 'react';
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 生成唯一ID
|
||||
@@ -40,7 +40,7 @@ const prefersDark = () => {
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
const ICONS = {
|
||||
@@ -537,7 +537,7 @@ const ICONS = {
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
const LOCALES = {
|
||||
@@ -1024,12 +1024,12 @@ const LOCALES = {
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
const VERSION = '0.4.0';
|
||||
const VERSION = '0.5.0';
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
@@ -1296,7 +1296,7 @@ const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', '
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
@@ -1785,7 +1785,7 @@ const injectStyles = () => {
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
@@ -2129,7 +2129,7 @@ const themeUtils = {
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
@@ -2497,7 +2497,7 @@ const formatNumber = (number, options = {}) => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -2512,7 +2512,7 @@ const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
...options,
|
||||
}).format(amount);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -2528,7 +2528,7 @@ const formatPercent = (value, options = {}) => {
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -2540,7 +2540,7 @@ const formatDate = (date, options = {}) => {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2584,7 +2584,7 @@ const formatRelativeTime = (date, options = {}) => {
|
||||
}
|
||||
return String(date);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -2611,7 +2611,7 @@ const formatPlural = (count, options = {}) => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
}
|
||||
catch (e) {
|
||||
catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
@@ -2804,7 +2804,7 @@ const animationUtils = {
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
@@ -2823,12 +2823,19 @@ let Toast$1 = class Toast {
|
||||
if (!this._hooks.has(name))
|
||||
this._hooks.set(name, []);
|
||||
this._hooks.get(name).push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
static off(name, fn) {
|
||||
const list = this._hooks.get(name);
|
||||
if (list)
|
||||
this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0)
|
||||
this._hooks.delete(name);
|
||||
else
|
||||
this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
@@ -3436,7 +3443,7 @@ const _containerCache = new Map();
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
/**
|
||||
@@ -3508,7 +3515,7 @@ const actionHTML = (actions) => {
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 插件管理器
|
||||
@@ -3663,6 +3670,12 @@ const presetPlugins = {
|
||||
}
|
||||
catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
meta._saveOff();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
@@ -3685,6 +3698,14 @@ const presetPlugins = {
|
||||
setTimeout(() => { if (el.parentNode)
|
||||
el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
uninstall() {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this;
|
||||
if (typeof meta._off === 'function') {
|
||||
meta._off();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
@@ -3754,7 +3775,7 @@ const defaultPluginManager = new PluginManager();
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/**
|
||||
* 参数标准化工具
|
||||
@@ -4090,12 +4111,14 @@ const meToast = {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try {
|
||||
a.onClick(toast);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Action onClick error:', e);
|
||||
catch (err) {
|
||||
console.error('Action onClick error:', err);
|
||||
}
|
||||
if (a.close !== false)
|
||||
toast.close();
|
||||
@@ -4332,25 +4355,37 @@ const meToast = {
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin);
|
||||
if (plugin === 'accessibility') {
|
||||
Toast$1.on('afterShow', (toast) => {
|
||||
const acc = preset;
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') {
|
||||
stored._off();
|
||||
}
|
||||
const acc = preset;
|
||||
if (stored) {
|
||||
stored._off = Toast$1.on('afterShow', (toast) => {
|
||||
if (typeof acc.announce === 'function')
|
||||
acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') {
|
||||
stored._saveOff();
|
||||
}
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins)
|
||||
: null;
|
||||
if (saved)
|
||||
this.configure(saved);
|
||||
Toast$1.on('afterClose', () => {
|
||||
const p = preset;
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
const p = preset;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast$1.on('afterClose', () => {
|
||||
if (typeof p.save === 'function')
|
||||
p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (plugin && typeof plugin === 'object') {
|
||||
@@ -4426,7 +4461,7 @@ Toast$1.setCallbacks({
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
@@ -4492,7 +4527,7 @@ if (typeof window !== 'undefined') {
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'];
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -18,5 +18,13 @@ module.exports = {
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 85,
|
||||
branches: 65,
|
||||
functions: 85,
|
||||
lines: 95,
|
||||
},
|
||||
},
|
||||
verbose: true,
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
||||
"type": "module",
|
||||
"main": "dist/metona-toast.cjs.js",
|
||||
|
||||
+2
-2
@@ -244,7 +244,7 @@
|
||||
|
||||
<!-- 14. 错误回调 -->
|
||||
<div class="card">
|
||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.4.0</span></div>
|
||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.5.0</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
|
||||
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
|
||||
@@ -282,7 +282,7 @@
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
MetonaToast v0.4.0 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
MetonaToast v0.5.0 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@
|
||||
<main class="main">
|
||||
|
||||
<h1>API 文档</h1>
|
||||
<p>MetonaToast v0.4.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||
<p>MetonaToast v0.5.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||
|
||||
<!-- ===== 基础通知 ===== -->
|
||||
<h2 id="show">show(message, opts?)</h2>
|
||||
|
||||
+1
-1
@@ -247,7 +247,7 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>MetonaToast v0.4.0 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||
<p>MetonaToast v0.5.0 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
|
||||
+22
-13
@@ -1,12 +1,11 @@
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
import { DEFAULTS, VERSION } from './constants.js';
|
||||
import { escapeHTML } from './utils.js';
|
||||
import { t, setCurrentLocale } from './i18n.js';
|
||||
import { applyTheme } from './themes.js';
|
||||
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
||||
@@ -395,8 +394,10 @@ const meToast: MeToast = {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', () => {
|
||||
try { a.onClick(toast); } catch (e) { console.error('Action onClick error:', e); }
|
||||
btn.addEventListener('click', (e: Event) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try { a.onClick(toast); } catch (err) { console.error('Action onClick error:', err); }
|
||||
if (a.close !== false) toast.close();
|
||||
});
|
||||
}
|
||||
@@ -658,22 +659,30 @@ const meToast: MeToast = {
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
|
||||
// 连接插件钩子
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin) as (Record<string, unknown> & Plugin) | null;
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
||||
if (typeof acc.announce === 'function') acc.announce(toast);
|
||||
});
|
||||
if (stored && typeof stored._off === 'function') { (stored._off as () => void)(); }
|
||||
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
||||
if (stored) {
|
||||
stored._off = Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
if (typeof acc.announce === 'function') acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') { (stored._saveOff as () => void)(); }
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins as unknown as import('./types.js').PluginManager)
|
||||
: null;
|
||||
if (saved) this.configure(saved as Partial<ToastConfig>);
|
||||
Toast.on('afterClose', () => {
|
||||
const p = preset as Record<string, (c: unknown) => void>;
|
||||
if (typeof p.save === 'function') p.save(this.getConfig());
|
||||
});
|
||||
const p = preset as Record<string, (c: unknown) => void>;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast.on('afterClose', () => {
|
||||
if (typeof p.save === 'function') p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (plugin && typeof plugin === 'object') {
|
||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { ICONS } from './icons.js';
|
||||
@@ -12,7 +12,7 @@ export { ICONS, LOCALES };
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
export const VERSION = '0.4.0';
|
||||
export const VERSION = '0.5.0';
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
|
||||
+7
-7
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { LOCALES } from './constants.js';
|
||||
@@ -405,7 +405,7 @@ export const clearLocaleListeners = (): void => {
|
||||
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
@@ -420,7 +420,7 @@ export const formatCurrency = (amount: number, currency = 'USD', options: Intl.N
|
||||
currency,
|
||||
...options,
|
||||
}).format(amount);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
@@ -436,7 +436,7 @@ export const formatPercent = (value: number, options: Intl.NumberFormatOptions =
|
||||
maximumFractionDigits: 2,
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
@@ -448,7 +448,7 @@ export const formatDate = (date: Date | number | string, options: Intl.DateTimeF
|
||||
try {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -497,7 +497,7 @@ export const formatRelativeTime = (date: Date | number | string, options: Intl.R
|
||||
}
|
||||
|
||||
return String(date);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
@@ -524,7 +524,7 @@ export const formatList = (list: string[], _options: Record<string, unknown> = {
|
||||
export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
|
||||
|
||||
+16
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
@@ -154,6 +154,12 @@ const presetPlugins: Record<string, Plugin> = {
|
||||
localStorage.removeItem(key);
|
||||
} catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this as Record<string, unknown>;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
(meta._saveOff as () => void)();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -176,6 +182,15 @@ const presetPlugins: Record<string, Plugin> = {
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this as Record<string, unknown>;
|
||||
if (typeof meta._off === 'function') {
|
||||
(meta._off as () => void)();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
|
||||
+1
-16
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import type { ThemeConfig } from './types.js';
|
||||
@@ -595,18 +595,3 @@ export const watchSystemTheme = (callback: (theme: string) => void): (() => void
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 自动应用系统主题
|
||||
*/
|
||||
export const autoApplySystemTheme = (): (() => void) => {
|
||||
const applyTheme = (theme: string): void => {
|
||||
updateStyles();
|
||||
// _THEMES reference is from constants, but here we just need to use theme string
|
||||
// applyThemeVariables is called externally
|
||||
};
|
||||
|
||||
applyTheme(getSystemTheme());
|
||||
|
||||
return watchSystemTheme(applyTheme);
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
|
||||
+9
-3
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { generateId, escapeHTML } from './utils.js';
|
||||
@@ -40,12 +40,18 @@ export class Toast implements ToastInstance {
|
||||
static on(name: string, fn: (toast: Toast) => boolean | void): () => void {
|
||||
if (!this._hooks.has(name)) this._hooks.set(name, []);
|
||||
this._hooks.get(name)!.push(fn);
|
||||
return () => this.off(name, fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
|
||||
static off(name: string, fn: (toast: Toast) => boolean | void): void {
|
||||
const list = this._hooks.get(name);
|
||||
if (list) this._hooks.set(name, list.filter(f => f !== fn));
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0) this._hooks.delete(name);
|
||||
else this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
// ========== 基础类型 ==========
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
+187
-14
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 覆盖率补充测试 — 目标 95%+
|
||||
* @module tests
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast, VERSION } from '../src/index';
|
||||
@@ -809,7 +809,7 @@ describe('api.ts 覆盖率', () => {
|
||||
|
||||
test('getStatus 返回完整状态', () => {
|
||||
const status = MeToast.getStatus();
|
||||
expect(status.version).toBe('0.4.0');
|
||||
expect(status.version).toBe('0.5.0');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
@@ -1080,12 +1080,6 @@ describe('styles.ts 覆盖率', () => {
|
||||
expect(typeof unsub).toBe('function');
|
||||
unsub();
|
||||
});
|
||||
|
||||
test('autoApplySystemTheme 返回取消函数', () => {
|
||||
const unsub = styles.autoApplySystemTheme();
|
||||
expect(typeof unsub).toBe('function');
|
||||
unsub();
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
@@ -1261,12 +1255,6 @@ describe('最终补漏', () => {
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
});
|
||||
|
||||
// === styles.ts 剩余 ===
|
||||
test('autoApplySystemTheme 内部逻辑不抛异常', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
expect(() => styles.autoApplySystemTheme()).not.toThrow();
|
||||
});
|
||||
|
||||
// === api.ts prompt 流程 ===
|
||||
test('prompt 创建后按钮事件绑定', async () => {
|
||||
const promise = MeToast.prompt('name?', {
|
||||
@@ -1568,3 +1556,188 @@ describe('v0.4.0 能力增强', () => {
|
||||
expect(MeToast.count()).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
// ==================================================================
|
||||
// 13. v0.5.0 异常路径与边界覆盖
|
||||
// ==================================================================
|
||||
describe('v0.5.0 异常路径与边界', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('i18n.ts 异常与 fallback', () => {
|
||||
const i18n = require('../src/i18n.js');
|
||||
|
||||
test('t fallback 到 fallbackLocale', () => {
|
||||
i18n.setFallbackLocale('en-US');
|
||||
i18n.removeTranslation('zh-CN', 'retry');
|
||||
expect(i18n.t('retry')).toBe('Retry');
|
||||
i18n.addTranslations('zh-CN', { retry: '重试' });
|
||||
i18n.setFallbackLocale('zh-CN');
|
||||
});
|
||||
|
||||
test('removeTranslation 中间层级缺失提前返回', () => {
|
||||
i18n.addTranslations('ja', { a: { b: 'x' } });
|
||||
expect(() => i18n.removeTranslation('ja', 'a.nonexistent.c')).not.toThrow();
|
||||
});
|
||||
|
||||
test('saveLocale localStorage 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(() => i18n.saveLocale('en-US')).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回默认', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(typeof i18n.loadLocale()).toBe('string');
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('getDefaultLocale 不支持浏览器语言时回退', () => {
|
||||
const orig = Object.getOwnPropertyDescriptor(navigator, 'language');
|
||||
Object.defineProperty(navigator, 'language', { value: 'zz-ZZ', configurable: true });
|
||||
expect(typeof i18n.getDefaultLocale()).toBe('string');
|
||||
if (orig) Object.defineProperty(navigator, 'language', orig);
|
||||
});
|
||||
|
||||
test('locale listener 异常不中断其他监听器', () => {
|
||||
const bad = () => { throw new Error('listener-err'); };
|
||||
const good = jest.fn();
|
||||
i18n.addLocaleListener(bad);
|
||||
i18n.addLocaleListener(good);
|
||||
expect(() => i18n.switchLocale('en-US')).not.toThrow();
|
||||
expect(good).toHaveBeenCalled();
|
||||
i18n.removeLocaleListener(bad);
|
||||
i18n.removeLocaleListener(good);
|
||||
i18n.switchLocale('zh-CN');
|
||||
});
|
||||
|
||||
test('Intl 异常时 format 系列降级返回原始值', () => {
|
||||
const origIntl = (global as Record<string, unknown>).Intl;
|
||||
(global as Record<string, unknown>).Intl = new Proxy(origIntl as object, {
|
||||
get: (t: Record<string, unknown>, p: string) => {
|
||||
if (['NumberFormat', 'DateTimeFormat', 'RelativeTimeFormat', 'PluralRules'].includes(p)) {
|
||||
return class { constructor() { throw new RangeError('invalid'); } };
|
||||
}
|
||||
return (t as Record<string, unknown>)[p];
|
||||
},
|
||||
});
|
||||
expect(i18n.formatNumber(1)).toBe('1');
|
||||
expect(i18n.formatCurrency(1)).toBe('1');
|
||||
expect(i18n.formatPercent(50)).toBe('50%');
|
||||
expect(i18n.formatDate('2026-01-01')).toBe('2026-01-01');
|
||||
expect(typeof i18n.formatRelativeTime(Date.now())).toBe('string');
|
||||
expect(i18n.formatPlural(5)).toBe('other');
|
||||
(global as Record<string, unknown>).Intl = origIntl;
|
||||
});
|
||||
|
||||
test('formatList Intl.ListFormat 缺失时逗号拼接', () => {
|
||||
const origLF = (Intl as unknown as Record<string, unknown>).ListFormat;
|
||||
(Intl as unknown as Record<string, unknown>).ListFormat = undefined;
|
||||
const result = i18n.formatList(['a', 'b']);
|
||||
expect(result).toBe('a, b');
|
||||
(Intl as unknown as Record<string, unknown>).ListFormat = origLF;
|
||||
});
|
||||
|
||||
test('plural 缺失时返回原 key', () => {
|
||||
expect(i18n.plural('missing_plural_key_xyz', 5)).toBe('missing_plural_key_xyz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('themes.ts 异常与监听', () => {
|
||||
const themes = require('../src/themes.js');
|
||||
|
||||
test('saveTheme/loadTheme localStorage 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(() => themes.saveTheme('dark')).not.toThrow();
|
||||
spy.mockRestore();
|
||||
const spy2 = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
expect(themes.loadTheme()).toBe('auto');
|
||||
spy2.mockRestore();
|
||||
});
|
||||
|
||||
test('watchSystemTheme 重复调用先移除旧监听', () => {
|
||||
expect(() => { themes.watchSystemTheme(); themes.watchSystemTheme(); }).not.toThrow();
|
||||
themes.unwatchSystemTheme();
|
||||
});
|
||||
|
||||
test('unwatchSystemTheme 清理监听', () => {
|
||||
themes.watchSystemTheme();
|
||||
expect(() => themes.unwatchSystemTheme()).not.toThrow();
|
||||
});
|
||||
|
||||
test('theme listener 异常不中断其他监听器', () => {
|
||||
const bad = () => { throw new Error('theme-listener-err'); };
|
||||
const good = jest.fn();
|
||||
themes.addThemeListener(bad);
|
||||
themes.addThemeListener(good);
|
||||
expect(() => themes.switchTheme('dark')).not.toThrow();
|
||||
expect(good).toHaveBeenCalled();
|
||||
themes.removeThemeListener(bad);
|
||||
themes.removeThemeListener(good);
|
||||
themes.switchTheme('auto');
|
||||
});
|
||||
|
||||
test('removeThemeCSS 移除已存在样式', () => {
|
||||
themes.applyThemeCSS('dark');
|
||||
expect(() => themes.removeThemeCSS()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('styles.ts 边界', () => {
|
||||
const styles = require('../src/styles.js');
|
||||
|
||||
test('applyThemeVariables 先移除旧样式再追加', () => {
|
||||
const config = { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' };
|
||||
styles.applyThemeVariables(config);
|
||||
styles.applyThemeVariables(config);
|
||||
const els = document.querySelectorAll('#metona-toast-custom-styles');
|
||||
expect(els.length).toBeLessThanOrEqual(1);
|
||||
styles.clearThemeVariables();
|
||||
});
|
||||
|
||||
test('watchSystemTheme 无 matchMedia 时返回空函数', () => {
|
||||
const orig = (window as unknown as Record<string, unknown>).matchMedia;
|
||||
(window as unknown as Record<string, unknown>).matchMedia = undefined;
|
||||
const unsub = styles.watchSystemTheme(jest.fn());
|
||||
expect(unsub).toBeInstanceOf(Function);
|
||||
(window as unknown as Record<string, unknown>).matchMedia = orig;
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.ts use 预设插件', () => {
|
||||
test('use persistence 从 localStorage 恢复配置', () => {
|
||||
localStorage.setItem('metona-toast-config', JSON.stringify({ duration: 3000 }));
|
||||
MeToast.use('persistence');
|
||||
expect(MeToast.getConfig().duration).toBe(3000);
|
||||
MeToast.plugins.unregister('persistence');
|
||||
localStorage.removeItem('metona-toast-config');
|
||||
MeToast.resetConfig();
|
||||
});
|
||||
|
||||
test('use accessibility 注册朗读钩子', () => {
|
||||
const announceSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||
MeToast.use('accessibility');
|
||||
MeToast.success('announce-test');
|
||||
MeToast.plugins.unregister('accessibility');
|
||||
announceSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('重复 use persistence 不重复注册保存钩子', () => {
|
||||
localStorage.removeItem('metona-toast-config');
|
||||
MeToast.use('persistence');
|
||||
MeToast.use('persistence');
|
||||
MeToast.plugins.unregister('persistence');
|
||||
expect(Toast._hooks.has('afterClose')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,369 @@
|
||||
/**
|
||||
* MetonaToast DOM 交互测试 — 基于 jsdom 真实 DOM 事件驱动
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast } from '../src/index';
|
||||
import type { ToastInstance } from '../src/types';
|
||||
|
||||
// jsdom polyfill: PointerEvent 相关方法
|
||||
if (typeof Element !== 'undefined') {
|
||||
if (!(Element.prototype as unknown as { setPointerCapture?: unknown }).setPointerCapture) {
|
||||
(Element.prototype as unknown as Record<string, unknown>).setPointerCapture = jest.fn();
|
||||
}
|
||||
if (!(Element.prototype as unknown as { releasePointerCapture?: unknown }).releasePointerCapture) {
|
||||
(Element.prototype as unknown as Record<string, unknown>).releasePointerCapture = jest.fn();
|
||||
}
|
||||
}
|
||||
|
||||
const pointer = (type: string, x: number, y: number): PointerEvent =>
|
||||
new MouseEvent(type, { clientX: x, clientY: y, bubbles: true }) as unknown as PointerEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('toast.ts — DOM 交互', () => {
|
||||
test('onShow 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.success('x', { onShow: () => { throw new Error('show-err'); } });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('onUpdate 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const t = MeToast.success('x', { onUpdate: () => { throw new Error('upd-err'); } });
|
||||
t.update({ message: 'y' });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('_applyStyles 字符串 width 生效', () => {
|
||||
const t = MeToast.info({ message: 'w', width: '80%' });
|
||||
expect(t.el!.style.width).toBe('80%');
|
||||
});
|
||||
|
||||
test('点击 close 按钮关闭 toast 且不触发 click 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('click', fn);
|
||||
const t = MeToast.info('test');
|
||||
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||
closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(t.closing).toBe(true);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
Toast.off('click', fn);
|
||||
});
|
||||
|
||||
test('点击主体触发 onClick 并关闭', () => {
|
||||
const onClick = jest.fn();
|
||||
const t = MeToast.info('test', { onClick });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(onClick).toHaveBeenCalledWith(t);
|
||||
expect(t.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('onClick 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const t = MeToast.info('test', { onClick: () => { throw new Error('click-err'); } });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('closeOnClick false 时点击不关闭但触发 click 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('click', fn);
|
||||
const t = MeToast.info('test', { closeOnClick: false });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(t.closing).toBe(false);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
Toast.off('click', fn);
|
||||
});
|
||||
|
||||
test('hover 触发暂停/恢复与 hover 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('hover', fn);
|
||||
const t = MeToast.info({ message: 'h', duration: 5000 });
|
||||
t.el!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||
expect(t.paused).toBe(true);
|
||||
t.el!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
||||
expect(t.paused).toBe(false);
|
||||
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
Toast.off('hover', fn);
|
||||
});
|
||||
|
||||
test('animationstart/animationend 触发钩子', () => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('animationStart', start);
|
||||
Toast.on('animationEnd', end);
|
||||
const t = MeToast.info('a');
|
||||
t.el!.dispatchEvent(new Event('animationstart'));
|
||||
t.el!.dispatchEvent(new Event('animationend'));
|
||||
expect(start).toHaveBeenCalled();
|
||||
expect(end).toHaveBeenCalled();
|
||||
Toast.off('animationStart', start);
|
||||
Toast.off('animationEnd', end);
|
||||
});
|
||||
|
||||
test('拖拽 down→move→up 超过阈值触发关闭', (done) => {
|
||||
const t = MeToast.info('drag', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||
el.dispatchEvent(pointer('pointermove', 250, 60));
|
||||
expect(el.style.transform).toContain('translate(150px');
|
||||
expect(el.style.opacity).toBe('0.25');
|
||||
el.dispatchEvent(pointer('pointerup', 250, 60));
|
||||
expect(el.style.opacity).toBe('0');
|
||||
setTimeout(() => {
|
||||
expect(t.closing).toBe(true);
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
test('拖拽未超阈值松手恢复', () => {
|
||||
const t = MeToast.info('drag2', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||
el.dispatchEvent(pointer('pointermove', 150, 60));
|
||||
el.dispatchEvent(pointer('pointerup', 150, 60));
|
||||
expect(t.closing).toBe(false);
|
||||
expect(t.paused).toBe(false);
|
||||
expect(el.style.transform).toBe('');
|
||||
});
|
||||
|
||||
test('拖拽触发 dragStart/dragEnd 钩子', () => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('dragStart', start);
|
||||
Toast.on('dragEnd', end);
|
||||
const t = MeToast.info('d', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||
el.dispatchEvent(pointer('pointerup', 0, 0));
|
||||
expect(start).toHaveBeenCalled();
|
||||
expect(end).toHaveBeenCalled();
|
||||
Toast.off('dragStart', start);
|
||||
Toast.off('dragEnd', end);
|
||||
});
|
||||
|
||||
test('pointercancel 等价于 pointerup', () => {
|
||||
const t = MeToast.info('pc', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 10, 10));
|
||||
el.dispatchEvent(pointer('pointercancel', 10, 10));
|
||||
expect(t.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('拖拽从 close 按钮按下时早退', () => {
|
||||
const t = MeToast.info('dc', { duration: 5000 });
|
||||
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||
closeBtn.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||
expect(t.paused).toBe(false);
|
||||
});
|
||||
|
||||
test('close 非 immediate 走离场动画并清理', (done) => {
|
||||
const t = MeToast.info('anim-close');
|
||||
t.close();
|
||||
expect(t.el!.classList.contains('met-leaving')).toBe(true);
|
||||
setTimeout(() => {
|
||||
expect(t.el).toBeNull();
|
||||
expect(MeToast.count()).toBe(0);
|
||||
done();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
test('updatePosition 移动到新容器', () => {
|
||||
const t = MeToast.info('move', { position: 'top-left' });
|
||||
t.updatePosition('bottom-right');
|
||||
expect(t.config.position).toBe('bottom-right');
|
||||
expect(t.el!.parentElement!.className).toContain('bottom-right');
|
||||
});
|
||||
|
||||
test('updatePosition 同位置直接返回', () => {
|
||||
const t = MeToast.info('same');
|
||||
const container = t.el!.parentElement;
|
||||
t.updatePosition('top-right');
|
||||
expect(t.el!.parentElement).toBe(container);
|
||||
});
|
||||
|
||||
test('_limitToasts registry 无实例时走 _removeToast 兜底', () => {
|
||||
const el = document.createElement('div');
|
||||
const ghost = document.createElement('div');
|
||||
ghost.dataset.id = 'ghost-id';
|
||||
(el.querySelectorAll as unknown) = () => [ghost];
|
||||
const t = new Toast({ message: 'x', max: 0 });
|
||||
expect(() => t._limitToasts(el)).not.toThrow();
|
||||
});
|
||||
|
||||
test('remove() 清理注册表与内存', () => {
|
||||
const t = MeToast.info('rm');
|
||||
expect(Toast._registry.has(t.id)).toBe(true);
|
||||
t.remove();
|
||||
expect(Toast._registry.has(t.id)).toBe(false);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.ts — 对话框与按钮交互', () => {
|
||||
test('confirm 点击确认按钮 resolve true', async () => {
|
||||
const promise = MeToast.confirm('确定删除?');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-confirm-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('confirm 点击取消按钮 resolve false', async () => {
|
||||
const promise = MeToast.confirm('确定删除?');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('prompt Enter 提交输入值', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||
input.value = '张三';
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
const result = await promise;
|
||||
expect(result).toBe('张三');
|
||||
});
|
||||
|
||||
test('prompt 点击提交按钮', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||
input.value = '李四';
|
||||
(toast.el!.querySelector('.met-submit-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe('李四');
|
||||
});
|
||||
|
||||
test('prompt 点击取消按钮 resolve null', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('action 按钮点击触发回调并关闭', async () => {
|
||||
const onClick = jest.fn();
|
||||
const a = MeToast.action('msg', [{ text: 'OK', onClick }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(onClick).toHaveBeenCalledWith(a.toast);
|
||||
expect(a.toast.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('action 按钮 close:false 点击不关闭', async () => {
|
||||
const onClick = jest.fn();
|
||||
const a = MeToast.action('msg', [{ text: 'Keep', onClick, close: false }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(a.toast.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('action 按钮 onClick 异常被捕获', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const a = MeToast.action('msg', [{ text: 'Boom', onClick: () => { throw new Error('act-err'); } }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('countdown onComplete 异常被捕获', (done) => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.countdown('{seconds} 秒', 1, { onComplete: () => { throw new Error('cd-err'); } });
|
||||
setTimeout(() => {
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
done();
|
||||
}, 1600);
|
||||
});
|
||||
|
||||
test('loading control 支持 update 与 dismiss', () => {
|
||||
const l = MeToast.loading('l1');
|
||||
l.update({ message: 'l2' });
|
||||
const toast = MeToast.find(l.id);
|
||||
expect(toast!.message).toBe('l2');
|
||||
l.dismiss();
|
||||
expect(toast!.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('group 单对象参数分支', () => {
|
||||
const g = MeToast.group('obj-group');
|
||||
const t = g.show({ message: 'obj-arg' });
|
||||
expect(t.group).toBe('obj-group');
|
||||
expect(t.message).toBe('obj-arg');
|
||||
g.dismiss();
|
||||
});
|
||||
|
||||
test('queue 消息级 onClose 异常被静默捕获', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const q = MeToast.queue([
|
||||
{ message: 'a', onClose: () => { throw new Error('q-err'); } },
|
||||
], { delay: 5, duration: 10 });
|
||||
await q;
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('_remove 从内存移除指定 id', () => {
|
||||
const t = MeToast.info('x');
|
||||
MeToast._remove(t.id);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugins.ts — 异常与钩子管理', () => {
|
||||
test('插件 install 抛错被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.plugins.register('bad-install', { name: 'bad-install', install: () => { throw new Error('install-err'); } });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('keyboard 插件 install/uninstall 移除事件监听', () => {
|
||||
MeToast.use('keyboard');
|
||||
MeToast.plugins.unregister('keyboard');
|
||||
expect(MeToast.plugins.has('keyboard')).toBe(false);
|
||||
});
|
||||
|
||||
test('persistence 插件 save 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||
const preset = pluginUtils.getPreset('persistence');
|
||||
const saveFn = (preset as Record<string, (c: unknown) => void>).save;
|
||||
expect(() => saveFn({ duration: 3000 })).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('persistence 插件 uninstall removeItem 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||
const preset = pluginUtils.getPreset('persistence');
|
||||
expect(() => preset?.uninstall?.()).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
+4
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 单元测试
|
||||
* @module tests
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast, VERSION } from '../src/index';
|
||||
@@ -105,8 +105,8 @@ const mockWindow = {
|
||||
|
||||
describe('版本信息', () => {
|
||||
test('应该有正确的版本号', () => {
|
||||
expect(VERSION).toBe('0.4.0');
|
||||
expect(MeToast.version).toBe('0.4.0');
|
||||
expect(VERSION).toBe('0.5.0');
|
||||
expect(MeToast.version).toBe('0.5.0');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -526,7 +526,7 @@ const mockWindow = {
|
||||
test('应该能够获取状态信息', () => {
|
||||
MeToast.success('消息');
|
||||
const status = MeToast.getStatus();
|
||||
expect(status.version).toBe('0.4.0');
|
||||
expect(status.version).toBe('0.5.0');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
|
||||
+61
-2
@@ -1,15 +1,19 @@
|
||||
/**
|
||||
* MetonaToast React 适配器测试
|
||||
* @module tests
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { act } from 'react';
|
||||
import MeToast, { Toast } from '../src/index';
|
||||
import { createBoundApi, Toast as ToastComponent } from '../src/react';
|
||||
import { createBoundApi, Toast as ToastComponent, useToast } from '../src/react';
|
||||
import type { ToastInstance } from '../src/types';
|
||||
|
||||
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
@@ -87,4 +91,59 @@ describe('React 适配器 — Toast 组件', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'y', type: 'custom-type' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('组件挂载创建 toast,卸载自动移除', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'mounted', type: 'success' })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('组件 autoClose:false 卸载不移除', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'keep', type: 'error', autoClose: false })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
MeToast.dismiss();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('组件 message 变化重新创建实例', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'v1', type: 'info' })); });
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'v2', type: 'warning' })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
const t = MeToast.getToasts()[0];
|
||||
expect(t.message).toBe('v2');
|
||||
expect(t.type).toBe('warning');
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('useToast 组件内创建的 toast 在卸载时清理', () => {
|
||||
const Harness = (): React.ReactElement => {
|
||||
const toast = useToast() as Record<string, (m: string) => ToastInstance>;
|
||||
return React.createElement('button', { onClick: () => toast.info('tracked') });
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(Harness)); });
|
||||
const btn = host.querySelector('button')!;
|
||||
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
|
||||
+16
-7
@@ -2,29 +2,38 @@
|
||||
* 环境隔离测试 — 需要切换 document/window/localStorage 环境并调用 jest.resetModules()。
|
||||
* 独立成文件:resetModules 会清空模块缓存,若与其他测试同文件会导致
|
||||
* 后续 require 拿到新模块实例、与 import 时绑定的实例状态分裂。
|
||||
* 注:jsdom 以 getter 暴露 document/window,普通赋值无效,须用 defineProperty 覆盖。
|
||||
* @module tests
|
||||
* @version 0.4.0
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/** 临时移除全局对象,返回恢复函数 */
|
||||
const hideGlobal = (key: string): (() => void) => {
|
||||
const desc = Object.getOwnPropertyDescriptor(global, key);
|
||||
Object.defineProperty(global, key, { value: undefined, configurable: true, writable: true });
|
||||
return () => {
|
||||
if (desc) Object.defineProperty(global, key, desc);
|
||||
else delete (global as Record<string, unknown>)[key];
|
||||
};
|
||||
};
|
||||
|
||||
describe('环境隔离(SSR / localStorage 异常路径)', () => {
|
||||
test('escapeHTML SSR 路径(无 document)', () => {
|
||||
const originalDoc = (global as Record<string, unknown>).document;
|
||||
(global as Record<string, unknown>).document = undefined;
|
||||
const restore = hideGlobal('document');
|
||||
jest.resetModules();
|
||||
const { escapeHTML } = require('../src/utils.js');
|
||||
const result = escapeHTML('<script>alert("xss")</script>');
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).toContain('<script>');
|
||||
(global as Record<string, unknown>).document = originalDoc;
|
||||
restore();
|
||||
});
|
||||
|
||||
test('prefersDark 无 window 时返回 false', () => {
|
||||
const originalWindow = (global as Record<string, unknown>).window;
|
||||
(global as Record<string, unknown>).window = undefined;
|
||||
const restore = hideGlobal('window');
|
||||
jest.resetModules();
|
||||
const { prefersDark } = require('../src/utils.js');
|
||||
expect(prefersDark()).toBe(false);
|
||||
(global as Record<string, unknown>).window = originalWindow;
|
||||
restore();
|
||||
});
|
||||
|
||||
test('saveTheme localStorage 异常被捕获', () => {
|
||||
|
||||
Reference in New Issue
Block a user