release: v0.3.0 — 自定义动画真实生效、max超限修复、钩子系统完整化、destroy全量清理

- 自定义动画:animations.register() 注册的动画动态注入 keyframes 真实生效(此前仅存 Map 实际 fallback slide)
- max 超限:通过实例注册表真正 close() 最早的 toast(此前仅删内存记录,DOM 残留)
- 钩子收敛:beforeClose/beforeUpdate 支持返回 false 拦截;接入 click/hover/dragStart/dragEnd/animationStart/animationEnd/progressStart/progressEnd/configChange/themeChange/localeChange/beforeInit/afterInit/beforeDestroy/afterDestroy;HOOK_NAMES 移除永不触发的声明
- destroy() 全量清理:hooks、实例注册表、全部 metona-toast-* 样式标签;动画注册表真正清空
- applyThemeVariables 重复注入修复;init() 支持插件对象并复活 _destroyed;AnimationConfig 类型放宽
- VERSION 统一到 constants.ts 唯一维护;删除失效 scripts;新增 .gitattributes 统一 LF
- docs.html 补 v0.2.1 新 API 文档;CHANGELOG 更新;测试 309 用例全过
This commit is contained in:
tianhao
2026-08-08 14:23:41 +08:00
parent cb8dd623db
commit 2937ce5e2d
30 changed files with 1098 additions and 372 deletions
+212 -99
View File
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
/**
* MetonaToast Utils — 工具函数
* @module utils
* @version 0.2.1
* @version 0.3.0
*/
/**
* 生成唯一ID
@@ -42,7 +42,7 @@ const prefersDark = () => {
/**
* MetonaToast Icons — 图标SVG定义
* @module icons
* @version 0.2.1
* @version 0.3.0
* @description 107 个内置 SVG 图标
*/
const ICONS = {
@@ -539,7 +539,7 @@ const ICONS = {
/**
* MetonaToast Locales — 国际化翻译数据
* @module locales
* @version 0.2.1
* @version 0.3.0
* @description 内置 zh-CN / en-US 完整翻译
*/
const LOCALES = {
@@ -1026,8 +1026,12 @@ const LOCALES = {
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.2.1
* @version 0.3.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
const VERSION = '0.3.0';
/**
* 默认配置
*/
@@ -1285,11 +1289,15 @@ const THEMES = {
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
},
};
/**
* 动画类型
*/
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
/**
* MetonaToast Styles — 样式管理
* @module styles
* @version 0.2.1
* @version 0.3.0
*/
// 样式缓存
let styleElement = null;
@@ -1743,7 +1751,7 @@ const injectStyles = () => {
/**
* MetonaToast Themes — 主题管理
* @module themes
* @version 0.2.1
* @version 0.3.0
*/
// 当前主题状态
let currentTheme = 'auto';
@@ -2087,7 +2095,7 @@ const themeUtils = {
/**
* MetonaToast i18n — 国际化管理
* @module i18n
* @version 0.2.1
* @version 0.3.0
*/
// 当前语言状态
let currentLocale = 'zh-CN';
@@ -2643,10 +2651,126 @@ const i18nUtils = {
},
});
/**
* MetonaToast Animations — 动画管理
* @module animations
* @version 0.2.1
*/
// 动画缓存
const animationMap = new Map();
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
// 动态注入的自定义动画样式(惰性创建)
let animStyleElement = null;
const injectedAnimations = new Set();
/**
* 将 CSS 属性对象转换为内联 CSS 字符串
*/
const toCss = (props) => Object.entries(props)
.map(([key, value]) => `${key}:${value}`)
.join(';');
/**
* 为自定义动画生成入场 keyframes + 类规则
*/
const buildAnimationCSS = (name, config) => {
const enter = toCss(config.enter);
return `
@keyframes met-${name}-in {
from { ${enter}; }
}
.met-anim-${name}.met-toast { opacity: 0; }
.met-anim-${name}.met-toast.met-show {
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
}
`;
};
/**
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
*/
const ensureAnimationCSS = (name) => {
if (typeof document === 'undefined')
return;
if (BUILTIN_ANIMATIONS.has(name))
return;
const config = animationMap.get(name);
if (!config || injectedAnimations.has(name))
return;
if (!animStyleElement) {
animStyleElement = document.getElementById('metona-toast-anim-styles');
if (!animStyleElement) {
animStyleElement = document.createElement('style');
animStyleElement.id = 'metona-toast-anim-styles';
document.head.appendChild(animStyleElement);
}
}
animStyleElement.textContent += buildAnimationCSS(name, config);
injectedAnimations.add(name);
};
/**
* 检查动画是否存在(内置或自定义)
*/
const hasAnimation = (name) => animationMap.has(name);
// 注册默认动画
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
/**
* 动画工具函数
*/
const animationUtils = {
register(name, config) {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name) {
animationMap.delete(name);
},
get(name) {
return animationMap.get(name) || null;
},
getAnimationNames() {
return Array.from(animationMap.keys());
},
getActiveCount() {
return animationMap.size;
},
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy() {
animationMap.clear();
injectedAnimations.clear();
animStyleElement = null;
},
};
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.2.1
* @version 0.3.0
*/
/**
* Toast 类 — 核心通知组件
@@ -2674,8 +2798,9 @@ class Toast {
}
/**
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
*/
static trigger(name, toast) {
static trigger(name, toast = null) {
const list = this._hooks.get(name);
if (!list || list.length === 0)
return true;
@@ -2690,7 +2815,7 @@ class Toast {
console.error('Hook error:', name, e);
if (Toast._onError) {
try {
Toast._onError({ hook: name, error: e, toast });
Toast._onError({ hook: name, error: e, toast: toast });
}
catch (_e) { /* noop */ }
}
@@ -2745,6 +2870,7 @@ class Toast {
}
}
injectStyles();
ensureAnimationCSS(this.config.animation || 'slide');
const container = this._getContainer();
const { theme, c, t } = this._palette();
this._limitToasts(container);
@@ -2764,6 +2890,7 @@ class Toast {
else {
container.appendChild(el);
}
Toast._registry.set(this.id, this);
this._bindEvents(el);
this._startTimer();
// 进入动画
@@ -2831,18 +2958,22 @@ class Toast {
_limitToasts(container) {
const max = this.config.max || 6;
const list = Array.from(container.querySelectorAll('.met-toast'));
if (list.length >= max) {
const first = list[0];
const id = first?.dataset.id;
if (id && Toast._removeToast) {
while (list.length >= max) {
const el = list.shift();
const id = el?.dataset.id;
if (!id)
continue;
const existing = Toast._registry.get(id);
if (existing) {
existing.close(true);
}
else if (Toast._removeToast) {
Toast._removeToast(id);
}
}
}
_buildClassName(theme) {
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
return [
'met-toast',
`met-${this.type}`,
@@ -2919,6 +3050,7 @@ class Toast {
this.close();
return;
}
Toast.trigger('click', this);
if (this.config.closeOnClick && !target.closest('.met-close')) {
if (typeof this.config.onClick === 'function') {
try {
@@ -2935,8 +3067,14 @@ class Toast {
el.addEventListener('click', eventHandler);
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
const mouseEnter = () => this._pause();
const mouseLeave = () => this._resume();
const mouseEnter = () => {
Toast.trigger('hover', this);
this._pause();
};
const mouseLeave = () => {
Toast.trigger('hover', this);
this._resume();
};
el.addEventListener('mouseenter', mouseEnter);
el.addEventListener('mouseleave', mouseLeave);
this._cleanups.push(() => {
@@ -2944,6 +3082,15 @@ class Toast {
el.removeEventListener('mouseleave', mouseLeave);
});
}
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
const animStart = () => Toast.trigger('animationStart', this);
const animEnd = () => Toast.trigger('animationEnd', this);
el.addEventListener('animationstart', animStart);
el.addEventListener('animationend', animEnd);
this._cleanups.push(() => {
el.removeEventListener('animationstart', animStart);
el.removeEventListener('animationend', animEnd);
});
if (this.config.draggable) {
this._bindDrag(el);
}
@@ -2959,6 +3106,7 @@ class Toast {
el.setPointerCapture(e.pointerId);
el.style.transition = 'none';
this._pause();
Toast.trigger('dragStart', this);
};
const move = (e) => {
if (!dragging)
@@ -2985,6 +3133,7 @@ class Toast {
this._resume();
}
dx = dy = 0;
Toast.trigger('dragEnd', this);
};
el.addEventListener('pointerdown', down);
el.addEventListener('pointermove', move);
@@ -3003,6 +3152,7 @@ class Toast {
if (!resuming) {
this.startedAt = Date.now();
this.remaining = this.config.duration || 0;
Toast.trigger('progressStart', this);
}
const tick = () => {
if (this.paused || this.closing)
@@ -3017,6 +3167,7 @@ class Toast {
this.barEl.style.transform = t;
}
if (this.remaining <= 0) {
Toast.trigger('progressEnd', this);
this.close();
return;
}
@@ -3050,7 +3201,9 @@ class Toast {
this._startTimer(true);
}
update(partial) {
Toast.trigger('beforeUpdate', this);
// beforeUpdate 钩子 — 返回 false 可阻止更新
if (!Toast.trigger('beforeUpdate', this))
return this;
const typeChanged = partial.type && partial.type !== this.type;
if (partial.type)
this.type = partial.type;
@@ -3147,14 +3300,17 @@ class Toast {
this.el.parentNode.removeChild(this.el);
}
this.el = null;
Toast._registry.delete(this.id);
if (Toast._removeToast)
Toast._removeToast(this.id);
}
close(immediate = false) {
if (this.closing)
return;
// beforeClose 钩子 — 返回 false 可阻止关闭
if (!Toast.trigger('beforeClose', this))
return;
this.closing = true;
Toast.trigger('beforeClose', this);
if (this.rafId !== null)
cancelAnimationFrame(this.rafId);
this._cleanups.forEach(fn => { try {
@@ -3210,12 +3366,15 @@ class Toast {
}
}
Toast.trigger('afterClose', this);
Toast._registry.delete(this.id);
if (Toast._removeToast)
Toast._removeToast(this.id);
}
}
// 静态钩子系统
Toast._hooks = new Map();
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
Toast._registry = new Map();
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
Toast._onError = null;
Toast._removeToast = null;
@@ -3227,7 +3386,7 @@ const _containerCache = new Map();
/**
* MetonaToast Templates — HTML 模板辅助函数
* @module templates
* @version 0.2.1
* @version 0.3.0
* @description confirm / prompt / progress / action 的 DOM 模板
*/
/**
@@ -3299,7 +3458,7 @@ const actionHTML = (actions) => {
/**
* MetonaToast Plugins — 插件系统
* @module plugins
* @version 0.2.1
* @version 0.3.0
*/
/**
* 插件管理器
@@ -3519,7 +3678,7 @@ const defaultPluginManager = new PluginManager();
/**
* MetonaToast API — meToast 核心 API 对象
* @module api
* @version 0.2.1
* @version 0.3.0
*/
/**
* 参数标准化工具
@@ -3544,15 +3703,13 @@ const normalizeArgs = (args, defaultType = 'default') => {
message: '',
};
};
/** 版本号常量 */
const VERSION$1 = '0.2.1';
/**
* meToast API 对象
*/
const meToast = {
_toasts: new Map(),
_config: { ...DEFAULTS },
version: '0.2.1',
version: VERSION,
configure(opts) {
if (!opts || typeof opts !== 'object')
return this;
@@ -3563,6 +3720,7 @@ const meToast = {
if (opts.locale) {
setCurrentLocale(opts.locale);
}
Toast.trigger('configChange');
return this;
},
_emit(opts) {
@@ -4034,6 +4192,8 @@ const meToast = {
},
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
init(options = {}) {
Toast.trigger('beforeInit');
this._destroyed = false;
if (options.config) {
this.configure(options.config);
}
@@ -4048,11 +4208,12 @@ const meToast = {
this.use(p);
});
}
Toast.trigger('afterInit');
return this;
},
getStatus() {
return {
version: VERSION$1,
version: VERSION,
toasts: this._toasts.size,
theme: this.themes?.getCurrentTheme?.() || 'auto',
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
@@ -4066,6 +4227,7 @@ const meToast = {
updateConfig(config) {
if (config && typeof config === 'object') {
Object.assign(this._config, config);
Toast.trigger('configChange');
}
return this;
},
@@ -4073,6 +4235,7 @@ const meToast = {
const keys = Object.keys(this._config);
keys.forEach(k => delete this._config[k]);
Object.assign(this._config, DEFAULTS);
Toast.trigger('configChange');
return this;
},
use(plugin, options = {}) {
@@ -4113,6 +4276,7 @@ const meToast = {
if (this._destroyed)
return;
this._destroyed = true;
Toast.trigger('beforeDestroy');
this.dismiss();
// 清理子模块
if (this.plugins && typeof this.plugins.destroy === 'function') {
@@ -4129,20 +4293,26 @@ const meToast = {
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
this.i18n.clearLocaleListeners();
}
if (this.animations && typeof this.animations.cancelAll === 'function') {
this.animations.cancelAll();
if (this.animations && typeof this.animations.destroy === 'function') {
this.animations.destroy();
}
// 清理 DOM
// 清理 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);
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
const el = document.getElementById(id);
if (el && el.parentNode)
el.parentNode.removeChild(el);
});
}
this._toasts.clear();
_containerCache.clear();
Toast._registry.clear();
// afterDestroy 必须在清理 hooks 之前触发
Toast.trigger('afterDestroy');
Toast._hooks.clear();
},
// 子模块引用(由 index.ts 注入实际实现)
animations: null,
@@ -4167,77 +4337,13 @@ Toast.setCallbacks({
},
});
/**
* MetonaToast Animations — 动画管理
* @module animations
* @version 0.2.1
*/
// 动画缓存
const animationMap = new Map();
// 注册默认动画
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
/**
* 动画工具函数
*/
const animationUtils = {
register(name, config) {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name) {
animationMap.delete(name);
},
get(name) {
return animationMap.get(name) || null;
},
getAnimationNames() {
return Array.from(animationMap.keys());
},
getActiveCount() {
return animationMap.size;
},
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy() {
animationMap.clear();
},
};
/**
* MetonaToast — 轻量级Toast通知库
* @module metona-toast
* @version 0.2.1
* @version 0.3.0
* @author thzxx
* @license MIT
*/
// 版本信息
const VERSION = '0.2.1';
/**
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
*/
@@ -4257,6 +4363,13 @@ if (typeof themeUtils.initTheme === 'function') {
if (typeof i18nUtils.initI18n === 'function') {
i18nUtils.initI18n();
}
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
if (typeof themeUtils.addThemeListener === 'function') {
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
}
if (typeof i18nUtils.addLocaleListener === 'function') {
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
}
// 浏览器环境全局注册
if (typeof window !== 'undefined') {
window.MeToast = enhancedMeToast;
+1 -1
View File
File diff suppressed because one or more lines are too long
+18 -6
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast — 核心类型定义
* @module types
* @version 0.2.1
* @version 0.3.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';
@@ -193,8 +193,8 @@ interface StatusInfo {
animations: number;
}
interface AnimationConfig {
enter: Record<string, string>;
leave: Record<string, string>;
enter: Record<string, string | number>;
leave: Record<string, string | number>;
duration: number;
easing: string;
name?: string;
@@ -417,7 +417,7 @@ declare global {
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.2.1
* @version 0.3.0
*/
/**
@@ -425,6 +425,7 @@ declare global {
*/
declare class Toast implements ToastInstance {
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>>;
static _registry: Map<string, Toast>;
private static _onError;
private static _removeToast;
/**
@@ -438,8 +439,9 @@ declare class Toast implements ToastInstance {
static off(name: string, fn: (toast: Toast) => boolean | void): void;
/**
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
*/
static trigger(name: string, toast: Toast): boolean;
static trigger(name: string, toast?: Toast | null): boolean;
id: string;
type: string;
title: string;
@@ -486,7 +488,17 @@ declare class Toast implements ToastInstance {
_destroy(): void;
}
declare const VERSION = "0.2.1";
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.3.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
declare const VERSION = "0.3.0";
/**
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
*/
+212 -99
View File
@@ -1,7 +1,7 @@
/**
* MetonaToast Utils — 工具函数
* @module utils
* @version 0.2.1
* @version 0.3.0
*/
/**
* 生成唯一ID
@@ -38,7 +38,7 @@ const prefersDark = () => {
/**
* MetonaToast Icons — 图标SVG定义
* @module icons
* @version 0.2.1
* @version 0.3.0
* @description 107 个内置 SVG 图标
*/
const ICONS = {
@@ -535,7 +535,7 @@ const ICONS = {
/**
* MetonaToast Locales — 国际化翻译数据
* @module locales
* @version 0.2.1
* @version 0.3.0
* @description 内置 zh-CN / en-US 完整翻译
*/
const LOCALES = {
@@ -1022,8 +1022,12 @@ const LOCALES = {
/**
* MetonaToast Constants — 常量定义
* @module constants
* @version 0.2.1
* @version 0.3.0
*/
/**
* 版本号 — 唯一来源,发布时只需修改此处
*/
const VERSION = '0.3.0';
/**
* 默认配置
*/
@@ -1281,11 +1285,15 @@ const THEMES = {
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
},
};
/**
* 动画类型
*/
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
/**
* MetonaToast Styles — 样式管理
* @module styles
* @version 0.2.1
* @version 0.3.0
*/
// 样式缓存
let styleElement = null;
@@ -1739,7 +1747,7 @@ const injectStyles = () => {
/**
* MetonaToast Themes — 主题管理
* @module themes
* @version 0.2.1
* @version 0.3.0
*/
// 当前主题状态
let currentTheme = 'auto';
@@ -2083,7 +2091,7 @@ const themeUtils = {
/**
* MetonaToast i18n — 国际化管理
* @module i18n
* @version 0.2.1
* @version 0.3.0
*/
// 当前语言状态
let currentLocale = 'zh-CN';
@@ -2639,10 +2647,126 @@ const i18nUtils = {
},
});
/**
* MetonaToast Animations — 动画管理
* @module animations
* @version 0.2.1
*/
// 动画缓存
const animationMap = new Map();
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
// 动态注入的自定义动画样式(惰性创建)
let animStyleElement = null;
const injectedAnimations = new Set();
/**
* 将 CSS 属性对象转换为内联 CSS 字符串
*/
const toCss = (props) => Object.entries(props)
.map(([key, value]) => `${key}:${value}`)
.join(';');
/**
* 为自定义动画生成入场 keyframes + 类规则
*/
const buildAnimationCSS = (name, config) => {
const enter = toCss(config.enter);
return `
@keyframes met-${name}-in {
from { ${enter}; }
}
.met-anim-${name}.met-toast { opacity: 0; }
.met-anim-${name}.met-toast.met-show {
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
}
`;
};
/**
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
*/
const ensureAnimationCSS = (name) => {
if (typeof document === 'undefined')
return;
if (BUILTIN_ANIMATIONS.has(name))
return;
const config = animationMap.get(name);
if (!config || injectedAnimations.has(name))
return;
if (!animStyleElement) {
animStyleElement = document.getElementById('metona-toast-anim-styles');
if (!animStyleElement) {
animStyleElement = document.createElement('style');
animStyleElement.id = 'metona-toast-anim-styles';
document.head.appendChild(animStyleElement);
}
}
animStyleElement.textContent += buildAnimationCSS(name, config);
injectedAnimations.add(name);
};
/**
* 检查动画是否存在(内置或自定义)
*/
const hasAnimation = (name) => animationMap.has(name);
// 注册默认动画
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
/**
* 动画工具函数
*/
const animationUtils = {
register(name, config) {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name) {
animationMap.delete(name);
},
get(name) {
return animationMap.get(name) || null;
},
getAnimationNames() {
return Array.from(animationMap.keys());
},
getActiveCount() {
return animationMap.size;
},
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy() {
animationMap.clear();
injectedAnimations.clear();
animStyleElement = null;
},
};
/**
* MetonaToast Toast — Toast 类
* @module toast
* @version 0.2.1
* @version 0.3.0
*/
/**
* Toast 类 — 核心通知组件
@@ -2670,8 +2794,9 @@ class Toast {
}
/**
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
*/
static trigger(name, toast) {
static trigger(name, toast = null) {
const list = this._hooks.get(name);
if (!list || list.length === 0)
return true;
@@ -2686,7 +2811,7 @@ class Toast {
console.error('Hook error:', name, e);
if (Toast._onError) {
try {
Toast._onError({ hook: name, error: e, toast });
Toast._onError({ hook: name, error: e, toast: toast });
}
catch (_e) { /* noop */ }
}
@@ -2741,6 +2866,7 @@ class Toast {
}
}
injectStyles();
ensureAnimationCSS(this.config.animation || 'slide');
const container = this._getContainer();
const { theme, c, t } = this._palette();
this._limitToasts(container);
@@ -2760,6 +2886,7 @@ class Toast {
else {
container.appendChild(el);
}
Toast._registry.set(this.id, this);
this._bindEvents(el);
this._startTimer();
// 进入动画
@@ -2827,18 +2954,22 @@ class Toast {
_limitToasts(container) {
const max = this.config.max || 6;
const list = Array.from(container.querySelectorAll('.met-toast'));
if (list.length >= max) {
const first = list[0];
const id = first?.dataset.id;
if (id && Toast._removeToast) {
while (list.length >= max) {
const el = list.shift();
const id = el?.dataset.id;
if (!id)
continue;
const existing = Toast._registry.get(id);
if (existing) {
existing.close(true);
}
else if (Toast._removeToast) {
Toast._removeToast(id);
}
}
}
_buildClassName(theme) {
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
return [
'met-toast',
`met-${this.type}`,
@@ -2915,6 +3046,7 @@ class Toast {
this.close();
return;
}
Toast.trigger('click', this);
if (this.config.closeOnClick && !target.closest('.met-close')) {
if (typeof this.config.onClick === 'function') {
try {
@@ -2931,8 +3063,14 @@ class Toast {
el.addEventListener('click', eventHandler);
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
const mouseEnter = () => this._pause();
const mouseLeave = () => this._resume();
const mouseEnter = () => {
Toast.trigger('hover', this);
this._pause();
};
const mouseLeave = () => {
Toast.trigger('hover', this);
this._resume();
};
el.addEventListener('mouseenter', mouseEnter);
el.addEventListener('mouseleave', mouseLeave);
this._cleanups.push(() => {
@@ -2940,6 +3078,15 @@ class Toast {
el.removeEventListener('mouseleave', mouseLeave);
});
}
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
const animStart = () => Toast.trigger('animationStart', this);
const animEnd = () => Toast.trigger('animationEnd', this);
el.addEventListener('animationstart', animStart);
el.addEventListener('animationend', animEnd);
this._cleanups.push(() => {
el.removeEventListener('animationstart', animStart);
el.removeEventListener('animationend', animEnd);
});
if (this.config.draggable) {
this._bindDrag(el);
}
@@ -2955,6 +3102,7 @@ class Toast {
el.setPointerCapture(e.pointerId);
el.style.transition = 'none';
this._pause();
Toast.trigger('dragStart', this);
};
const move = (e) => {
if (!dragging)
@@ -2981,6 +3129,7 @@ class Toast {
this._resume();
}
dx = dy = 0;
Toast.trigger('dragEnd', this);
};
el.addEventListener('pointerdown', down);
el.addEventListener('pointermove', move);
@@ -2999,6 +3148,7 @@ class Toast {
if (!resuming) {
this.startedAt = Date.now();
this.remaining = this.config.duration || 0;
Toast.trigger('progressStart', this);
}
const tick = () => {
if (this.paused || this.closing)
@@ -3013,6 +3163,7 @@ class Toast {
this.barEl.style.transform = t;
}
if (this.remaining <= 0) {
Toast.trigger('progressEnd', this);
this.close();
return;
}
@@ -3046,7 +3197,9 @@ class Toast {
this._startTimer(true);
}
update(partial) {
Toast.trigger('beforeUpdate', this);
// beforeUpdate 钩子 — 返回 false 可阻止更新
if (!Toast.trigger('beforeUpdate', this))
return this;
const typeChanged = partial.type && partial.type !== this.type;
if (partial.type)
this.type = partial.type;
@@ -3143,14 +3296,17 @@ class Toast {
this.el.parentNode.removeChild(this.el);
}
this.el = null;
Toast._registry.delete(this.id);
if (Toast._removeToast)
Toast._removeToast(this.id);
}
close(immediate = false) {
if (this.closing)
return;
// beforeClose 钩子 — 返回 false 可阻止关闭
if (!Toast.trigger('beforeClose', this))
return;
this.closing = true;
Toast.trigger('beforeClose', this);
if (this.rafId !== null)
cancelAnimationFrame(this.rafId);
this._cleanups.forEach(fn => { try {
@@ -3206,12 +3362,15 @@ class Toast {
}
}
Toast.trigger('afterClose', this);
Toast._registry.delete(this.id);
if (Toast._removeToast)
Toast._removeToast(this.id);
}
}
// 静态钩子系统
Toast._hooks = new Map();
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
Toast._registry = new Map();
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
Toast._onError = null;
Toast._removeToast = null;
@@ -3223,7 +3382,7 @@ const _containerCache = new Map();
/**
* MetonaToast Templates — HTML 模板辅助函数
* @module templates
* @version 0.2.1
* @version 0.3.0
* @description confirm / prompt / progress / action 的 DOM 模板
*/
/**
@@ -3295,7 +3454,7 @@ const actionHTML = (actions) => {
/**
* MetonaToast Plugins — 插件系统
* @module plugins
* @version 0.2.1
* @version 0.3.0
*/
/**
* 插件管理器
@@ -3515,7 +3674,7 @@ const defaultPluginManager = new PluginManager();
/**
* MetonaToast API — meToast 核心 API 对象
* @module api
* @version 0.2.1
* @version 0.3.0
*/
/**
* 参数标准化工具
@@ -3540,15 +3699,13 @@ const normalizeArgs = (args, defaultType = 'default') => {
message: '',
};
};
/** 版本号常量 */
const VERSION$1 = '0.2.1';
/**
* meToast API 对象
*/
const meToast = {
_toasts: new Map(),
_config: { ...DEFAULTS },
version: '0.2.1',
version: VERSION,
configure(opts) {
if (!opts || typeof opts !== 'object')
return this;
@@ -3559,6 +3716,7 @@ const meToast = {
if (opts.locale) {
setCurrentLocale(opts.locale);
}
Toast.trigger('configChange');
return this;
},
_emit(opts) {
@@ -4030,6 +4188,8 @@ const meToast = {
},
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
init(options = {}) {
Toast.trigger('beforeInit');
this._destroyed = false;
if (options.config) {
this.configure(options.config);
}
@@ -4044,11 +4204,12 @@ const meToast = {
this.use(p);
});
}
Toast.trigger('afterInit');
return this;
},
getStatus() {
return {
version: VERSION$1,
version: VERSION,
toasts: this._toasts.size,
theme: this.themes?.getCurrentTheme?.() || 'auto',
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
@@ -4062,6 +4223,7 @@ const meToast = {
updateConfig(config) {
if (config && typeof config === 'object') {
Object.assign(this._config, config);
Toast.trigger('configChange');
}
return this;
},
@@ -4069,6 +4231,7 @@ const meToast = {
const keys = Object.keys(this._config);
keys.forEach(k => delete this._config[k]);
Object.assign(this._config, DEFAULTS);
Toast.trigger('configChange');
return this;
},
use(plugin, options = {}) {
@@ -4109,6 +4272,7 @@ const meToast = {
if (this._destroyed)
return;
this._destroyed = true;
Toast.trigger('beforeDestroy');
this.dismiss();
// 清理子模块
if (this.plugins && typeof this.plugins.destroy === 'function') {
@@ -4125,20 +4289,26 @@ const meToast = {
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
this.i18n.clearLocaleListeners();
}
if (this.animations && typeof this.animations.cancelAll === 'function') {
this.animations.cancelAll();
if (this.animations && typeof this.animations.destroy === 'function') {
this.animations.destroy();
}
// 清理 DOM
// 清理 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);
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
const el = document.getElementById(id);
if (el && el.parentNode)
el.parentNode.removeChild(el);
});
}
this._toasts.clear();
_containerCache.clear();
Toast._registry.clear();
// afterDestroy 必须在清理 hooks 之前触发
Toast.trigger('afterDestroy');
Toast._hooks.clear();
},
// 子模块引用(由 index.ts 注入实际实现)
animations: null,
@@ -4163,77 +4333,13 @@ Toast.setCallbacks({
},
});
/**
* MetonaToast Animations — 动画管理
* @module animations
* @version 0.2.1
*/
// 动画缓存
const animationMap = new Map();
// 注册默认动画
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
/**
* 动画工具函数
*/
const animationUtils = {
register(name, config) {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name) {
animationMap.delete(name);
},
get(name) {
return animationMap.get(name) || null;
},
getAnimationNames() {
return Array.from(animationMap.keys());
},
getActiveCount() {
return animationMap.size;
},
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy() {
animationMap.clear();
},
};
/**
* MetonaToast — 轻量级Toast通知库
* @module metona-toast
* @version 0.2.1
* @version 0.3.0
* @author thzxx
* @license MIT
*/
// 版本信息
const VERSION = '0.2.1';
/**
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
*/
@@ -4253,6 +4359,13 @@ if (typeof themeUtils.initTheme === 'function') {
if (typeof i18nUtils.initI18n === 'function') {
i18nUtils.initI18n();
}
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
if (typeof themeUtils.addThemeListener === 'function') {
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
}
if (typeof i18nUtils.addLocaleListener === 'function') {
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
}
// 浏览器环境全局注册
if (typeof window !== 'undefined') {
window.MeToast = enhancedMeToast;
+1 -1
View File
File diff suppressed because one or more lines are too long
+212 -99
View File
@@ -7,7 +7,7 @@
/**
* MetonaToast Utils 工具函数
* @module utils
* @version 0.2.1
* @version 0.3.0
*/
/**
* 生成唯一ID
@@ -44,7 +44,7 @@
/**
* MetonaToast Icons 图标SVG定义
* @module icons
* @version 0.2.1
* @version 0.3.0
* @description 107 个内置 SVG 图标
*/
const ICONS = {
@@ -541,7 +541,7 @@
/**
* MetonaToast Locales 国际化翻译数据
* @module locales
* @version 0.2.1
* @version 0.3.0
* @description 内置 zh-CN / en-US 完整翻译
*/
const LOCALES = {
@@ -1028,8 +1028,12 @@
/**
* MetonaToast Constants 常量定义
* @module constants
* @version 0.2.1
* @version 0.3.0
*/
/**
* 版本号 唯一来源发布时只需修改此处
*/
const VERSION = '0.3.0';
/**
* 默认配置
*/
@@ -1287,11 +1291,15 @@
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
},
};
/**
* 动画类型
*/
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
/**
* MetonaToast Styles 样式管理
* @module styles
* @version 0.2.1
* @version 0.3.0
*/
// 样式缓存
let styleElement = null;
@@ -1745,7 +1753,7 @@
/**
* MetonaToast Themes 主题管理
* @module themes
* @version 0.2.1
* @version 0.3.0
*/
// 当前主题状态
let currentTheme = 'auto';
@@ -2089,7 +2097,7 @@
/**
* MetonaToast i18n 国际化管理
* @module i18n
* @version 0.2.1
* @version 0.3.0
*/
// 当前语言状态
let currentLocale = 'zh-CN';
@@ -2645,10 +2653,126 @@
},
});
/**
* MetonaToast Animations 动画管理
* @module animations
* @version 0.2.1
*/
// 动画缓存
const animationMap = new Map();
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
// 动态注入的自定义动画样式(惰性创建)
let animStyleElement = null;
const injectedAnimations = new Set();
/**
* CSS 属性对象转换为内联 CSS 字符串
*/
const toCss = (props) => Object.entries(props)
.map(([key, value]) => `${key}:${value}`)
.join(';');
/**
* 为自定义动画生成入场 keyframes + 类规则
*/
const buildAnimationCSS = (name, config) => {
const enter = toCss(config.enter);
return `
@keyframes met-${name}-in {
from { ${enter}; }
}
.met-anim-${name}.met-toast { opacity: 0; }
.met-anim-${name}.met-toast.met-show {
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
}
`;
};
/**
* 确保自定义动画的 CSS 已注入文档幂等内置动画自动跳过
*/
const ensureAnimationCSS = (name) => {
if (typeof document === 'undefined')
return;
if (BUILTIN_ANIMATIONS.has(name))
return;
const config = animationMap.get(name);
if (!config || injectedAnimations.has(name))
return;
if (!animStyleElement) {
animStyleElement = document.getElementById('metona-toast-anim-styles');
if (!animStyleElement) {
animStyleElement = document.createElement('style');
animStyleElement.id = 'metona-toast-anim-styles';
document.head.appendChild(animStyleElement);
}
}
animStyleElement.textContent += buildAnimationCSS(name, config);
injectedAnimations.add(name);
};
/**
* 检查动画是否存在内置或自定义
*/
const hasAnimation = (name) => animationMap.has(name);
// 注册默认动画
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
/**
* 动画工具函数
*/
const animationUtils = {
register(name, config) {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name) {
animationMap.delete(name);
},
get(name) {
return animationMap.get(name) || null;
},
getAnimationNames() {
return Array.from(animationMap.keys());
},
getActiveCount() {
return animationMap.size;
},
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy() {
animationMap.clear();
injectedAnimations.clear();
animStyleElement = null;
},
};
/**
* MetonaToast Toast Toast
* @module toast
* @version 0.2.1
* @version 0.3.0
*/
/**
* Toast 核心通知组件
@@ -2676,8 +2800,9 @@
}
/**
* 触发钩子 如果任意钩子返回 false 则整体返回 false
* toast 参数可选全局事件configChange/themeChange 不关联具体 toast
*/
static trigger(name, toast) {
static trigger(name, toast = null) {
const list = this._hooks.get(name);
if (!list || list.length === 0)
return true;
@@ -2692,7 +2817,7 @@
console.error('Hook error:', name, e);
if (Toast._onError) {
try {
Toast._onError({ hook: name, error: e, toast });
Toast._onError({ hook: name, error: e, toast: toast });
}
catch (_e) { /* noop */ }
}
@@ -2747,6 +2872,7 @@
}
}
injectStyles();
ensureAnimationCSS(this.config.animation || 'slide');
const container = this._getContainer();
const { theme, c, t } = this._palette();
this._limitToasts(container);
@@ -2766,6 +2892,7 @@
else {
container.appendChild(el);
}
Toast._registry.set(this.id, this);
this._bindEvents(el);
this._startTimer();
// 进入动画
@@ -2833,18 +2960,22 @@
_limitToasts(container) {
const max = this.config.max || 6;
const list = Array.from(container.querySelectorAll('.met-toast'));
if (list.length >= max) {
const first = list[0];
const id = first?.dataset.id;
if (id && Toast._removeToast) {
while (list.length >= max) {
const el = list.shift();
const id = el?.dataset.id;
if (!id)
continue;
const existing = Toast._registry.get(id);
if (existing) {
existing.close(true);
}
else if (Toast._removeToast) {
Toast._removeToast(id);
}
}
}
_buildClassName(theme) {
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
return [
'met-toast',
`met-${this.type}`,
@@ -2921,6 +3052,7 @@
this.close();
return;
}
Toast.trigger('click', this);
if (this.config.closeOnClick && !target.closest('.met-close')) {
if (typeof this.config.onClick === 'function') {
try {
@@ -2937,8 +3069,14 @@
el.addEventListener('click', eventHandler);
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
const mouseEnter = () => this._pause();
const mouseLeave = () => this._resume();
const mouseEnter = () => {
Toast.trigger('hover', this);
this._pause();
};
const mouseLeave = () => {
Toast.trigger('hover', this);
this._resume();
};
el.addEventListener('mouseenter', mouseEnter);
el.addEventListener('mouseleave', mouseLeave);
this._cleanups.push(() => {
@@ -2946,6 +3084,15 @@
el.removeEventListener('mouseleave', mouseLeave);
});
}
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
const animStart = () => Toast.trigger('animationStart', this);
const animEnd = () => Toast.trigger('animationEnd', this);
el.addEventListener('animationstart', animStart);
el.addEventListener('animationend', animEnd);
this._cleanups.push(() => {
el.removeEventListener('animationstart', animStart);
el.removeEventListener('animationend', animEnd);
});
if (this.config.draggable) {
this._bindDrag(el);
}
@@ -2961,6 +3108,7 @@
el.setPointerCapture(e.pointerId);
el.style.transition = 'none';
this._pause();
Toast.trigger('dragStart', this);
};
const move = (e) => {
if (!dragging)
@@ -2987,6 +3135,7 @@
this._resume();
}
dx = dy = 0;
Toast.trigger('dragEnd', this);
};
el.addEventListener('pointerdown', down);
el.addEventListener('pointermove', move);
@@ -3005,6 +3154,7 @@
if (!resuming) {
this.startedAt = Date.now();
this.remaining = this.config.duration || 0;
Toast.trigger('progressStart', this);
}
const tick = () => {
if (this.paused || this.closing)
@@ -3019,6 +3169,7 @@
this.barEl.style.transform = t;
}
if (this.remaining <= 0) {
Toast.trigger('progressEnd', this);
this.close();
return;
}
@@ -3052,7 +3203,9 @@
this._startTimer(true);
}
update(partial) {
Toast.trigger('beforeUpdate', this);
// beforeUpdate 钩子 — 返回 false 可阻止更新
if (!Toast.trigger('beforeUpdate', this))
return this;
const typeChanged = partial.type && partial.type !== this.type;
if (partial.type)
this.type = partial.type;
@@ -3149,14 +3302,17 @@
this.el.parentNode.removeChild(this.el);
}
this.el = null;
Toast._registry.delete(this.id);
if (Toast._removeToast)
Toast._removeToast(this.id);
}
close(immediate = false) {
if (this.closing)
return;
// beforeClose 钩子 — 返回 false 可阻止关闭
if (!Toast.trigger('beforeClose', this))
return;
this.closing = true;
Toast.trigger('beforeClose', this);
if (this.rafId !== null)
cancelAnimationFrame(this.rafId);
this._cleanups.forEach(fn => { try {
@@ -3212,12 +3368,15 @@
}
}
Toast.trigger('afterClose', this);
Toast._registry.delete(this.id);
if (Toast._removeToast)
Toast._removeToast(this.id);
}
}
// 静态钩子系统
Toast._hooks = new Map();
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
Toast._registry = new Map();
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
Toast._onError = null;
Toast._removeToast = null;
@@ -3229,7 +3388,7 @@
/**
* MetonaToast Templates HTML 模板辅助函数
* @module templates
* @version 0.2.1
* @version 0.3.0
* @description confirm / prompt / progress / action DOM 模板
*/
/**
@@ -3301,7 +3460,7 @@
/**
* MetonaToast Plugins 插件系统
* @module plugins
* @version 0.2.1
* @version 0.3.0
*/
/**
* 插件管理器
@@ -3521,7 +3680,7 @@
/**
* MetonaToast API meToast 核心 API 对象
* @module api
* @version 0.2.1
* @version 0.3.0
*/
/**
* 参数标准化工具
@@ -3546,15 +3705,13 @@
message: '',
};
};
/** 版本号常量 */
const VERSION$1 = '0.2.1';
/**
* meToast API 对象
*/
const meToast = {
_toasts: new Map(),
_config: { ...DEFAULTS },
version: '0.2.1',
version: VERSION,
configure(opts) {
if (!opts || typeof opts !== 'object')
return this;
@@ -3565,6 +3722,7 @@
if (opts.locale) {
setCurrentLocale(opts.locale);
}
Toast.trigger('configChange');
return this;
},
_emit(opts) {
@@ -4036,6 +4194,8 @@
},
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
init(options = {}) {
Toast.trigger('beforeInit');
this._destroyed = false;
if (options.config) {
this.configure(options.config);
}
@@ -4050,11 +4210,12 @@
this.use(p);
});
}
Toast.trigger('afterInit');
return this;
},
getStatus() {
return {
version: VERSION$1,
version: VERSION,
toasts: this._toasts.size,
theme: this.themes?.getCurrentTheme?.() || 'auto',
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
@@ -4068,6 +4229,7 @@
updateConfig(config) {
if (config && typeof config === 'object') {
Object.assign(this._config, config);
Toast.trigger('configChange');
}
return this;
},
@@ -4075,6 +4237,7 @@
const keys = Object.keys(this._config);
keys.forEach(k => delete this._config[k]);
Object.assign(this._config, DEFAULTS);
Toast.trigger('configChange');
return this;
},
use(plugin, options = {}) {
@@ -4115,6 +4278,7 @@
if (this._destroyed)
return;
this._destroyed = true;
Toast.trigger('beforeDestroy');
this.dismiss();
// 清理子模块
if (this.plugins && typeof this.plugins.destroy === 'function') {
@@ -4131,20 +4295,26 @@
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
this.i18n.clearLocaleListeners();
}
if (this.animations && typeof this.animations.cancelAll === 'function') {
this.animations.cancelAll();
if (this.animations && typeof this.animations.destroy === 'function') {
this.animations.destroy();
}
// 清理 DOM
// 清理 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);
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
const el = document.getElementById(id);
if (el && el.parentNode)
el.parentNode.removeChild(el);
});
}
this._toasts.clear();
_containerCache.clear();
Toast._registry.clear();
// afterDestroy 必须在清理 hooks 之前触发
Toast.trigger('afterDestroy');
Toast._hooks.clear();
},
// 子模块引用(由 index.ts 注入实际实现)
animations: null,
@@ -4169,77 +4339,13 @@
},
});
/**
* MetonaToast Animations 动画管理
* @module animations
* @version 0.2.1
*/
// 动画缓存
const animationMap = new Map();
// 注册默认动画
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
/**
* 动画工具函数
*/
const animationUtils = {
register(name, config) {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name) {
animationMap.delete(name);
},
get(name) {
return animationMap.get(name) || null;
},
getAnimationNames() {
return Array.from(animationMap.keys());
},
getActiveCount() {
return animationMap.size;
},
cancelAll() {
// CSS动画由浏览器原生管理,无需手动取消
},
reset() {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy() {
animationMap.clear();
},
};
/**
* MetonaToast 轻量级Toast通知库
* @module metona-toast
* @version 0.2.1
* @version 0.3.0
* @author thzxx
* @license MIT
*/
// 版本信息
const VERSION = '0.2.1';
/**
* 增强的 MeToast 对象 meToast 基础上注入子模块
*/
@@ -4259,6 +4365,13 @@
if (typeof i18nUtils.initI18n === 'function') {
i18nUtils.initI18n();
}
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
if (typeof themeUtils.addThemeListener === 'function') {
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
}
if (typeof i18nUtils.addLocaleListener === 'function') {
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
}
// 浏览器环境全局注册
if (typeof window !== 'undefined') {
window.MeToast = enhancedMeToast;
+1 -1
View File
File diff suppressed because one or more lines are too long
+1 -1
View File
File diff suppressed because one or more lines are too long