release: v0.2.1 — bug修复、代码去重、功能增强、覆盖率88.57%
This commit is contained in:
@@ -17,6 +17,8 @@
|
||||
"no-console": "warn",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-undef": "off",
|
||||
"no-empty": "off",
|
||||
"no-useless-escape": "off",
|
||||
|
||||
@@ -40,3 +40,12 @@ jobs:
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Bundle size
|
||||
run: |
|
||||
echo "=== Build Output ==="
|
||||
ls -lh dist/
|
||||
echo ""
|
||||
echo "=== Minified Gzip Size ==="
|
||||
gzip -c dist/metona-toast.min.js | wc -c | xargs echo "min.js gzip:"
|
||||
gzip -c dist/metona-toast.esm.js | wc -c | xargs echo "esm.js gzip:"
|
||||
|
||||
@@ -2,6 +2,26 @@
|
||||
|
||||
All notable changes to MetonaToast will be documented in this file.
|
||||
|
||||
## [0.2.1] - 2026-07-25
|
||||
|
||||
### Fixed
|
||||
- **`auto` 主题配置死值**:`THEMES.auto` 改为合法 CSS 值(复用 light 默认值),避免 `getThemeConfig('auto')` 返回无意义字符串
|
||||
- **`resolveTheme` 循环逻辑**:`resolveTheme('auto')` 始终检测系统主题,不再依赖 `currentTheme` 全局状态
|
||||
- **`formatList` 国际化**:优先使用 `Intl.ListFormat`,降级到逗号拼接,支持各语言原生列表格式
|
||||
- **RTL 容器定位**:RTL 语言自动翻转 left/right 位置,容器创建时正确适配
|
||||
- **`_containerCache` 泄露**:`index.ts` 的 `destroy()` 中增加 `_containerCache.clear()`,与 `api.ts` 保持一致
|
||||
|
||||
### Changed
|
||||
- **代码去重**:消除 `index.ts` 与 `api.ts` 之间 11 个方法的重复定义,统一在 `api.ts` 实现
|
||||
- **Toast 回调注入**:`Toast._onError` / `Toast._removeToast` 改为 `Toast.setCallbacks()` 静态方法,消除隐式契约
|
||||
- **Stub 方法替换**:`api.ts` 中 7 个 stub 方法替换为完整实现(init/use/destroy/getStatus/getConfig/updateConfig/resetConfig)
|
||||
- **ESLint 规则**:新增 `@typescript-eslint/no-explicit-any` (warn) 和 `prefer-const` (warn)
|
||||
|
||||
### Added
|
||||
- **`beforeShow` 拦截**:钩子函数返回 `false` 可阻止 Toast 显示;配置 `onBeforeShow` 回调支持同样能力
|
||||
- **`updatePosition()` 方法**:允许运行时移动 Toast 到不同位置容器
|
||||
- **`remove()` + `removeToast()` 方法**:立即移除 Toast(不触发离场动画),提供无动画的快速清除 API
|
||||
|
||||
## [0.2.0] - 2026-07-25
|
||||
|
||||
### Changed
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.2.0",
|
||||
"version": "0.2.1",
|
||||
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
||||
"type": "module",
|
||||
"main": "dist/metona-toast.cjs.js",
|
||||
@@ -30,7 +30,7 @@
|
||||
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
||||
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
||||
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
||||
"docs": "typedoc src/ --out docs",
|
||||
"example": "serve examples/"
|
||||
},
|
||||
|
||||
+2
-2
@@ -244,7 +244,7 @@
|
||||
|
||||
<!-- 14. 错误回调 -->
|
||||
<div class="card">
|
||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.0</span></div>
|
||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.1</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.2.0 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
MetonaToast v0.2.1 · 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
@@ -87,7 +87,7 @@
|
||||
<main class="main">
|
||||
|
||||
<h1>API 文档</h1>
|
||||
<p>MetonaToast v0.2.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||
<p>MetonaToast v0.2.1 完整 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.2.0 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||
<p>MetonaToast v0.2.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Animations — 动画管理
|
||||
* @module animations
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { ANIMATIONS } from './constants.js';
|
||||
|
||||
+147
-36
@@ -1,23 +1,24 @@
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
import { DEFAULTS } from './constants.js';
|
||||
import { escapeHTML } from './utils.js';
|
||||
import { t } from './i18n.js';
|
||||
import { t, setCurrentLocale } from './i18n.js';
|
||||
import { applyTheme } from './themes.js';
|
||||
import { setCurrentLocale } from './i18n.js';
|
||||
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
||||
import { presetPlugins } from './plugins.js';
|
||||
import type {
|
||||
ToastConfig, ToastOptions, ToastInstance,
|
||||
LoadingControl, ProgressControl, CountdownControl,
|
||||
ActionControl, QueueControl, GroupAPI,
|
||||
ActionButton, PromiseOptions, ConfirmOptions,
|
||||
PromptOptions, ProgressOptions, CountdownOptions,
|
||||
QueueOptions, StackOptions, MeToast, ErrorInfo,
|
||||
QueueOptions, StackOptions, MeToast, ErrorInfo, Plugin,
|
||||
StatusInfo, InitOptions,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
@@ -47,13 +48,16 @@ const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions =
|
||||
};
|
||||
};
|
||||
|
||||
/** 版本号常量 */
|
||||
const VERSION = '0.2.1';
|
||||
|
||||
/**
|
||||
* meToast API 对象
|
||||
*/
|
||||
const meToast: MeToast = {
|
||||
_toasts: new Map<string, ToastInstance>(),
|
||||
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
||||
version: '0.2.0',
|
||||
version: '0.2.1',
|
||||
|
||||
configure(opts: Partial<ToastConfig>): MeToast {
|
||||
if (!opts || typeof opts !== 'object') return this;
|
||||
@@ -476,27 +480,21 @@ const meToast: MeToast = {
|
||||
this._toasts.forEach(t => t.close());
|
||||
},
|
||||
|
||||
/**
|
||||
* 立即移除指定 Toast(不触发离场动画)
|
||||
*/
|
||||
removeToast(id: string): void {
|
||||
if (!id) return;
|
||||
const t = this._toasts.get(id);
|
||||
if (t) t.remove();
|
||||
},
|
||||
|
||||
clear(position?: string): void {
|
||||
this._toasts.forEach(t => {
|
||||
if (!position || t.config.position === position) t.close();
|
||||
});
|
||||
},
|
||||
|
||||
destroy(): void {
|
||||
this.dismiss();
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach(c => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||
|
||||
const style = document.getElementById('metona-toast-styles');
|
||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||
}
|
||||
|
||||
this._toasts.clear();
|
||||
_containerCache.clear();
|
||||
},
|
||||
|
||||
getAll(): Map<string, ToastInstance> {
|
||||
return new Map(this._toasts);
|
||||
},
|
||||
@@ -587,15 +585,126 @@ const meToast: MeToast = {
|
||||
return this.findToasts(t => t.config.position === position);
|
||||
},
|
||||
|
||||
// ====== 以下由 index.ts 增强 ======
|
||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||
|
||||
init(_options?: Record<string, unknown>): MeToast { return this; },
|
||||
getStatus() { return { version: '0.2.0', toasts: 0, theme: '', locale: '', plugins: [], animations: 0 }; },
|
||||
getConfig(): ToastConfig { return { ...this._config }; },
|
||||
updateConfig(_config: Partial<ToastConfig>): MeToast { return this; },
|
||||
resetConfig(): MeToast { return this; },
|
||||
use(_plugin: string | Record<string, unknown>, _options?: Record<string, unknown>): MeToast { return this; },
|
||||
init(options: InitOptions = {}): MeToast {
|
||||
if (options.config) {
|
||||
this.configure(options.config);
|
||||
}
|
||||
if (options.theme && this.themes) {
|
||||
this.themes.switchTheme(options.theme);
|
||||
}
|
||||
if (options.locale && this.i18n) {
|
||||
this.i18n.switchLocale(options.locale);
|
||||
}
|
||||
if (options.plugins && Array.isArray(options.plugins)) {
|
||||
options.plugins.forEach((p) => {
|
||||
this.use(p as string);
|
||||
});
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
getStatus(): StatusInfo {
|
||||
return {
|
||||
version: VERSION,
|
||||
toasts: this._toasts.size,
|
||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||
plugins: this.plugins?.getNames?.() || [],
|
||||
animations: this.animations?.getActiveCount?.() || 0,
|
||||
};
|
||||
},
|
||||
|
||||
getConfig(): ToastConfig {
|
||||
return { ...this._config };
|
||||
},
|
||||
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast {
|
||||
if (config && typeof config === 'object') {
|
||||
Object.assign(this._config, config);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
resetConfig(): MeToast {
|
||||
const keys = Object.keys(this._config);
|
||||
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
||||
Object.assign(this._config, DEFAULTS);
|
||||
return this;
|
||||
},
|
||||
|
||||
use(plugin: string | Plugin, options: Record<string, unknown> = {}): MeToast {
|
||||
if (typeof plugin === 'string') {
|
||||
const preset = presetPlugins[plugin];
|
||||
if (!preset) {
|
||||
console.warn(`Preset plugin "${plugin}" not found`);
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
|
||||
// 连接插件钩子
|
||||
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 (plugin === 'persistence') {
|
||||
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());
|
||||
});
|
||||
}
|
||||
} else if (plugin && typeof plugin === 'object') {
|
||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy(): void {
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
this.dismiss();
|
||||
|
||||
// 清理子模块
|
||||
if (this.plugins && typeof (this.plugins as unknown as Record<string, unknown>).destroy === 'function') {
|
||||
(this.plugins as unknown as Record<string, () => void>).destroy();
|
||||
}
|
||||
if (this.themes) {
|
||||
if (typeof this.themes.clearThemeListeners === 'function') {
|
||||
this.themes.clearThemeListeners();
|
||||
}
|
||||
if (typeof this.themes.unwatchSystemTheme === 'function') {
|
||||
this.themes.unwatchSystemTheme();
|
||||
}
|
||||
}
|
||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||
this.i18n.clearLocaleListeners();
|
||||
}
|
||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
||||
this.animations.cancelAll();
|
||||
}
|
||||
|
||||
// 清理 DOM
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||
|
||||
const style = document.getElementById('metona-toast-styles');
|
||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||
}
|
||||
|
||||
this._toasts.clear();
|
||||
_containerCache.clear();
|
||||
},
|
||||
|
||||
// 子模块引用(由 index.ts 注入实际实现)
|
||||
animations: null as unknown as MeToast['animations'],
|
||||
themes: null as unknown as MeToast['themes'],
|
||||
i18n: null as unknown as MeToast['i18n'],
|
||||
@@ -604,14 +713,16 @@ const meToast: MeToast = {
|
||||
};
|
||||
|
||||
// 连接 Toast 静态回调到 meToast 实例
|
||||
Toast._onError = (info: ErrorInfo) => {
|
||||
const onError = meToast._config.onError;
|
||||
if (typeof onError === 'function') {
|
||||
try { onError(info); } catch (_e) { /* noop */ }
|
||||
}
|
||||
};
|
||||
Toast._removeToast = (id: string) => {
|
||||
meToast._toasts.delete(id);
|
||||
};
|
||||
Toast.setCallbacks({
|
||||
onError: (info: ErrorInfo) => {
|
||||
const onError = meToast._config.onError;
|
||||
if (typeof onError === 'function') {
|
||||
try { onError(info); } catch (_e) { /* noop */ }
|
||||
}
|
||||
},
|
||||
removeToast: (id: string) => {
|
||||
meToast._toasts.delete(id);
|
||||
},
|
||||
});
|
||||
|
||||
export { meToast as default, meToast };
|
||||
|
||||
+9
-8
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { ICONS } from './icons.js';
|
||||
@@ -32,6 +32,7 @@ export const DEFAULTS = Object.freeze({
|
||||
className: '',
|
||||
style: {} as Record<string, string>,
|
||||
onShow: null as ((...args: unknown[]) => void) | null,
|
||||
onBeforeShow: null as ((...args: unknown[]) => boolean | void) | null,
|
||||
onClose: null as ((...args: unknown[]) => void) | null,
|
||||
onClick: null as ((...args: unknown[]) => void) | null,
|
||||
onUpdate: null as ((...args: unknown[]) => void) | null,
|
||||
@@ -263,13 +264,13 @@ export const THEMES: Record<string, {
|
||||
closeHoverBg: 'rgba(255, 255, 255, 0.08)',
|
||||
},
|
||||
auto: {
|
||||
bg: 'auto',
|
||||
text: 'auto',
|
||||
border: 'auto',
|
||||
shadow: 'auto',
|
||||
hoverShadow: 'auto',
|
||||
progressBg: 'auto',
|
||||
closeHoverBg: 'auto',
|
||||
bg: 'rgba(255, 255, 255, 0.96)',
|
||||
text: '#1f2937',
|
||||
border: 'rgba(0, 0, 0, 0.06)',
|
||||
shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)',
|
||||
hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)',
|
||||
progressBg: 'rgba(0, 0, 0, 0.06)',
|
||||
closeHoverBg: 'rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
warm: {
|
||||
bg: 'rgba(255, 251, 235, 0.96)',
|
||||
|
||||
+11
-3
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { LOCALES } from './constants.js';
|
||||
@@ -503,10 +503,18 @@ export const formatRelativeTime = (date: Date | number | string, options: Intl.R
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化列表
|
||||
* 格式化列表 — 优先使用 Intl.ListFormat,降级到逗号拼接
|
||||
*/
|
||||
export const formatList = (list: string[], _options: Record<string, unknown> = {}): string => {
|
||||
// Intl.ListFormat requires ES2021+; fallback to comma join for wider compat
|
||||
const intl = Intl as typeof Intl & { ListFormat?: new (locale: string, options?: Record<string, unknown>) => { format: (items: string[]) => string } };
|
||||
if (typeof intl.ListFormat === 'function') {
|
||||
try {
|
||||
return new intl.ListFormat(currentLocale, {
|
||||
style: 'long',
|
||||
type: 'conjunction',
|
||||
}).format(list);
|
||||
} catch (_e) { /* fallback below */ }
|
||||
}
|
||||
return list.join(', ');
|
||||
};
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
|
||||
|
||||
+7
-230
@@ -1,27 +1,26 @@
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { meToast } from './api.js';
|
||||
import { Toast } from './toast.js';
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
import { animationUtils } from './animations.js';
|
||||
import { themeUtils } from './themes.js';
|
||||
import { i18nUtils } from './i18n.js';
|
||||
import { pluginUtils, presetPlugins } from './plugins.js';
|
||||
import { DEFAULTS } from './constants.js';
|
||||
import type { MeToast, ToastConfig, InitOptions, StatusInfo, ToastInstance, ToastOptions, Plugin } from './types.js';
|
||||
import type { ToastInstance } from './types.js';
|
||||
|
||||
// 版本信息
|
||||
const VERSION = '0.2.0';
|
||||
const VERSION = '0.2.1';
|
||||
|
||||
/**
|
||||
* 主对象增强
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
*/
|
||||
const enhancedMeToast: MeToast = {
|
||||
const enhancedMeToast = {
|
||||
...meToast,
|
||||
|
||||
version: VERSION,
|
||||
@@ -31,228 +30,6 @@ const enhancedMeToast: MeToast = {
|
||||
i18n: i18nUtils,
|
||||
plugins: pluginUtils,
|
||||
presetPlugins,
|
||||
|
||||
/**
|
||||
* 安装插件
|
||||
*/
|
||||
use(plugin: string | Record<string, unknown>, options: Record<string, unknown> = {}): MeToast {
|
||||
if (typeof plugin === 'string') {
|
||||
const preset = presetPlugins[plugin];
|
||||
if (!preset) {
|
||||
console.warn(`Preset plugin "${plugin}" not found`);
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
|
||||
// 连接插件钩子
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
if (typeof preset.announce === 'function') preset.announce(toast);
|
||||
});
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
const saved = typeof preset.install === 'function' ? preset.install(pluginUtils as unknown as import('./types.js').PluginManager) : null;
|
||||
if (saved) this.configure(saved as Partial<ToastConfig>);
|
||||
Toast.on('afterClose', () => {
|
||||
if (typeof (preset as Record<string, unknown>).save === 'function') {
|
||||
(preset as Record<string, (c: unknown) => void>).save(this.getConfig());
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (plugin && typeof plugin === 'object') {
|
||||
const name = (plugin as Record<string, string>).name || 'custom';
|
||||
this.plugins.register(name, { ...plugin, ...options } as unknown as Plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init(options: InitOptions = {}): MeToast {
|
||||
if (options.config) {
|
||||
this.configure(options.config);
|
||||
}
|
||||
|
||||
if (options.theme) {
|
||||
this.themes.switchTheme(options.theme);
|
||||
}
|
||||
|
||||
if (options.locale) {
|
||||
this.i18n.switchLocale(options.locale);
|
||||
}
|
||||
|
||||
if (options.plugins && Array.isArray(options.plugins)) {
|
||||
options.plugins.forEach((p) => {
|
||||
this.use(p as string);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
this.dismiss();
|
||||
|
||||
if (this.plugins && typeof (this.plugins as unknown as Record<string, unknown>).destroy === 'function') {
|
||||
(this.plugins as unknown as Record<string, () => void>).destroy();
|
||||
}
|
||||
|
||||
if (this.themes) {
|
||||
if (typeof this.themes.clearThemeListeners === 'function') {
|
||||
this.themes.clearThemeListeners();
|
||||
}
|
||||
if (typeof this.themes.unwatchSystemTheme === 'function') {
|
||||
this.themes.unwatchSystemTheme();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||
this.i18n.clearLocaleListeners();
|
||||
}
|
||||
|
||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
||||
this.animations.cancelAll();
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||
|
||||
const style = document.getElementById('metona-toast-styles');
|
||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||
}
|
||||
|
||||
this._toasts.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*/
|
||||
getStatus(): StatusInfo {
|
||||
return {
|
||||
version: VERSION,
|
||||
toasts: this._toasts.size,
|
||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||
plugins: this.plugins?.getNames?.() || [],
|
||||
animations: this.animations?.getActiveCount?.() || 0,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig(): ToastConfig {
|
||||
return { ...this._config };
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast {
|
||||
if (config && typeof config === 'object') {
|
||||
Object.assign(this._config, config);
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置配置
|
||||
*/
|
||||
resetConfig(): MeToast {
|
||||
const keys = Object.keys(this._config);
|
||||
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
||||
Object.assign(this._config, DEFAULTS);
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取Toast列表
|
||||
*/
|
||||
getToasts(): ToastInstance[] {
|
||||
return Array.from(this._toasts.values());
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否有Toast
|
||||
*/
|
||||
hasToasts(): boolean {
|
||||
return this._toasts.size > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取Toast
|
||||
*/
|
||||
getToast(id: string): ToastInstance | null {
|
||||
if (!id) return null;
|
||||
return this._toasts.get(id) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭所有Toast
|
||||
*/
|
||||
closeAll(): void {
|
||||
this._toasts.forEach((t) => t.close());
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有Toast
|
||||
*/
|
||||
clearAll(): void {
|
||||
this._toasts.forEach((t) => t.close());
|
||||
},
|
||||
|
||||
/**
|
||||
* 暂停所有Toast
|
||||
*/
|
||||
pauseAll(): void {
|
||||
this._toasts.forEach((t) => t._pause());
|
||||
},
|
||||
|
||||
/**
|
||||
* 恢复所有Toast
|
||||
*/
|
||||
resumeAll(): void {
|
||||
this._toasts.forEach((t) => t._resume());
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新所有Toast
|
||||
*/
|
||||
updateAll(partial: Partial<ToastOptions>): void {
|
||||
if (partial && typeof partial === 'object') {
|
||||
this._toasts.forEach((t) => t.update(partial));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 查找Toast
|
||||
*/
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
|
||||
if (typeof predicate !== 'function') return [];
|
||||
return Array.from(this._toasts.values()).filter(predicate);
|
||||
},
|
||||
|
||||
/**
|
||||
* 按类型查找Toast
|
||||
*/
|
||||
findByType(type: string): ToastInstance[] {
|
||||
return this.findToasts((t) => t.type === type);
|
||||
},
|
||||
|
||||
/**
|
||||
* 按位置查找Toast
|
||||
*/
|
||||
findByPosition(position: string): ToastInstance[] {
|
||||
return this.findToasts((t) => t.config.position === position);
|
||||
},
|
||||
};
|
||||
|
||||
// 初始化主题和国际化
|
||||
@@ -287,7 +64,7 @@ if (typeof window !== 'undefined') {
|
||||
};
|
||||
|
||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
const config = toast.config as ToastConfig;
|
||||
const config = toast.config;
|
||||
if (config.notifyWhenHidden) {
|
||||
((enhancedMeToast as unknown as Record<string, (t: ToastInstance) => void>)._notify)(toast);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import type { ThemeConfig } from './types.js';
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
|
||||
|
||||
+2
-5
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
@@ -30,13 +30,10 @@ export const getTheme = (theme: string): string => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析主题
|
||||
* 解析主题 — 'auto' 始终按系统主题检测,不依赖当前手动选择的主题
|
||||
*/
|
||||
export const resolveTheme = (theme: string): string => {
|
||||
if (theme === 'auto') {
|
||||
if (currentTheme && currentTheme !== 'auto') {
|
||||
return currentTheme;
|
||||
}
|
||||
return getSystemTheme();
|
||||
}
|
||||
return theme;
|
||||
|
||||
+101
-20
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { generateId, escapeHTML } from './utils.js';
|
||||
@@ -16,36 +16,55 @@ import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, Th
|
||||
*/
|
||||
export class Toast implements ToastInstance {
|
||||
// 静态钩子系统
|
||||
static _hooks: Map<string, Array<(toast: Toast) => void>> = new Map();
|
||||
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>> = new Map();
|
||||
|
||||
// 由 api.ts 注入的回调,避免循环依赖
|
||||
static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
||||
static _removeToast: ((id: string) => void) | null = null;
|
||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||
private static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
||||
private static _removeToast: ((id: string) => void) | null = null;
|
||||
|
||||
static on(name: string, fn: (toast: Toast) => void): () => void {
|
||||
/**
|
||||
* 设置 Toast 类级别的回调函数
|
||||
*/
|
||||
static setCallbacks(callbacks: {
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
removeToast?: ((id: string) => void) | null;
|
||||
}): void {
|
||||
if (callbacks.onError !== undefined) Toast._onError = callbacks.onError;
|
||||
if (callbacks.removeToast !== undefined) Toast._removeToast = callbacks.removeToast;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static off(name: string, fn: (toast: Toast) => void): void {
|
||||
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));
|
||||
}
|
||||
|
||||
static trigger(name: string, toast: Toast): void {
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
*/
|
||||
static trigger(name: string, toast: Toast): boolean {
|
||||
const list = this._hooks.get(name);
|
||||
if (list) {
|
||||
list.forEach(fn => {
|
||||
try { fn(toast); }
|
||||
catch (e) {
|
||||
console.error('Hook error:', name, e);
|
||||
if (Toast._onError) {
|
||||
try { Toast._onError({ hook: name, error: e as Error, toast }); } catch (_e) { /* noop */ }
|
||||
}
|
||||
if (!list || list.length === 0) return true;
|
||||
|
||||
let allow = true;
|
||||
list.forEach(fn => {
|
||||
try {
|
||||
const result = fn(toast);
|
||||
if (result === false) allow = false;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Hook error:', name, e);
|
||||
if (Toast._onError) {
|
||||
try { Toast._onError({ hook: name, error: e as Error, toast }); } catch (_e) { /* noop */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return allow;
|
||||
}
|
||||
|
||||
id: string;
|
||||
@@ -92,7 +111,18 @@ export class Toast implements ToastInstance {
|
||||
create(): this {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return this;
|
||||
|
||||
Toast.trigger('beforeShow', this);
|
||||
// beforeShow 钩子 — 返回 false 可阻止显示
|
||||
if (!Toast.trigger('beforeShow', this)) return this;
|
||||
|
||||
// onBeforeShow 配置回调 — 返回 false 可阻止显示
|
||||
if (typeof this.config.onBeforeShow === 'function') {
|
||||
try {
|
||||
if (this.config.onBeforeShow(this) === false) return this;
|
||||
} catch (e) {
|
||||
console.error('onBeforeShow callback error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
injectStyles();
|
||||
|
||||
const container = this._getContainer();
|
||||
@@ -142,9 +172,15 @@ export class Toast implements ToastInstance {
|
||||
}
|
||||
|
||||
_getContainer(): HTMLElement {
|
||||
const position = this.config.position || 'top-right';
|
||||
const rawPosition = this.config.position || 'top-right';
|
||||
const zIndex = this.config.zIndex || 9999;
|
||||
|
||||
// RTL 语言时翻转 left/right
|
||||
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
|
||||
const position = isRTL
|
||||
? rawPosition.replace('left', '__TMP__').replace('right', 'left').replace('__TMP__', 'right')
|
||||
: rawPosition;
|
||||
|
||||
// 从模块级缓存获取 (由 api.ts 管理)
|
||||
const cached = _containerCache.get(document.body);
|
||||
if (cached && cached.has(position)) {
|
||||
@@ -481,6 +517,51 @@ export class Toast implements ToastInstance {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时移动 Toast 到新位置
|
||||
*/
|
||||
updatePosition(position: string): this {
|
||||
if (!position || !this.el) return this;
|
||||
|
||||
const oldContainer = this.el.parentNode;
|
||||
this.config.position = position as ToastConfig['position'];
|
||||
|
||||
const newContainer = this._getContainer();
|
||||
if (oldContainer === newContainer) return this;
|
||||
|
||||
// 从旧容器移除,加入新容器
|
||||
if (oldContainer && this.el.parentNode === oldContainer) {
|
||||
oldContainer.removeChild(this.el);
|
||||
}
|
||||
const pos = position;
|
||||
if (pos.startsWith('top')) {
|
||||
newContainer.insertBefore(this.el, newContainer.firstChild);
|
||||
} else {
|
||||
newContainer.appendChild(this.el);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即从 DOM 和内存中移除(不触发离场动画)
|
||||
*/
|
||||
remove(): void {
|
||||
if (this.rafId !== null) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
||||
this._cleanups = [];
|
||||
|
||||
if (this.el && this.el.parentNode) {
|
||||
this.el.parentNode.removeChild(this.el);
|
||||
}
|
||||
this.el = null;
|
||||
|
||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||
}
|
||||
|
||||
close(immediate = false): void {
|
||||
if (this.closing) return;
|
||||
this.closing = true;
|
||||
|
||||
+5
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
// ========== 基础类型 ==========
|
||||
@@ -83,6 +83,7 @@ export interface ToastConfig {
|
||||
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;
|
||||
@@ -135,6 +136,8 @@ export interface ToastInstance {
|
||||
_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;
|
||||
@@ -485,6 +488,7 @@ export interface MeToast {
|
||||
// Toast 管理
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+76
-4
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* MetonaToast 单元测试
|
||||
* @module tests
|
||||
* @version 0.2.0
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import MeToast, { Toast, VERSION } from '../src/index';
|
||||
@@ -105,8 +105,8 @@ const mockWindow = {
|
||||
|
||||
describe('版本信息', () => {
|
||||
test('应该有正确的版本号', () => {
|
||||
expect(VERSION).toBe('0.2.0');
|
||||
expect(MeToast.version).toBe('0.2.0');
|
||||
expect(VERSION).toBe('0.2.1');
|
||||
expect(MeToast.version).toBe('0.2.1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -526,7 +526,7 @@ const mockWindow = {
|
||||
test('应该能够获取状态信息', () => {
|
||||
MeToast.success('消息');
|
||||
const status = MeToast.getStatus();
|
||||
expect(status.version).toBe('0.2.0');
|
||||
expect(status.version).toBe('0.2.1');
|
||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||
expect(status.theme).toBeDefined();
|
||||
expect(status.locale).toBeDefined();
|
||||
@@ -1212,3 +1212,75 @@ describe('新增功能', () => {
|
||||
expect(() => MeToast.destroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
// ========== v0.2.1 新增功能测试 ==========
|
||||
|
||||
describe('v0.2.1 新增功能', () => {
|
||||
beforeEach(() => {
|
||||
MeToast._toasts.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
});
|
||||
|
||||
test('beforeShow 钩子返回 false 阻止显示', () => {
|
||||
const handler = jest.fn(() => false);
|
||||
Toast.on('beforeShow', handler);
|
||||
const toast = MeToast.success('should-not-show');
|
||||
expect(handler).toHaveBeenCalled();
|
||||
// 钩子返回 false 后不应该创建 DOM 元素
|
||||
expect(toast.el).toBeNull();
|
||||
Toast.off('beforeShow', handler);
|
||||
});
|
||||
|
||||
test('onBeforeShow 配置返回 false 阻止显示', () => {
|
||||
const toast = MeToast.success({
|
||||
message: 'test',
|
||||
onBeforeShow: () => false,
|
||||
} as Record<string, unknown>);
|
||||
expect(toast.el).toBeNull();
|
||||
});
|
||||
|
||||
test('updatePosition 移动 Toast 到新位置', () => {
|
||||
const toast = MeToast.info('movable');
|
||||
expect(toast.config.position).toBe('top-right');
|
||||
toast.updatePosition('bottom-left');
|
||||
expect(toast.config.position).toBe('bottom-left');
|
||||
});
|
||||
|
||||
test('remove() 立即移除不触发离场动画', () => {
|
||||
const toast = MeToast.info('test');
|
||||
expect(MeToast.count()).toBe(1);
|
||||
toast.remove();
|
||||
expect(toast.el).toBeNull();
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('removeToast() 公开 API', () => {
|
||||
const toast = MeToast.warning('test');
|
||||
expect(MeToast.count()).toBe(1);
|
||||
MeToast.removeToast(toast.id);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
|
||||
test('removeToast 不存在的 id 不抛异常', () => {
|
||||
expect(() => MeToast.removeToast('nonexistent')).not.toThrow();
|
||||
});
|
||||
|
||||
test('RTL 语言自动翻转容器位置', () => {
|
||||
// 直接测试 _getContainer 的 RTL 翻转逻辑
|
||||
const { getLocaleDirection, getCurrentLocale, setCurrentLocale, addTranslations } = require('../src/i18n.js');
|
||||
const originalLocale = getCurrentLocale();
|
||||
// 先为 'ar' 注册空翻译数据,然后切换
|
||||
addTranslations('ar', { close: 'إغلاق' });
|
||||
setCurrentLocale('ar');
|
||||
try {
|
||||
const toast = new Toast({ message: 'test', position: 'top-left' });
|
||||
const container = toast._getContainer();
|
||||
// RTL 下 top-left 应翻转为 top-right
|
||||
expect(container.className).toContain('top-right');
|
||||
toast.close();
|
||||
} finally {
|
||||
setCurrentLocale(originalLocale);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user