- 覆盖率 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)
762 lines
23 KiB
TypeScript
762 lines
23 KiB
TypeScript
/**
|
|
* MetonaToast API — meToast 核心 API 对象
|
|
* @module api
|
|
* @version 0.5.0
|
|
*/
|
|
|
|
import { Toast, _containerCache } from './toast.js';
|
|
import { DEFAULTS, VERSION } from './constants.js';
|
|
import { t, setCurrentLocale } from './i18n.js';
|
|
import { applyTheme } from './themes.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, Plugin,
|
|
StatusInfo, InitOptions,
|
|
} from './types.js';
|
|
|
|
/**
|
|
* 参数标准化工具
|
|
*/
|
|
const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions => {
|
|
const [first, second] = args;
|
|
|
|
if (typeof first === 'string') {
|
|
return {
|
|
...((second as Record<string, unknown>) || {}),
|
|
type: (second as Record<string, unknown>)?.type as string || defaultType,
|
|
message: first,
|
|
};
|
|
}
|
|
|
|
if (first && typeof first === 'object') {
|
|
return {
|
|
...(first as Record<string, unknown>),
|
|
type: (first as Record<string, unknown>).type as string || defaultType,
|
|
} as ToastOptions;
|
|
}
|
|
|
|
return {
|
|
type: defaultType,
|
|
message: '',
|
|
};
|
|
};
|
|
|
|
/**
|
|
* meToast API 对象
|
|
*/
|
|
const meToast: MeToast = {
|
|
_toasts: new Map<string, ToastInstance>(),
|
|
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
|
version: VERSION,
|
|
|
|
configure(opts: Partial<ToastConfig>): MeToast {
|
|
if (!opts || typeof opts !== 'object') return this;
|
|
Object.assign(this._config, opts);
|
|
|
|
if (opts.theme) {
|
|
applyTheme(opts.theme);
|
|
}
|
|
|
|
if (opts.locale) {
|
|
setCurrentLocale(opts.locale);
|
|
}
|
|
|
|
Toast.trigger('configChange');
|
|
return this;
|
|
},
|
|
|
|
_emit(opts: ToastOptions): Toast {
|
|
const merged: ToastOptions = { ...this._config, ...opts };
|
|
if (merged.content && !merged.message) merged.message = merged.content;
|
|
|
|
const t = new Toast(merged);
|
|
t.create();
|
|
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
|
if (t.el !== null) {
|
|
this._toasts.set(t.id, t);
|
|
}
|
|
|
|
return t;
|
|
},
|
|
|
|
_remove(id: string): void {
|
|
this._toasts.delete(id);
|
|
},
|
|
|
|
find(id: string): ToastInstance | undefined {
|
|
if (!id) return undefined;
|
|
return this._toasts.get(id);
|
|
},
|
|
|
|
show(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'default');
|
|
return this._emit(normalized);
|
|
},
|
|
|
|
success(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'success');
|
|
return this._emit(normalized);
|
|
},
|
|
|
|
error(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'error');
|
|
return this._emit(normalized);
|
|
},
|
|
|
|
warning(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'warning');
|
|
return this._emit(normalized);
|
|
},
|
|
|
|
info(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
|
return this._emit(normalized);
|
|
},
|
|
|
|
loading(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): LoadingControl {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'loading');
|
|
normalized.duration = 0;
|
|
normalized.closeButton = false;
|
|
normalized.showProgress = false;
|
|
|
|
const toast = this._emit(normalized);
|
|
|
|
return {
|
|
id: toast.id,
|
|
success: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'success', msg || t('success'), o),
|
|
error: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'error', msg || t('error'), o),
|
|
info: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'info', msg || t('info'), o),
|
|
warning: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'warning', msg || t('warning'), o),
|
|
update: (p: Partial<ToastOptions>) => { toast.update(p); return this; },
|
|
dismiss: () => toast.close(),
|
|
};
|
|
},
|
|
|
|
promise<T>(promise: Promise<T>, opts: PromiseOptions = {}): Promise<T> {
|
|
if (!promise || typeof (promise as unknown as { then?: unknown }).then !== 'function') {
|
|
console.error('MeToast.promise: first argument must be a Promise');
|
|
return Promise.reject(new Error('Invalid promise'));
|
|
}
|
|
|
|
const loadingMsg = opts.loading || t('loading');
|
|
const successMsg = opts.success || t('success');
|
|
const errorMsg = opts.error || t('error');
|
|
|
|
const ctrl = this.loading(loadingMsg);
|
|
|
|
return Promise.resolve(promise)
|
|
.then((data: T) => {
|
|
ctrl.success(successMsg);
|
|
return data;
|
|
})
|
|
.catch((err: unknown) => {
|
|
ctrl.error(errorMsg);
|
|
throw err;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
|
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
|
*/
|
|
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
|
const old = this._toasts.get(loadingToast.id);
|
|
if (!old) return null;
|
|
|
|
old.update({
|
|
...opts,
|
|
type,
|
|
message,
|
|
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
|
} as ToastOptions);
|
|
|
|
return old;
|
|
},
|
|
|
|
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
|
if (typeof message !== 'string') {
|
|
console.error('MeToast.confirm: message must be a string');
|
|
return Promise.resolve(false);
|
|
}
|
|
|
|
return new Promise<boolean>((resolve) => {
|
|
let resolved = false;
|
|
const safeResolve = (val: boolean) => {
|
|
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
|
|
};
|
|
const safetyTimeout = setTimeout(() => safeResolve(false), 10000);
|
|
|
|
const toast = this._emit({
|
|
...opts,
|
|
type: opts.type || 'warning',
|
|
message,
|
|
duration: 0,
|
|
closeButton: false,
|
|
closeOnClick: false,
|
|
draggable: false,
|
|
html: confirmHTML(opts),
|
|
});
|
|
|
|
setTimeout(() => {
|
|
const confirmBtn = toast.el?.querySelector('.met-confirm-btn') as HTMLElement | null;
|
|
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
|
|
|
|
if (confirmBtn) {
|
|
confirmBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(true);
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(false);
|
|
});
|
|
}
|
|
}, 0);
|
|
});
|
|
},
|
|
|
|
prompt(message: string, opts: PromptOptions = {}): Promise<string | null> {
|
|
if (typeof message !== 'string') {
|
|
console.error('MeToast.prompt: message must be a string');
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
return new Promise<string | null>((resolve) => {
|
|
let resolved = false;
|
|
const safeResolve = (val: string | null) => {
|
|
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
|
|
};
|
|
const safetyTimeout = setTimeout(() => safeResolve(null), 10000);
|
|
|
|
const toast = this._emit({
|
|
...opts,
|
|
type: opts.type || 'info',
|
|
message,
|
|
duration: 0,
|
|
closeButton: false,
|
|
closeOnClick: false,
|
|
draggable: false,
|
|
html: promptHTML(opts),
|
|
});
|
|
|
|
setTimeout(() => {
|
|
const input = toast.el?.querySelector('.met-input') as HTMLInputElement | null;
|
|
const submitBtn = toast.el?.querySelector('.met-submit-btn') as HTMLElement | null;
|
|
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
|
|
|
|
if (input) {
|
|
input.focus();
|
|
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
if (e.key === 'Enter') {
|
|
toast.close();
|
|
safeResolve(input.value);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (submitBtn) {
|
|
submitBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(input?.value || null);
|
|
});
|
|
}
|
|
|
|
if (cancelBtn) {
|
|
cancelBtn.addEventListener('click', () => {
|
|
toast.close();
|
|
safeResolve(null);
|
|
});
|
|
}
|
|
}, 0);
|
|
});
|
|
},
|
|
|
|
progress(messageOrOpts: string | ToastOptions, opts: ProgressOptions = {}): ProgressControl {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
|
normalized.duration = 0;
|
|
normalized.closeButton = false;
|
|
normalized.showProgress = false;
|
|
|
|
const toast = this._emit({
|
|
...normalized,
|
|
html: progressHTML(opts),
|
|
});
|
|
|
|
return {
|
|
id: toast.id,
|
|
|
|
setProgress(percent: number) {
|
|
const fill = toast.el?.querySelector('.met-progress-fill') as HTMLElement | null;
|
|
const text = toast.el?.querySelector('.met-progress-text') as HTMLElement | null;
|
|
|
|
if (fill) {
|
|
fill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
|
}
|
|
if (text) {
|
|
text.textContent = `${Math.round(percent)}%`;
|
|
}
|
|
},
|
|
|
|
complete(message?: string) {
|
|
this.setProgress(100);
|
|
setTimeout(() => {
|
|
toast.update({
|
|
type: 'success',
|
|
message: message || t('success'),
|
|
html: '',
|
|
});
|
|
setTimeout(() => toast.close(), 1000);
|
|
}, 300);
|
|
},
|
|
|
|
error(message?: string) {
|
|
toast.update({
|
|
type: 'error',
|
|
message: message || t('error'),
|
|
html: '',
|
|
});
|
|
setTimeout(() => toast.close(), 2000);
|
|
},
|
|
|
|
dismiss() {
|
|
toast.close();
|
|
},
|
|
};
|
|
},
|
|
|
|
countdown(message: string, seconds = 10, opts: CountdownOptions = {}): CountdownControl {
|
|
if (typeof message !== 'string') {
|
|
console.error('MeToast.countdown: message must be a string');
|
|
return { id: '', cancel: () => {}, pause: () => {}, resume: () => {} };
|
|
}
|
|
|
|
let remaining = Math.max(1, parseInt(String(seconds)) || 10);
|
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
const toast = this._emit({
|
|
...opts,
|
|
type: opts.type || 'warning',
|
|
message: message.replace(/\{seconds\}/g, String(remaining)),
|
|
duration: 0,
|
|
closeButton: true,
|
|
showProgress: false,
|
|
});
|
|
|
|
const tick = (): void => {
|
|
remaining--;
|
|
|
|
if (remaining <= 0) {
|
|
if (timer !== null) clearInterval(timer);
|
|
toast.close();
|
|
|
|
if (typeof opts.onComplete === 'function') {
|
|
try { opts.onComplete(); } catch (e) { console.error('countdown onComplete error:', e); }
|
|
}
|
|
return;
|
|
}
|
|
|
|
toast.update({
|
|
message: message.replace(/\{seconds\}/g, String(remaining)),
|
|
});
|
|
};
|
|
|
|
timer = setInterval(tick, 1000);
|
|
|
|
return {
|
|
id: toast.id,
|
|
cancel() { if (timer !== null) clearInterval(timer); toast.close(); },
|
|
pause() { if (timer !== null) clearInterval(timer); },
|
|
resume() { if (timer !== null) clearInterval(timer); timer = setInterval(tick, 1000); },
|
|
};
|
|
},
|
|
|
|
action(messageOrOpts: string | ToastOptions, actions: ActionButton[] = [], opts: ToastOptions = {}): ActionControl {
|
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
|
normalized.duration = opts.duration ?? 0;
|
|
normalized.closeButton = opts.closeButton ?? true;
|
|
|
|
const toast = this._emit({
|
|
...normalized,
|
|
html: (normalized.html || '') + actionHTML(actions),
|
|
});
|
|
|
|
if (Array.isArray(actions)) {
|
|
setTimeout(() => {
|
|
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', (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();
|
|
});
|
|
}
|
|
});
|
|
}, 0);
|
|
}
|
|
|
|
return { id: toast.id, toast, dismiss: () => toast.close() };
|
|
},
|
|
|
|
queue(messages: Array<string | ToastOptions>, opts: QueueOptions = {}): QueueControl {
|
|
if (!Array.isArray(messages)) {
|
|
console.error('MeToast.queue: messages must be an array');
|
|
const p = Promise.resolve();
|
|
return { then: (fn) => p.then(fn), catch: (rj) => p.catch(rj), cancel: () => {} };
|
|
}
|
|
|
|
let cancelled = false;
|
|
const promise = new Promise<void>((resolve) => {
|
|
let index = 0;
|
|
const delay = opts.delay || 1000;
|
|
const userOnClose = opts.onClose;
|
|
|
|
const showNext = (): void => {
|
|
if (cancelled || index >= messages.length) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const message = messages[index];
|
|
index++;
|
|
|
|
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
|
|
const msgObj: ToastOptions = (typeof message === 'object' && message !== null) ? message as ToastOptions : {};
|
|
const msgOnClose = msgObj.onClose;
|
|
|
|
this._emit({
|
|
...opts,
|
|
...msgObj,
|
|
message: msg,
|
|
duration: msgObj.duration || opts.duration || 3000,
|
|
onClose: (toast: ToastInstance) => {
|
|
if (typeof msgOnClose === 'function') {
|
|
try { msgOnClose(toast); } catch (_e) { /* noop */ }
|
|
}
|
|
if (typeof userOnClose === 'function') {
|
|
try { userOnClose(toast); } catch (_e) { /* noop */ }
|
|
}
|
|
setTimeout(showNext, delay);
|
|
},
|
|
} as ToastOptions);
|
|
};
|
|
|
|
showNext();
|
|
});
|
|
|
|
return {
|
|
then: (fn, rj) => promise.then(fn, rj),
|
|
catch: (rj) => promise.catch(rj),
|
|
cancel: () => { cancelled = true; },
|
|
};
|
|
},
|
|
|
|
stack(messages: Array<string | ToastOptions>, opts: StackOptions = {}): void {
|
|
if (!Array.isArray(messages)) {
|
|
console.error('MeToast.stack: messages must be an array');
|
|
return;
|
|
}
|
|
|
|
messages.forEach((message, index) => {
|
|
setTimeout(() => {
|
|
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
|
|
|
|
this._emit({
|
|
...opts,
|
|
...((typeof message === 'object' && message !== null) ? message as ToastOptions : {}),
|
|
message: msg,
|
|
} as ToastOptions);
|
|
}, index * (opts.stagger || 100));
|
|
});
|
|
},
|
|
|
|
dismiss(id?: string): void {
|
|
if (id) {
|
|
const t = this._toasts.get(id);
|
|
if (t) t.close();
|
|
return;
|
|
}
|
|
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();
|
|
});
|
|
},
|
|
|
|
getAll(): Map<string, ToastInstance> {
|
|
return new Map(this._toasts);
|
|
},
|
|
|
|
count(): number {
|
|
return this._toasts.size;
|
|
},
|
|
|
|
group(name: string): GroupAPI {
|
|
const self = this;
|
|
const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action'];
|
|
const g: GroupAPI = { _group: name } as unknown as GroupAPI;
|
|
methods.forEach(m => {
|
|
(g as unknown as Record<string, (...args: unknown[]) => unknown>)[m] = (...args: unknown[]) => {
|
|
const lastArg = args[args.length - 1];
|
|
const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg);
|
|
const selfMethods = self as unknown as Record<string, (...a: unknown[]) => unknown>;
|
|
if (isObj && args.length === 1) {
|
|
return selfMethods[m]({ ...(lastArg as Record<string, unknown>), group: name });
|
|
}
|
|
if (isObj) {
|
|
args.pop();
|
|
return selfMethods[m](...args, { ...(lastArg as Record<string, unknown>), group: name });
|
|
}
|
|
return selfMethods[m](...args, { group: name });
|
|
};
|
|
});
|
|
g.dismiss = () => self.dismissGroup(name);
|
|
g.count = () => self._groupCount(name);
|
|
return g;
|
|
},
|
|
|
|
dismissGroup(name: string): void {
|
|
this._toasts.forEach(t => { if (t.group === name) t.close(); });
|
|
},
|
|
|
|
_groupCount(name: string): number {
|
|
let c = 0;
|
|
this._toasts.forEach(t => { if (t.group === name) c++; });
|
|
return c;
|
|
},
|
|
|
|
getToasts(): ToastInstance[] {
|
|
return Array.from(this._toasts.values());
|
|
},
|
|
|
|
hasToasts(): boolean {
|
|
return this._toasts.size > 0;
|
|
},
|
|
|
|
getToast(id: string): ToastInstance | null {
|
|
if (!id) return null;
|
|
return this._toasts.get(id) || null;
|
|
},
|
|
|
|
closeAll(): void {
|
|
this._toasts.forEach(t => t.close());
|
|
},
|
|
|
|
clearAll(): void {
|
|
this._toasts.forEach(t => t.close());
|
|
},
|
|
|
|
pauseAll(): void {
|
|
this._toasts.forEach(t => t._pause());
|
|
},
|
|
|
|
resumeAll(): void {
|
|
this._toasts.forEach(t => t._resume());
|
|
},
|
|
|
|
updateAll(partial: Partial<ToastOptions>): void {
|
|
if (partial && typeof partial === 'object') {
|
|
this._toasts.forEach(t => t.update(partial));
|
|
}
|
|
},
|
|
|
|
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
|
|
if (typeof predicate !== 'function') return [];
|
|
return Array.from(this._toasts.values()).filter(predicate);
|
|
},
|
|
|
|
findByType(type: string): ToastInstance[] {
|
|
return this.findToasts(t => t.type === type);
|
|
},
|
|
|
|
findByPosition(position: string): ToastInstance[] {
|
|
return this.findToasts(t => t.config.position === position);
|
|
},
|
|
|
|
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
|
|
|
init(options: InitOptions = {}): MeToast {
|
|
Toast.trigger('beforeInit');
|
|
this._destroyed = false;
|
|
|
|
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);
|
|
});
|
|
}
|
|
|
|
Toast.trigger('afterInit');
|
|
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);
|
|
Toast.trigger('configChange');
|
|
}
|
|
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);
|
|
Toast.trigger('configChange');
|
|
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 });
|
|
|
|
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
|
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
|
const stored = this.plugins.get(plugin) as (Record<string, unknown> & Plugin) | null;
|
|
if (plugin === 'accessibility') {
|
|
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>);
|
|
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);
|
|
}
|
|
|
|
return this;
|
|
},
|
|
|
|
destroy(): void {
|
|
if (this._destroyed) return;
|
|
this._destroyed = true;
|
|
|
|
Toast.trigger('beforeDestroy');
|
|
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.destroy === 'function') {
|
|
this.animations.destroy();
|
|
}
|
|
|
|
// 清理 DOM 与静态资源
|
|
if (typeof document !== 'undefined') {
|
|
const containers = document.querySelectorAll('.met-container');
|
|
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
|
|
|
['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 as unknown as MeToast['animations'],
|
|
themes: null as unknown as MeToast['themes'],
|
|
i18n: null as unknown as MeToast['i18n'],
|
|
plugins: null as unknown as MeToast['plugins'],
|
|
presetPlugins: {} as MeToast['presetPlugins'],
|
|
};
|
|
|
|
// 连接 Toast 静态回调到 meToast 实例
|
|
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 };
|