release: v0.2.0 — TypeScript 源码重构
### Changed - 全部源码从 JavaScript 迁移到 TypeScript (strict mode) - core.js (1244行) 拆分为 toast.ts + api.ts + templates.ts - 删除手动维护的 types/index.d.ts,类型从源码自动生成 - 构建工具链: Babel → ts-jest, 新增 @rollup/plugin-typescript - 新增 rollup-plugin-dts 生成合并 .d.ts ### Added - tsconfig.json (strict: true) - src/types.ts 核心类型模块 (39 个导出类型) - .eslintrc.json (@typescript-eslint) - .gitea/workflows/ci.yml (Node 18/20/22/24 矩阵) - CHANGELOG.md ### Aligned with metona-starter - package.json: type:module, engines≥16, prepublishOnly - rollup.config.js: dts plugin, port 3001 - jest.config.cjs, build.sh, serve.sh, .gitignore (.npmrc) ### Removed - babel.config.js, types/ 目录, 所有 src/*.js
This commit is contained in:
+220
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
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 };
|
||||
Reference in New Issue
Block a user