Files
MetonaToast/src/plugins.ts
T

221 lines
7.1 KiB
TypeScript

/**
* 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<string, Plugin>;
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<string, Plugin> = {
/**
* 键盘快捷键插件 — 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<string, unknown>)._handler = handler;
document.addEventListener('keydown', handler);
},
uninstall(this: Plugin) {
const handler = (this as Record<string, unknown>)._handler as ((e: KeyboardEvent) => void) | null;
if (handler && typeof document !== 'undefined') {
document.removeEventListener('keydown', handler);
(this as Record<string, unknown>)._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<string, unknown>).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<string, unknown>) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).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<string, unknown>).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<string, Plugin> { return { ...presetPlugins }; },
createPlugin(config: Partial<Plugin>): 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<Plugin>): { 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 };