/** * MetonaToast Plugins — 插件系统 * @module plugins * @version 0.2.1 */ import { t } from './i18n.js'; import type { Plugin, PluginManager as IPluginManager, PluginUtils } from './types.js'; /** * 插件管理器 */ class PluginManager implements IPluginManager { plugins: Map; initialized: boolean; constructor() { this.plugins = new Map(); this.initialized = false; } register(name: string, plugin: Plugin): PluginManager { if (this.plugins.has(name)) { console.warn(`Plugin "${name}" is already registered`); return this; } if (!plugin || typeof plugin !== 'object' || !plugin.name) { console.error(`Invalid plugin "${name}": must be an object with a "name" property`); return this; } const stored: Plugin = { ...plugin, installed: false, enabled: true }; if (!stored.name) stored.name = name; this.plugins.set(name, stored); if (stored.install) { try { stored.install(this); stored.installed = true; } catch (e) { console.error(`Failed to install plugin "${name}":`, e); } } return this; } unregister(name: string): PluginManager { const plugin = this.plugins.get(name); if (!plugin) return this; if (plugin.uninstall) { try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); } } this.plugins.delete(name); return this; } get(name: string): Plugin | null { return this.plugins.get(name) || null; } has(name: string): boolean { return this.plugins.has(name); } getAll(): Plugin[] { return Array.from(this.plugins.values()); } getNames(): string[] { return Array.from(this.plugins.keys()); } enable(name: string): PluginManager { const p = this.plugins.get(name); if (p) p.enabled = true; return this; } disable(name: string): PluginManager { const p = this.plugins.get(name); if (p) p.enabled = false; return this; } isEnabled(name: string): boolean { const p = this.plugins.get(name); return p ? !!p.enabled : false; } destroy(): void { this.plugins.forEach((p) => { if (p.destroy) { try { p.destroy(this); } catch (_e) { /* noop */ } } }); this.plugins.clear(); } } /** * 预设插件 */ const presetPlugins: Record = { /** * 键盘快捷键插件 — ESC 关闭所有 Toast */ keyboard: { name: 'keyboard', version: '1.0.0', description: 'ESC 关闭所有 Toast', _handler: null as ((e: KeyboardEvent) => void) | null, install(this: Plugin) { if (typeof document === 'undefined') return; const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') { document.querySelectorAll('.met-toast .met-close').forEach(btn => { try { (btn as HTMLElement).click(); } catch (_e) { /* noop */ } }); } }; (this as Record)._handler = handler; document.addEventListener('keydown', handler); }, uninstall(this: Plugin) { const handler = (this as Record)._handler as ((e: KeyboardEvent) => void) | null; if (handler && typeof document !== 'undefined') { document.removeEventListener('keydown', handler); (this as Record)._handler = null; } }, }, /** * 持久化插件 — 自动保存/加载配置到 localStorage */ persistence: { name: 'persistence', version: '1.0.0', description: '自动持久化配置到 localStorage', storageKey: 'metona-toast-config', install(this: Plugin) { if (typeof localStorage !== 'undefined') { try { const key = (this as Record).storageKey as string; const saved = localStorage.getItem(key); return saved ? JSON.parse(saved) : null; } catch (_e) { return null; } } return null; }, save(this: Plugin, config: Record) { if (typeof localStorage !== 'undefined') { try { const key = (this as Record).storageKey as string; localStorage.setItem(key, JSON.stringify(config)); } catch (_e) { /* noop */ } } }, uninstall(this: Plugin) { if (typeof localStorage !== 'undefined') { try { const key = (this as Record).storageKey as string; localStorage.removeItem(key); } catch (_e) { /* noop */ } } }, }, /** * 无障碍插件 — 屏幕阅读器公告 */ accessibility: { name: 'accessibility', version: '1.0.0', description: '通过屏幕阅读器朗读 Toast 内容', announce(toast: { type: string; title?: string; message?: string }) { if (typeof document === 'undefined') return; const el = document.createElement('div'); el.setAttribute('aria-live', 'assertive'); el.setAttribute('aria-atomic', 'true'); el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;'; const typeName = t(toast.type) || toast.type; el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`; document.body.appendChild(el); setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000); }, }, }; /** * 插件工具 */ const pluginUtils: PluginUtils = { createManager(): PluginManager { return new PluginManager(); }, register(name: string, plugin: Plugin): PluginManager { return defaultPluginManager.register(name, plugin); }, unregister(name: string): PluginManager { return defaultPluginManager.unregister(name); }, get(name: string): Plugin | null { return defaultPluginManager.get(name); }, has(name: string): boolean { return defaultPluginManager.has(name); }, getAll(): Plugin[] { return defaultPluginManager.getAll(); }, getNames(): string[] { return defaultPluginManager.getNames(); }, enable(name: string): PluginManager { return defaultPluginManager.enable(name); }, disable(name: string): PluginManager { return defaultPluginManager.disable(name); }, isEnabled(name: string): boolean { return defaultPluginManager.isEnabled(name); }, getPreset(name: string): Plugin | null { return presetPlugins[name] || null; }, getAllPresets(): Record { return { ...presetPlugins }; }, createPlugin(config: Partial): Plugin { return { name: config.name || 'custom', version: config.version || '1.0.0', description: config.description || '', hooks: config.hooks || {}, install: config.install || undefined, uninstall: config.uninstall || undefined, ...config, }; }, validatePlugin(plugin: Partial): { valid: boolean; errors: string[] } { const errors: string[] = []; if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object'); if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version'); return { valid: errors.length === 0, errors }; }, }; const defaultPluginManager = new PluginManager(); export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };