(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : typeof define === 'function' && define.amd ? define(['exports'], factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.MeToast = {})); })(this, (function (exports) { 'use strict'; /** * MetonaToast Utils — 工具函数 * @module utils * @version 0.2.1 */ /** * 生成唯一ID */ const generateId = () => { return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8); }; /** * HTML转义 * @param s - 输入字符串 */ const escapeHTML = (s) => { if (typeof document === 'undefined') { return String(s == null ? '' : s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } const div = document.createElement('div'); div.textContent = String(s == null ? '' : s); return div.innerHTML; }; /** * 检测暗色模式偏好 */ const prefersDark = () => { return typeof window !== 'undefined' && typeof window.matchMedia === 'function' && window.matchMedia('(prefers-color-scheme: dark)').matches; }; /** * MetonaToast Icons — 图标SVG定义 * @module icons * @version 0.2.1 * @description 107 个内置 SVG 图标 */ const ICONS = { success: ` `, error: ` `, warning: ` `, info: ` `, loading: ` `, close: ` `, 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: ` `, }; /** * MetonaToast Locales — 国际化翻译数据 * @module locales * @version 0.2.1 * @description 内置 zh-CN / en-US 完整翻译 */ const LOCALES = { 'zh-CN': { // === 操作 === close: '关闭', loading: '加载中...', success: '操作成功', error: '操作失败', warning: '警告', info: '信息', confirm: '确认', cancel: '取消', yes: '是', no: '否', ok: '确定', retry: '重试', undo: '撤销', redo: '重做', save: '保存', delete: '删除', edit: '编辑', add: '添加', remove: '移除', search: '搜索', filter: '筛选', sort: '排序', refresh: '刷新', sync: '同步', upload: '上传', download: '下载', share: '分享', copy: '复制', cut: '剪切', paste: '粘贴', print: '打印', export: '导出', import: '导入', settings: '设置', help: '帮助', about: '关于', version: '版本', language: '语言', theme: '主题', dark: '暗色', light: '亮色', auto: '自动', notifications: '通知', enable: '启用', disable: '禁用', on: '开启', off: '关闭', max: '最大', min: '最小', expand: '展开', collapse: '折叠', fullscreen: '全屏', exitFullscreen: '退出全屏', zoomIn: '放大', zoomOut: '缩小', reset: '重置', apply: '应用', submit: '提交', send: '发送', receive: '接收', connect: '连接', disconnect: '断开', start: '开始', stop: '停止', pause: '暂停', resume: '继续', next: '下一个', previous: '上一个', first: '第一个', last: '最后一个', back: '返回', forward: '前进', home: '首页', menu: '菜单', more: '更多', less: '更少', show: '显示', hide: '隐藏', visible: '可见', hidden: '隐藏', // === 状态 === enabled: '已启用', disabled: '已禁用', active: '活跃', inactive: '非活跃', online: '在线', offline: '离线', connected: '已连接', disconnected: '已断开', loaded: '已加载', saving: '保存中', saved: '已保存', deleting: '删除中', deleted: '已删除', uploading: '上传中', uploaded: '已上传', downloading: '下载中', downloaded: '已下载', processing: '处理中', processed: '已处理', syncing: '同步中', synced: '已同步', sending: '发送中', sent: '已发送', receiving: '接收中', received: '已接收', connecting: '连接中', // === 交互 === question: '问题', feedback: '反馈', report: '报告', bug: '错误', feature: '功能', improvement: '改进', suggestion: '建议', request: '请求', response: '响应', status: '状态', progress: '进度', complete: '完成', incomplete: '未完成', pending: '待处理', failed: '失败', cancelled: '已取消', timeout: '超时', expired: '已过期', invalid: '无效', valid: '有效', required: '必填', optional: '可选', allowed: '允许', denied: '拒绝', approved: '已批准', rejected: '已拒绝', accepted: '已接受', declined: '已拒绝', confirmed: '已确认', unconfirmed: '未确认', verified: '已验证', unverified: '未验证', authenticated: '已认证', unauthenticated: '未认证', authorized: '已授权', unauthorized: '未授权', // === 范围 / 角色 === public: '公开', private: '私有', protected: '受保护', internal: '内部', external: '外部', local: '本地', remote: '远程', global: '全局', system: '系统', user: '用户', admin: '管理员', moderator: '版主', guest: '访客', member: '成员', owner: '所有者', creator: '创建者', editor: '编辑者', viewer: '查看者', subscriber: '订阅者', follower: '关注者', following: '关注中', friend: '朋友', contact: '联系人', // === 组织 === group: '群组', team: '团队', organization: '组织', company: '公司', department: '部门', project: '项目', task: '任务', issue: '问题', ticket: '工单', message: '消息', notification: '通知', alert: '警报', reminder: '提醒', event: '事件', activity: '活动', log: '日志', analytics: '分析', statistics: '统计', metrics: '指标', performance: '性能', security: '安全', privacy: '隐私', compliance: '合规', policy: '政策', terms: '条款', conditions: '条件', agreement: '协议', contract: '合同', license: '许可证', copyright: '版权', trademark: '商标', patent: '专利', tradeSecret: '商业秘密', confidential: '机密', sensitive: '敏感', critical: '关键', important: '重要', urgent: '紧急', high: '高', medium: '中', low: '低', none: '无', all: '全部', any: '任何', some: '一些', each: '每个', every: '每个', both: '两者', neither: '两者都不', either: '任一', other: '其他', another: '另一个', this: '这个', that: '那个', these: '这些', those: '那些', here: '这里', there: '那里', where: '哪里', when: '何时', why: '为什么', how: '如何', what: '什么', who: '谁', which: '哪个', whose: '谁的', whom: '谁', }, 'en-US': { // === Actions === close: 'Close', loading: 'Loading...', success: 'Success', error: 'Error', warning: 'Warning', info: 'Info', confirm: 'Confirm', cancel: 'Cancel', yes: 'Yes', no: 'No', ok: 'OK', retry: 'Retry', undo: 'Undo', redo: 'Redo', save: 'Save', delete: 'Delete', edit: 'Edit', add: 'Add', remove: 'Remove', search: 'Search', filter: 'Filter', sort: 'Sort', refresh: 'Refresh', sync: 'Sync', upload: 'Upload', download: 'Download', share: 'Share', copy: 'Copy', cut: 'Cut', paste: 'Paste', print: 'Print', export: 'Export', import: 'Import', settings: 'Settings', help: 'Help', about: 'About', version: 'Version', language: 'Language', theme: 'Theme', dark: 'Dark', light: 'Light', auto: 'Auto', notifications: 'Notifications', enable: 'Enable', disable: 'Disable', on: 'On', off: 'Off', max: 'Max', min: 'Min', expand: 'Expand', collapse: 'Collapse', fullscreen: 'Fullscreen', exitFullscreen: 'Exit Fullscreen', zoomIn: 'Zoom In', zoomOut: 'Zoom Out', reset: 'Reset', apply: 'Apply', submit: 'Submit', send: 'Send', receive: 'Receive', connect: 'Connect', disconnect: 'Disconnect', start: 'Start', stop: 'Stop', pause: 'Pause', resume: 'Resume', next: 'Next', previous: 'Previous', first: 'First', last: 'Last', back: 'Back', forward: 'Forward', home: 'Home', menu: 'Menu', more: 'More', less: 'Less', show: 'Show', hide: 'Hide', visible: 'Visible', hidden: 'Hidden', // === Status === enabled: 'Enabled', disabled: 'Disabled', active: 'Active', inactive: 'Inactive', online: 'Online', offline: 'Offline', connected: 'Connected', disconnected: 'Disconnected', loaded: 'Loaded', saving: 'Saving', saved: 'Saved', deleting: 'Deleting', deleted: 'Deleted', uploading: 'Uploading', uploaded: 'Uploaded', downloading: 'Downloading', downloaded: 'Downloaded', processing: 'Processing', processed: 'Processed', syncing: 'Syncing', synced: 'Synced', sending: 'Sending', sent: 'Sent', receiving: 'Receiving', received: 'Received', connecting: 'Connecting', // === Interaction === question: 'Question', feedback: 'Feedback', report: 'Report', bug: 'Bug', feature: 'Feature', improvement: 'Improvement', suggestion: 'Suggestion', request: 'Request', response: 'Response', status: 'Status', progress: 'Progress', complete: 'Complete', incomplete: 'Incomplete', pending: 'Pending', failed: 'Failed', cancelled: 'Cancelled', timeout: 'Timeout', expired: 'Expired', invalid: 'Invalid', valid: 'Valid', required: 'Required', optional: 'Optional', allowed: 'Allowed', denied: 'Denied', approved: 'Approved', rejected: 'Rejected', accepted: 'Accepted', declined: 'Declined', confirmed: 'Confirmed', unconfirmed: 'Unconfirmed', verified: 'Verified', unverified: 'Unverified', authenticated: 'Authenticated', unauthenticated: 'Unauthenticated', authorized: 'Authorized', unauthorized: 'Unauthorized', // === Scope / Role === public: 'Public', private: 'Private', protected: 'Protected', internal: 'Internal', external: 'External', local: 'Local', remote: 'Remote', global: 'Global', system: 'System', user: 'User', admin: 'Admin', moderator: 'Moderator', guest: 'Guest', member: 'Member', owner: 'Owner', creator: 'Creator', editor: 'Editor', viewer: 'Viewer', subscriber: 'Subscriber', follower: 'Follower', following: 'Following', friend: 'Friend', contact: 'Contact', // === Organization === group: 'Group', team: 'Team', organization: 'Organization', company: 'Company', department: 'Department', project: 'Project', task: 'Task', issue: 'Issue', ticket: 'Ticket', message: 'Message', notification: 'Notification', alert: 'Alert', reminder: 'Reminder', event: 'Event', activity: 'Activity', log: 'Log', analytics: 'Analytics', statistics: 'Statistics', metrics: 'Metrics', performance: 'Performance', security: 'Security', privacy: 'Privacy', compliance: 'Compliance', policy: 'Policy', terms: 'Terms', conditions: 'Conditions', agreement: 'Agreement', contract: 'Contract', license: 'License', copyright: 'Copyright', trademark: 'Trademark', patent: 'Patent', tradeSecret: 'Trade Secret', confidential: 'Confidential', sensitive: 'Sensitive', critical: 'Critical', important: 'Important', urgent: 'Urgent', high: 'High', medium: 'Medium', low: 'Low', none: 'None', all: 'All', any: 'Any', some: 'Some', each: 'Each', every: 'Every', both: 'Both', neither: 'Neither', either: 'Either', other: 'Other', another: 'Another', this: 'This', that: 'That', these: 'These', those: 'Those', here: 'Here', there: 'There', where: 'Where', when: 'When', why: 'Why', how: 'How', what: 'What', who: 'Who', which: 'Which', whose: 'Whose', whom: 'Whom', }, }; /** * MetonaToast Constants — 常量定义 * @module constants * @version 0.2.1 */ /** * 默认配置 */ const DEFAULTS = Object.freeze({ position: 'top-right', duration: 4000, max: 6, gap: 12, offset: 24, pauseOnHover: true, closeOnClick: true, draggable: true, showProgress: true, progressDirection: 'horizontal', icon: true, closeButton: true, theme: 'auto', animation: 'slide', zIndex: 9999, width: 360, className: '', style: {}, onShow: null, onBeforeShow: null, onClose: null, onClick: null, onUpdate: null, resetTimerOnUpdate: false, notifyWhenHidden: false, onError: null, locale: 'zh-CN', plugins: [], }); /** * 类型颜色配置 */ const TYPE_COLORS = { success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, info: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, loading: { fg: '#6366f1', bg: 'rgba(99, 102, 241, 0.1)' }, default: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, check: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, x: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, alert: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, question: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, star: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, heart: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, bell: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, mail: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, settings: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, user: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, home: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, search: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, plus: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, minus: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, edit: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, trash: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, download: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, upload: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, share: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, link: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, external: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, clock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, calendar: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, map: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, compass: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, globe: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, wifi: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, cloud: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sun: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, moon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, zap: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, activity: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, cpu: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, database: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, server: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, terminal: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, code: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, git: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, package: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, layers: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, grid: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, list: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, filter: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sort: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, refresh: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, sync: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, power: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, battery: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, bluetooth: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, volume: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, mic: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, camera: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, image: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, video: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, music: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, file: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, folder: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, clipboard: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, save: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, print: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, eye: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, eyeOff: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, lock: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, unlock: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, shield: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, key: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, flag: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, bookmark: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, tag: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, gift: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, award: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, target: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, crosshair: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, move: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, maximize: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, minimize: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, copy: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, cut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, paste: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, rotateCw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, rotateCcw: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, zoomIn: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, zoomOut: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, crop: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, sliders: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, toggleLeft: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, toggleRight: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, checkCircle: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, xCircle: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, alertCircle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, infoCircle: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, helpCircle: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, alertTriangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, checkSquare: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' }, square: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, circle: { fg: '#6b7280', bg: 'rgba(107, 114, 128, 0.1)' }, triangle: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' }, hexagon: { fg: '#3b82f6', bg: 'rgba(59, 130, 246, 0.1)' }, octagon: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' }, pentagon: { fg: '#8b5cf6', bg: 'rgba(139, 92, 246, 0.1)' }, diamond: { fg: '#ec4899', bg: 'rgba(236, 72, 153, 0.1)' }, }; /** * 动画配置 */ const ANIMATIONS = { slide: { enter: { transform: 'translateX(80px)', opacity: '0' }, leave: { transform: 'translateX(120%)', opacity: '0' }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, fade: { enter: { opacity: '0', filter: 'blur(3px)' }, leave: { opacity: '0', filter: 'blur(3px)' }, duration: 500, easing: 'ease', }, scale: { enter: { transform: 'scale(0.55)', opacity: '0' }, leave: { transform: 'scale(0.55)', opacity: '0' }, duration: 450, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, bounce: { enter: { transform: 'translateY(-80px)', opacity: '0' }, leave: { transform: 'translateY(20px)', opacity: '0' }, duration: 650, easing: 'ease', }, flip: { enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: '0' }, leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: '0' }, duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, rotate: { enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: '0' }, leave: { transform: 'rotate(25deg) scale(0.6)', opacity: '0' }, duration: 500, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, zoom: { enter: { transform: 'scale(0.1)', opacity: '0' }, leave: { transform: 'scale(0.1)', opacity: '0' }, duration: 500, easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)', }, slideUp: { enter: { transform: 'translateY(60px)', opacity: '0' }, leave: { transform: 'translateY(-120%)', opacity: '0' }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideDown: { enter: { transform: 'translateY(-60px)', opacity: '0' }, leave: { transform: 'translateY(120%)', opacity: '0' }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideLeft: { enter: { transform: 'translateX(-90px)', opacity: '0' }, leave: { transform: 'translateX(120%)', opacity: '0' }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, slideRight: { enter: { transform: 'translateX(90px)', opacity: '0' }, leave: { transform: 'translateX(-120%)', opacity: '0' }, duration: 400, easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', }, }; /** * 主题配置 */ const THEMES = { light: { bg: 'rgba(255, 255, 255, 0.96)', text: '#1f2937', border: 'rgba(0, 0, 0, 0.06)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)', hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)', progressBg: 'rgba(0, 0, 0, 0.06)', closeHoverBg: 'rgba(0, 0, 0, 0.06)', }, dark: { bg: 'rgba(28, 32, 40, 0.94)', text: '#e6e8eb', border: 'rgba(255, 255, 255, 0.08)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4)', hoverShadow: '0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4)', progressBg: 'rgba(255, 255, 255, 0.08)', closeHoverBg: 'rgba(255, 255, 255, 0.08)', }, auto: { bg: 'rgba(255, 255, 255, 0.96)', text: '#1f2937', border: 'rgba(0, 0, 0, 0.06)', shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)', hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)', progressBg: 'rgba(0, 0, 0, 0.06)', closeHoverBg: 'rgba(0, 0, 0, 0.06)', }, warm: { bg: 'rgba(255, 251, 235, 0.96)', text: '#78350f', border: 'rgba(245, 158, 11, 0.2)', shadow: '0 10px 36px -10px rgba(245, 158, 11, 0.18), 0 4px 14px -4px rgba(245, 158, 11, 0.08)', hoverShadow: '0 14px 48px -10px rgba(245, 158, 11, 0.22), 0 6px 18px -4px rgba(245, 158, 11, 0.10)', progressBg: 'rgba(245, 158, 11, 0.1)', closeHoverBg: 'rgba(245, 158, 11, 0.1)', }, }; /** * MetonaToast Styles — 样式管理 * @module styles * @version 0.2.1 */ // 样式缓存 let styleElement = null; /** * 生成CSS样式 */ const generateCSS = () => { return ` .met-container { pointer-events: none; display: flex; flex-direction: column; gap: 12px; box-sizing: border-box; padding: 24px; max-width: 100vw; position: fixed; z-index: 9999; } .met-container.top-left { top: 0; left: 0; align-items: flex-start; } .met-container.top-center { top: 0; left: 0; right: 0; align-items: center; } .met-container.top-right { top: 0; right: 0; align-items: flex-end; } .met-container.bottom-left { bottom: 0; left: 0; align-items: flex-start; } .met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; } .met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; } .met-toast { position: relative; display: flex; align-items: flex-start; gap: 12px; padding: 14px 16px; border-radius: 12px; box-sizing: border-box; backdrop-filter: blur(14px) saturate(180%); -webkit-backdrop-filter: blur(14px) saturate(180%); font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; user-select: none; -webkit-user-select: none; transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.25s ease, opacity 0.25s ease; will-change: transform, opacity; flex-shrink: 0; pointer-events: auto; min-width: 240px; max-width: calc(100vw - 48px); width: 360px; border: 1px solid var(--met-border, rgba(0,0,0,0.06)); background: var(--met-bg, rgba(255,255,255,0.96)); color: var(--met-text, #1f2937); box-shadow: var(--met-shadow, 0 10px 36px -10px rgba(0,0,0,0.18), 0 4px 14px -4px rgba(0,0,0,0.08)); transform: translateZ(0); -webkit-transform: translateZ(0); backface-visibility: hidden; -webkit-backface-visibility: hidden; perspective: 1000; } .met-toast:hover { box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important; } .met-toast.met-clickable { cursor: pointer; } .met-icon { flex-shrink: 0; display: flex; align-items: center; justify-content: center; width: 24px; height: 24px; margin-top: 1px; } .met-content { flex: 1; min-width: 0; word-wrap: break-word; overflow-wrap: break-word; } .met-title { font-weight: 600; font-size: 14px; letter-spacing: 0.1px; } .met-message { font-size: 13px; opacity: 0.85; margin-top: 2px; } .met-close { flex-shrink: 0; background: transparent; border: 0; padding: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 6px; transition: background 0.2s, color 0.2s; line-height: 0; color: inherit; opacity: 0.5; } .met-close:hover { opacity: 1; background: var(--met-close-hover-bg, rgba(0,0,0,0.06)); } .met-progress { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 0 0 12px 12px; } .met-progress-v { position: absolute; left: 0; top: 0; bottom: 0; width: 3px; overflow: hidden; background: var(--met-progress-bg, rgba(0,0,0,0.06)); border-radius: 12px 0 0 12px; } .met-bar { position: absolute; left: 0; top: 0; height: 100%; width: 100%; transform-origin: left; transform: scaleX(1); } .met-bar-v { position: absolute; left: 0; bottom: 0; width: 100%; height: 100%; transform-origin: bottom; transform: scaleY(1); } .met-side { position: absolute; left: 0; top: 0; bottom: 0; width: 4px; border-radius: 12px 0 0 12px; } .met-spin { animation: met-rot 1s linear infinite; transform-origin: 50% 50%; } .met-toast.met-leaving { pointer-events: none; z-index: 1; } @keyframes met-rot { to { transform: rotate(360deg); } } .met-anim-slide.met-toast, .met-anim-fade.met-toast, .met-anim-scale.met-toast, .met-anim-bounce.met-toast, .met-anim-flip.met-toast, .met-anim-rotate.met-toast, .met-anim-zoom.met-toast, .met-anim-slideUp.met-toast, .met-anim-slideDown.met-toast, .met-anim-slideLeft.met-toast, .met-anim-slideRight.met-toast { opacity: 0; } @keyframes met-slide-in { 0% { transform: translateX(80px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-fade-in { 0% { opacity: 0; filter: blur(3px); } 100% { opacity: 1; filter: blur(0); } } @keyframes met-scale-in { 0% { transform: scale(0.55); opacity: 0; } 65% { transform: scale(1.07); opacity: 1; } 100% { transform: scale(1); opacity: 1; } } @keyframes met-bounce-in { 0% { transform: translateY(-80px); opacity: 0; animation-timing-function: ease-in; } 45% { transform: translateY(14px); opacity: 1; animation-timing-function: ease-out; } 65% { transform: translateY(-12px); animation-timing-function: ease-in; } 82% { transform: translateY(4px); animation-timing-function: ease-out; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-flip-in { 0% { transform: perspective(500px) rotateX(-90deg); opacity: 0; } 60% { transform: perspective(500px) rotateX(8deg); opacity: 1; } 85% { transform: perspective(500px) rotateX(-2deg); } 100% { transform: perspective(500px) rotateX(0); opacity: 1; } } @keyframes met-rotate-in { 0% { transform: rotate(-25deg) scale(0.6); opacity: 0; } 60% { transform: rotate(4deg) scale(1.05); opacity: 1; } 85% { transform: rotate(-1deg) scale(0.98); } 100% { transform: rotate(0) scale(1); opacity: 1; } } @keyframes met-zoom-in { 0% { transform: scale(0.1); opacity: 0; } 50% { transform: scale(1.12); opacity: 1; } 75% { transform: scale(0.94); } 100% { transform: scale(1); opacity: 1; } } @keyframes met-slideUp-in { 0% { transform: translateY(60px); opacity: 0; } 70% { transform: translateY(-6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideDown-in { 0% { transform: translateY(-60px); opacity: 0; } 70% { transform: translateY(6px); opacity: 1; } 100% { transform: translateY(0); opacity: 1; } } @keyframes met-slideLeft-in { 0% { transform: translateX(-90px); opacity: 0; } 65% { transform: translateX(6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } @keyframes met-slideRight-in { 0% { transform: translateX(90px); opacity: 0; } 65% { transform: translateX(-6px); opacity: 1; } 100% { transform: translateX(0); opacity: 1; } } .met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; } .met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-bounce.met-toast.met-show { animation: met-bounce-in 0.65s ease forwards; } .met-anim-flip.met-toast.met-show { animation: met-flip-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-rotate.met-toast.met-show { animation: met-rotate-in 0.50s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-zoom.met-toast.met-show { animation: met-zoom-in 0.50s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; } .met-anim-slideUp.met-toast.met-show { animation: met-slideUp-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideDown.met-toast.met-show { animation: met-slideDown-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } .met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; } @media (max-width: 480px) { .met-container { padding: 12px !important; } .met-toast { width: 100% !important; min-width: 0 !important; } } @media (max-width: 768px) { .met-toast { font-size: 13px; } .met-title { font-size: 13px; } .met-message { font-size: 12px; } } @media (min-width: 1200px) { .met-toast { width: 400px; } } @media (min-width: 1920px) { .met-toast { width: 440px; } } @media (max-width: 320px) { .met-toast { width: 100%; min-width: 0; padding: 12px 14px; } .met-container { padding: 8px; } } @media (orientation: landscape) and (max-height: 500px) { .met-container { padding: 12px 24px; } .met-toast { min-height: 44px; padding: 10px 14px; } } @media (orientation: portrait) and (max-width: 500px) { .met-container { padding: 12px; } .met-toast { width: 100%; min-width: 0; } } @media (prefers-reduced-motion: reduce) { .met-toast { transition: opacity 0.15s !important; } .met-toast.met-show { animation: none !important; opacity: 1 !important; } .met-spin { animation-duration: 2.5s !important; } } @media (forced-colors: active) { .met-toast { border: 2px solid ButtonText; } .met-close { border: 1px solid ButtonText; } } @media (prefers-contrast: high) { .met-toast { border-width: 3px; border-style: solid; } .met-close { border: 2px solid currentColor; border-radius: 4px; } } .met-toast:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } .met-close:focus-visible { outline: 2px solid #3b82f6; outline-offset: 2px; } @media (hover: none) and (pointer: coarse) { .met-toast:hover { box-shadow: inherit; } .met-toast { min-height: 48px; } .met-close { min-width: 48px; min-height: 48px; } } @media print { .met-container { display: none !important; } .met-toast { display: none !important; } } :fullscreen .met-container, :picture-in-picture .met-container { z-index: 2147483647; } @supports (padding: max(0px)) { .met-container { padding: max(24px, env(safe-area-inset-top)) max(24px, env(safe-area-inset-right)) max(24px, env(safe-area-inset-bottom)) max(24px, env(safe-area-inset-left)); } } .met-toast:active { transform: scale(0.98); } .met-toast.met-success { border-left: 4px solid #10b981; } .met-toast.met-error { border-left: 4px solid #ef4444; } .met-toast.met-warning { border-left: 4px solid #f59e0b; } .met-toast.met-info { border-left: 4px solid #3b82f6; } .met-toast.met-loading { border-left: 4px solid #6366f1; } .met-bar, .met-bar-v { transition: transform 0.1s linear; } .met-container::-webkit-scrollbar { width: 6px; height: 6px; } .met-container::-webkit-scrollbar-track { background: transparent; } .met-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; } .met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); } .met-toast.met-theme-dark { background: rgba(28, 32, 40, 0.94); color: #e6e8eb; border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4); } .met-toast.met-theme-dark:hover { box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; } .met-toast.met-theme-dark .met-close:hover { background: rgba(255, 255, 255, 0.08); } .met-toast.met-theme-dark .met-progress, .met-toast.met-theme-dark .met-progress-v { background: rgba(255, 255, 255, 0.08); } .met-toast.met-theme-light { background: rgba(255, 255, 255, 0.96); color: #1f2937; border-color: rgba(0, 0, 0, 0.06); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08); } .met-toast.met-theme-light:hover { box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; } .met-toast.met-theme-light .met-close:hover { background: rgba(0, 0, 0, 0.06); } .met-toast.met-theme-light .met-progress, .met-toast.met-theme-light .met-progress-v { background: rgba(0, 0, 0, 0.06); } .met-toast[data-theme] { --met-bg: var(--met-theme-bg); --met-text: var(--met-theme-text); --met-border: var(--met-theme-border); --met-shadow: var(--met-theme-shadow); } @media (prefers-color-scheme: dark) { .met-theme-auto .met-toast { background: rgba(28, 32, 40, 0.94); color: #e6e8eb; border-color: rgba(255, 255, 255, 0.08); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.6), 0 4px 14px -4px rgba(0, 0, 0, 0.4); } .met-theme-auto .met-toast:hover { box-shadow: 0 14px 48px -8px rgba(0, 0, 0, 0.6), 0 6px 18px -4px rgba(0, 0, 0, 0.4) !important; } .met-theme-auto .met-close:hover { background: rgba(255, 255, 255, 0.08); } .met-theme-auto .met-progress, .met-theme-auto .met-progress-v { background: rgba(255, 255, 255, 0.08); } } @media (prefers-color-scheme: light) { .met-theme-auto .met-toast { background: rgba(255, 255, 255, 0.96); color: #1f2937; border-color: rgba(0, 0, 0, 0.06); box-shadow: 0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08); } .met-theme-auto .met-toast:hover { box-shadow: 0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10) !important; } .met-theme-auto .met-close:hover { background: rgba(0, 0, 0, 0.06); } .met-theme-auto .met-progress, .met-theme-auto .met-progress-v { background: rgba(0, 0, 0, 0.06); } } `; }; /** * 注入样式 */ const injectStyles = () => { if (typeof document === 'undefined') return; if (styleElement && document.getElementById('metona-toast-styles')) { return; } const css = generateCSS(); styleElement = document.createElement('style'); styleElement.id = 'metona-toast-styles'; styleElement.textContent = css.replace(/\s+/g, ' '); document.head.appendChild(styleElement); }; /** * MetonaToast Themes — 主题管理 * @module themes * @version 0.2.1 */ // 当前主题状态 let currentTheme = 'auto'; const themeListeners = new Set(); let systemThemeMediaQuery = null; let systemThemeListener = null; /** * 获取系统主题 */ const getSystemTheme = () => { if (typeof window === 'undefined') return 'light'; return prefersDark() ? 'dark' : 'light'; }; /** * 获取主题(别名) */ const getTheme = (theme) => { return resolveTheme(theme); }; /** * 解析主题 — 'auto' 始终按系统主题检测,不依赖当前手动选择的主题 */ const resolveTheme = (theme) => { if (theme === 'auto') { return getSystemTheme(); } return theme; }; /** * 获取主题配置 */ const getThemeConfig = (theme) => { const resolved = resolveTheme(theme); return THEMES[resolved] || THEMES.light; }; /** * 应用主题 */ const applyTheme = (theme) => { currentTheme = theme; const resolved = resolveTheme(theme); if (typeof document !== 'undefined') { document.documentElement.setAttribute('data-met-theme', resolved); document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto'); document.documentElement.classList.add(`met-theme-${resolved}`); const config = getThemeConfig(theme); setThemeVariables(config); } notifyThemeListeners(theme, resolved); }; /** * 设置主题CSS变量 */ const setThemeVariables = (config) => { if (typeof document === 'undefined') return; const root = document.documentElement; root.style.setProperty('--met-bg', config.bg); root.style.setProperty('--met-text', config.text); root.style.setProperty('--met-border', config.border); root.style.setProperty('--met-shadow', config.shadow); root.style.setProperty('--met-hover-shadow', config.hoverShadow); root.style.setProperty('--met-progress-bg', config.progressBg); root.style.setProperty('--met-close-hover-bg', config.closeHoverBg); root.style.setProperty('--met-success', '#10b981'); root.style.setProperty('--met-error', '#ef4444'); root.style.setProperty('--met-warning', '#f59e0b'); root.style.setProperty('--met-info', '#3b82f6'); root.style.setProperty('--met-loading', '#6366f1'); }; /** * 获取当前主题 */ const getCurrentTheme = () => { return currentTheme; }; /** * 获取解析后的主题 */ const getResolvedTheme = () => { return resolveTheme(currentTheme); }; /** * 切换主题 */ const switchTheme = (theme) => { applyTheme(theme); saveTheme(theme); }; /** * 切换亮色/暗色主题 */ const toggleTheme = () => { const resolved = getResolvedTheme(); const newTheme = resolved === 'dark' ? 'light' : 'dark'; switchTheme(newTheme); }; /** * 重置为自动主题 */ const resetToAuto = () => { switchTheme('auto'); }; /** * 保存主题到本地存储 */ const saveTheme = (theme) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-theme', theme); } catch (e) { console.warn('Failed to save theme:', e); } } }; /** * 从本地存储加载主题 */ const loadTheme = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-theme') || 'auto'; } catch (e) { console.warn('Failed to load theme:', e); } } return 'auto'; }; /** * 初始化主题系统 */ const initTheme = () => { const savedTheme = loadTheme(); applyTheme(savedTheme); watchSystemTheme(); }; /** * 监听系统主题变化 */ const watchSystemTheme = () => { if (typeof window === 'undefined' || !window.matchMedia) return; if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); } systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); systemThemeListener = () => { if (currentTheme === 'auto') { applyTheme('auto'); } }; systemThemeMediaQuery.addEventListener('change', systemThemeListener); }; /** * 停止监听系统主题变化 */ const unwatchSystemTheme = () => { if (systemThemeMediaQuery && systemThemeListener) { systemThemeMediaQuery.removeEventListener('change', systemThemeListener); systemThemeMediaQuery = null; systemThemeListener = null; } }; /** * 添加主题监听器 */ const addThemeListener = (listener) => { themeListeners.add(listener); return () => { themeListeners.delete(listener); }; }; /** * 移除主题监听器 */ const removeThemeListener = (listener) => { themeListeners.delete(listener); }; /** * 通知主题监听器 */ const notifyThemeListeners = (theme, resolved) => { themeListeners.forEach((listener) => { try { listener(theme, resolved); } catch (e) { console.error('Theme listener error:', e); } }); }; /** * 清除所有主题监听器 */ const clearThemeListeners = () => { themeListeners.clear(); }; /** * 注册自定义主题 */ const registerTheme = (name, config) => { THEMES[name] = { bg: config.bg || THEMES.light.bg, text: config.text || THEMES.light.text, border: config.border || THEMES.light.border, shadow: config.shadow || THEMES.light.shadow, hoverShadow: config.hoverShadow || THEMES.light.hoverShadow, progressBg: config.progressBg || THEMES.light.progressBg, closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg, }; }; /** * 注销自定义主题 */ const unregisterTheme = (name) => { if (name === 'light' || name === 'dark' || name === 'auto') { console.warn('Cannot unregister built-in theme:', name); return; } delete THEMES[name]; }; /** * 获取所有主题 */ const getAllThemes = () => { return { ...THEMES }; }; /** * 获取主题名称列表 */ const getThemeNames = () => { return Object.keys(THEMES); }; /** * 检查主题是否存在 */ const hasTheme = (name) => { return name in THEMES; }; /** * 获取主题预览 */ const getThemePreview = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return { name: theme, resolved, colors: { background: config.bg, text: config.text, border: config.border, }, isDark: resolved === 'dark', isLight: resolved === 'light', isAuto: theme === 'auto', }; }; /** * 生成主题CSS */ const generateThemeCSS = (theme) => { const config = getThemeConfig(theme); const resolved = resolveTheme(theme); return ` .met-theme-${resolved} { --met-bg: ${config.bg}; --met-text: ${config.text}; --met-border: ${config.border}; --met-shadow: ${config.shadow}; --met-hover-shadow: ${config.hoverShadow}; --met-progress-bg: ${config.progressBg}; --met-close-hover-bg: ${config.closeHoverBg}; } `; }; /** * 应用主题CSS */ const applyThemeCSS = (theme) => { if (typeof document === 'undefined') return; const css = generateThemeCSS(theme); const styleId = 'metona-toast-theme-css'; const oldStyle = document.getElementById(styleId); if (oldStyle) { oldStyle.remove(); } const style = document.createElement('style'); style.id = styleId; style.textContent = css; document.head.appendChild(style); }; /** * 移除主题CSS */ const removeThemeCSS = () => { if (typeof document === 'undefined') return; const styleId = 'metona-toast-theme-css'; const style = document.getElementById(styleId); if (style) { style.remove(); } }; /** * 主题工具 — 代理层 */ const themeUtils = { getSystemTheme, resolveTheme, getThemeConfig, applyTheme, getCurrentTheme, getResolvedTheme, switchTheme, toggleTheme, resetToAuto, initTheme, watchSystemTheme, unwatchSystemTheme, addThemeListener, removeThemeListener, clearThemeListeners, registerTheme, unregisterTheme, getAllThemes, getThemeNames, hasTheme, getThemePreview, generateThemeCSS, applyThemeCSS, removeThemeCSS, saveTheme, loadTheme, }; /** * MetonaToast i18n — 国际化管理 * @module i18n * @version 0.2.1 */ // 当前语言状态 let currentLocale = 'zh-CN'; const localeListeners = new Set(); let fallbackLocale = 'zh-CN'; /** * 获取当前语言 */ const getCurrentLocale = () => { return currentLocale; }; /** * 设置当前语言 */ const setCurrentLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`); locale = fallbackLocale; } currentLocale = locale; notifyLocaleListeners(locale); saveLocale(locale); }; /** * 获取回退语言 */ const getFallbackLocale = () => { return fallbackLocale; }; /** * 设置回退语言 */ const setFallbackLocale = (locale) => { if (!LOCALES[locale]) { console.warn(`Fallback locale "${locale}" not found`); return; } fallbackLocale = locale; }; /** * 翻译函数 */ const t = (key, params = {}) => { const currentTranslation = getTranslation(currentLocale, key); if (currentTranslation !== undefined) { return interpolate(currentTranslation, params); } if (currentLocale !== fallbackLocale) { const fallbackTranslation = getTranslation(fallbackLocale, key); if (fallbackTranslation !== undefined) { return interpolate(fallbackTranslation, params); } } console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`); return key; }; /** * 获取翻译 */ const getTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return undefined; const keys = key.split('.'); let result = localeData; for (const k of keys) { if (result && typeof result === 'object' && k in result) { result = result[k]; } else { return undefined; } } return typeof result === 'string' ? result : undefined; }; /** * 插值函数 */ const interpolate = (str, params) => { return str.replace(/\{(\w+)\}/g, (_match, key) => { return params[key] !== undefined ? String(params[key]) : _match; }); }; /** * 检查翻译是否存在 */ const hasTranslation = (key) => { return getTranslation(currentLocale, key) !== undefined || getTranslation(fallbackLocale, key) !== undefined; }; /** * 获取所有翻译 */ const getTranslations = (locale) => { return LOCALES[locale] || {}; }; /** * 添加翻译 */ const addTranslations = (locale, translations) => { if (!LOCALES[locale]) { LOCALES[locale] = {}; } deepMerge(LOCALES[locale], translations); }; /** * 深度合并对象 */ const deepMerge = (target, source) => { for (const key in source) { if (source[key] instanceof Object && key in target && target[key] instanceof Object) { deepMerge(target[key], source[key]); } else { target[key] = source[key]; } } return target; }; /** * 移除翻译 */ const removeTranslation = (locale, key) => { const localeData = LOCALES[locale]; if (!localeData) return; const keys = key.split('.'); let current = localeData; for (let i = 0; i < keys.length - 1; i++) { if (current[keys[i]] && typeof current[keys[i]] === 'object') { current = current[keys[i]]; } else { return; } } delete current[keys[keys.length - 1]]; }; /** * 清除翻译 */ const clearTranslations = (locale) => { if (LOCALES[locale]) { LOCALES[locale] = {}; } }; /** * 获取支持的语言列表 */ const getSupportedLocales = () => { return Object.keys(LOCALES); }; /** * 检查语言是否支持 */ const isLocaleSupported = (locale) => { return locale in LOCALES; }; /** * 获取语言名称 */ const getLocaleName = (locale) => { const names = { 'zh-CN': '简体中文', 'zh-TW': '繁體中文', 'en-US': 'English (US)', 'en-GB': 'English (UK)', 'ja': '日本語', 'ko': '한국어', 'fr': 'Français', 'de': 'Deutsch', 'es': 'Español', 'pt': 'Português', 'ru': 'Русский', 'ar': 'العربية', 'hi': 'हिन्दी', 'th': 'ไทย', 'vi': 'Tiếng Việt', 'id': 'Bahasa Indonesia', 'ms': 'Bahasa Melayu', 'tr': 'Türkçe', 'it': 'Italiano', 'nl': 'Nederlands', 'pl': 'Polski', 'uk': 'Українська', 'cs': 'Čeština', 'sv': 'Svenska', 'da': 'Dansk', 'fi': 'Suomi', 'nb': 'Norsk', 'el': 'Ελληνικά', 'he': 'עברית', 'hu': 'Magyar', 'ro': 'Română', 'bg': 'Български', 'hr': 'Hrvatski', 'sk': 'Slovenčina', 'sl': 'Slovenščina', 'et': 'Eesti', 'lv': 'Latviešu', 'lt': 'Lietuvių', 'ca': 'Català', 'gl': 'Galego', 'eu': 'Euskara', 'cy': 'Cymraeg', 'ga': 'Gaeilge', 'mt': 'Malti', 'is': 'Íslenska', 'mk': 'Македонски', 'sq': 'Shqip', 'sr': 'Српски', 'bs': 'Bosanski', 'me': 'Crnogorski', 'ka': 'ქართული', 'hy': 'Հայերեն', 'az': 'Azərbaycan', 'uz': "O'zbek", 'kk': 'Қазақ', 'ky': 'Кыргыз', 'tg': 'Тоҷикӣ', 'tk': 'Türkmen', 'mn': 'Монгол', 'ne': 'नेपाली', 'si': 'සිංහල', 'my': 'မြန်မာ', 'km': 'ខ្មែរ', 'lo': 'ລາວ', 'am': 'አማርኛ', 'sw': 'Kiswahili', 'yo': 'Yorùbá', 'ig': 'Igbo', 'ha': 'Hausa', 'zu': 'isiZulu', 'af': 'Afrikaans', 'xh': 'isiXhosa', 'st': 'Sesotho', 'tn': 'Setswana', 'ts': 'Xitsonga', 'ss': 'siSwati', 've': 'Tshivenda', 'nr': 'isiNdebele', }; return names[locale] || locale; }; /** * 获取语言方向 */ const getLocaleDirection = (locale) => { const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug']; return rtlLocales.includes(locale) ? 'rtl' : 'ltr'; }; /** * 获取语言信息 */ const getLocaleInfo = (locale) => { return { code: locale, name: getLocaleName(locale), direction: getLocaleDirection(locale), isSupported: isLocaleSupported(locale), isCurrent: locale === currentLocale, isFallback: locale === fallbackLocale, }; }; /** * 获取所有语言信息 */ const getAllLocaleInfo = () => { return getSupportedLocales().map(getLocaleInfo); }; /** * 保存语言到本地存储 */ const saveLocale = (locale) => { if (typeof localStorage !== 'undefined') { try { localStorage.setItem('metona-toast-locale', locale); } catch (e) { console.warn('Failed to save locale:', e); } } }; /** * 从本地存储加载语言 */ const loadLocale = () => { if (typeof localStorage !== 'undefined') { try { return localStorage.getItem('metona-toast-locale') || getDefaultLocale(); } catch (e) { console.warn('Failed to load locale:', e); } } return getDefaultLocale(); }; /** * 获取默认语言 */ const getDefaultLocale = () => { if (typeof navigator !== 'undefined') { const browserLocale = navigator.language || navigator.userLanguage; if (browserLocale && isLocaleSupported(browserLocale)) { return browserLocale; } const shortLocale = browserLocale?.split('-')[0]; if (shortLocale && isLocaleSupported(shortLocale)) { return shortLocale; } } return fallbackLocale; }; /** * 初始化国际化系统 */ const initI18n = () => { const savedLocale = loadLocale(); setCurrentLocale(savedLocale); }; /** * 切换语言 */ const switchLocale = (locale) => { setCurrentLocale(locale); }; /** * 添加语言监听器 */ const addLocaleListener = (listener) => { localeListeners.add(listener); return () => { localeListeners.delete(listener); }; }; /** * 移除语言监听器 */ const removeLocaleListener = (listener) => { localeListeners.delete(listener); }; /** * 通知语言监听器 */ const notifyLocaleListeners = (locale) => { localeListeners.forEach((listener) => { try { listener(locale); } catch (e) { console.error('Locale listener error:', e); } }); }; /** * 清除所有语言监听器 */ const clearLocaleListeners = () => { localeListeners.clear(); }; /** * 格式化数字 */ const formatNumber = (number, options = {}) => { try { return new Intl.NumberFormat(currentLocale, options).format(number); } catch (e) { return number.toString(); } }; /** * 格式化货币 */ const formatCurrency = (amount, currency = 'USD', options = {}) => { try { return new Intl.NumberFormat(currentLocale, { style: 'currency', currency, ...options, }).format(amount); } catch (e) { return amount.toString(); } }; /** * 格式化百分比 */ const formatPercent = (value, options = {}) => { try { return new Intl.NumberFormat(currentLocale, { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2, ...options, }).format(value / 100); } catch (e) { return `${value}%`; } }; /** * 格式化日期 */ const formatDate = (date, options = {}) => { try { const dateObj = date instanceof Date ? date : new Date(date); return new Intl.DateTimeFormat(currentLocale, options).format(dateObj); } catch (e) { return String(date); } }; /** * 格式化时间 */ const formatTime = (date, options = {}) => { return formatDate(date, { hour: 'numeric', minute: 'numeric', second: 'numeric', ...options, }); }; /** * 格式化相对时间 */ const formatRelativeTime = (date, options = {}) => { try { const dateObj = date instanceof Date ? date : new Date(date); const now = new Date(); const diff = dateObj.getTime() - now.getTime(); const rtf = new Intl.RelativeTimeFormat(currentLocale, { numeric: 'auto', ...options, }); const units = [ { unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 }, { unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 }, { unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 }, { unit: 'day', ms: 24 * 60 * 60 * 1000 }, { unit: 'hour', ms: 60 * 60 * 1000 }, { unit: 'minute', ms: 60 * 1000 }, { unit: 'second', ms: 1000 }, ]; for (const { unit, ms } of units) { if (Math.abs(diff) >= ms || unit === 'second') { const value = Math.round(diff / ms); return rtf.format(value, unit); } } return String(date); } catch (e) { return String(date); } }; /** * 格式化列表 — 优先使用 Intl.ListFormat,降级到逗号拼接 */ const formatList = (list, _options = {}) => { const intl = Intl; if (typeof intl.ListFormat === 'function') { try { return new intl.ListFormat(currentLocale, { style: 'long', type: 'conjunction', }).format(list); } catch (_e) { /* fallback below */ } } return list.join(', '); }; /** * 格式化复数 */ const formatPlural = (count, options = {}) => { try { return new Intl.PluralRules(currentLocale, options).select(count); } catch (e) { return count === 1 ? 'one' : 'other'; } }; /** * 复数翻译 */ const plural = (key, count, params = {}) => { const pluralForm = formatPlural(count); const pluralKey = `${key}.${pluralForm}`; if (hasTranslation(pluralKey)) { return t(pluralKey, { ...params, count }); } if (hasTranslation(key)) { return t(key, { ...params, count }); } return key; }; /** * 国际化工具 — 代理层 */ const i18nUtils = { t, plural, getCurrentLocale, setCurrentLocale, switchLocale, getFallbackLocale, setFallbackLocale, hasTranslation, getTranslations, addTranslations, removeTranslation, clearTranslations, getSupportedLocales, isLocaleSupported, getLocaleName, getLocaleDirection, getLocaleInfo, getAllLocaleInfo, formatNumber, formatCurrency, formatPercent, formatDate, formatTime, formatRelativeTime, formatList, formatPlural, addLocaleListener, removeLocaleListener, clearLocaleListeners, initI18n, saveLocale, loadLocale, getDefaultLocale, }; /** * 预设语言包 */ ({ 'zh-CN': { name: '简体中文', nativeName: '简体中文', direction: 'ltr', translations: LOCALES['zh-CN'], }, 'en-US': { name: 'English (US)', nativeName: 'English (US)', direction: 'ltr', translations: LOCALES['en-US'], }, }); /** * MetonaToast Toast — Toast 类 * @module toast * @version 0.2.1 */ /** * Toast 类 — 核心通知组件 */ class Toast { /** * 设置 Toast 类级别的回调函数 */ static setCallbacks(callbacks) { if (callbacks.onError !== undefined) Toast._onError = callbacks.onError; if (callbacks.removeToast !== undefined) Toast._removeToast = callbacks.removeToast; } static on(name, fn) { if (!this._hooks.has(name)) this._hooks.set(name, []); this._hooks.get(name).push(fn); return () => this.off(name, fn); } static off(name, fn) { const list = this._hooks.get(name); if (list) this._hooks.set(name, list.filter(f => f !== fn)); } /** * 触发钩子 — 如果任意钩子返回 false 则整体返回 false */ static trigger(name, toast) { const list = this._hooks.get(name); if (!list || list.length === 0) return true; let allow = true; list.forEach(fn => { try { const result = fn(toast); if (result === false) allow = false; } catch (e) { console.error('Hook error:', name, e); if (Toast._onError) { try { Toast._onError({ hook: name, error: e, toast }); } catch (_e) { /* noop */ } } } }); return allow; } constructor(opts) { this.el = null; this.barEl = null; this.rafId = null; this.startedAt = 0; this.paused = false; this.closing = false; this.group = null; this._cleanups = []; this.id = opts.id || generateId(); this.type = opts.type || 'default'; this.title = opts.title || ''; this.message = opts.message ?? opts.content ?? ''; this.html = opts.html || ''; this.iconHTML = opts.iconHTML || ''; this.config = { ...DEFAULTS, ...opts }; // 防止嵌套对象被多个 toast 实例共享引用 if (opts.style && typeof opts.style === 'object') { this.config.style = { ...opts.style }; } this.remaining = this.config.duration || 0; this.group = opts.group || null; } _palette() { const theme = getTheme(this.config.theme || 'auto'); const c = TYPE_COLORS[this.type] || TYPE_COLORS.default; const tc = THEMES[theme] || THEMES.light; const t = { bg: tc.bg, border: tc.border, shadow: tc.shadow }; return { theme, c, t }; } create() { if (typeof window === 'undefined' || typeof document === 'undefined') return this; // beforeShow 钩子 — 返回 false 可阻止显示 if (!Toast.trigger('beforeShow', this)) return this; // onBeforeShow 配置回调 — 返回 false 可阻止显示 if (typeof this.config.onBeforeShow === 'function') { try { if (this.config.onBeforeShow(this) === false) return this; } catch (e) { console.error('onBeforeShow callback error:', e); } } injectStyles(); const container = this._getContainer(); const { theme, c, t } = this._palette(); this._limitToasts(container); const el = document.createElement('div'); el.className = this._buildClassName(theme); el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status'); el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite'); el.dataset.id = this.id; this._applyStyles(el, theme, t); this._buildContent(el, c); this.el = el; // Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore const pos = this.config.position || 'top-right'; if (pos.startsWith('top')) { container.insertBefore(el, container.firstChild); } else { container.appendChild(el); } this._bindEvents(el); this._startTimer(); // 进入动画 requestAnimationFrame(() => { requestAnimationFrame(() => { el.classList.add('met-show'); }); }); if (typeof this.config.onShow === 'function') { try { this.config.onShow(this); } catch (e) { console.error('onShow callback error:', e); } } Toast.trigger('afterShow', this); return this; } _getContainer() { const rawPosition = this.config.position || 'top-right'; const zIndex = this.config.zIndex || 9999; // RTL 语言时翻转 left/right const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl'; const position = isRTL ? rawPosition.replace('left', '__TMP__').replace('right', 'left').replace('__TMP__', 'right') : rawPosition; // 从模块级缓存获取 (由 api.ts 管理) const cached = _containerCache.get(document.body); if (cached && cached.has(position)) { return cached.get(position); } // 缓存未命中时创建新容器 if (!_containerCache.has(document.body)) { _containerCache.set(document.body, new Map()); } const el = document.createElement('div'); el.className = `met-container ${position}`; el.setAttribute('aria-label', 'Notifications'); el.setAttribute('role', 'region'); const posStyles = { 'top-left': 'top:0;left:0;align-items:flex-start', 'top-center': 'top:0;left:0;right:0;align-items:center', 'top-right': 'top:0;right:0;align-items:flex-end', 'bottom-left': 'bottom:0;left:0;align-items:flex-start', 'bottom-center': 'bottom:0;left:0;right:0;align-items:center', 'bottom-right': 'bottom:0;right:0;align-items:flex-end', }; el.style.cssText = ` display:flex; flex-direction:column; gap:${this.config.gap || 12}px; box-sizing:border-box; padding:${this.config.offset || 24}px; max-width:100vw; position:fixed; z-index:${zIndex}; pointer-events:none; ${posStyles[position] || 'top:0;right:0;align-items:flex-end'} `; document.body.appendChild(el); _containerCache.get(document.body).set(position, el); return el; } _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) { 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'; return [ 'met-toast', `met-${this.type}`, `met-theme-${theme}`, `met-anim-${anim}`, (this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '', this.config.className || '', ].filter(Boolean).join(' '); } _applyStyles(el, _theme, _t) { const widthValue = typeof this.config.width === 'number' ? `${this.config.width}px` : (typeof this.config.width === 'string' ? this.config.width : '360px'); const set = (p, v) => el.style.setProperty(p, v, 'important'); set('position', 'relative'); set('flex-shrink', '0'); set('min-width', '240px'); set('max-width', 'calc(100vw - 48px)'); set('width', widthValue); // 用户自定义样式最后应用 if (this.config.style && Object.keys(this.config.style).length > 0) { Object.entries(this.config.style).forEach(([k, v]) => { el.style[k] = v; }); } } _buildContent(el, c) { // 自定义渲染函数 — 完全接管 DOM 构建 if (typeof this.config.render === 'function') { el.innerHTML = this.config.render(this); this.barEl = el.querySelector('.met-bar, .met-bar-v'); return; } const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]); const showClose = this.config.closeButton !== false; const showProgress = this.config.showProgress !== false && (this.config.duration || 0) > 0; const showSide = (!showIcon && this.type !== 'default'); const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; const safeMessage = this.html ? `
${this.html}
` : (this.message ? `
${escapeHTML(this.message)}
` : ''); const iconHTML = showIcon ? `
${this.iconHTML || ICONS[this.type]}
` : ''; const closeHTML = showClose ? `` : ''; const progressHTML = showProgress ? (this.config.progressDirection === 'vertical' ? `
` : `
`) : ''; const sideHTML = showSide ? `
` : ''; el.innerHTML = ` ${iconHTML} ${sideHTML}
${safeTitle} ${safeMessage}
${closeHTML} ${progressHTML} `; this.barEl = el.querySelector('.met-bar, .met-bar-v'); } _bindEvents(el) { const eventHandler = (e) => { const target = e.target; if (target.closest('.met-close')) { e.stopPropagation(); this.close(); return; } if (this.config.closeOnClick && !target.closest('.met-close')) { if (typeof this.config.onClick === 'function') { try { this.config.onClick(this); } catch (err) { console.error('onClick callback error:', err); } } this.close(); return; } }; 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(); el.addEventListener('mouseenter', mouseEnter); el.addEventListener('mouseleave', mouseLeave); this._cleanups.push(() => { el.removeEventListener('mouseenter', mouseEnter); el.removeEventListener('mouseleave', mouseLeave); }); } if (this.config.draggable) { this._bindDrag(el); } } _bindDrag(el) { let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false; const down = (e) => { if (e.target.closest('.met-close')) return; dragging = true; sx = e.clientX; sy = e.clientY; el.setPointerCapture(e.pointerId); el.style.transition = 'none'; this._pause(); }; const move = (e) => { if (!dragging) return; dx = e.clientX - sx; dy = e.clientY - sy; el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`; el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200)); }; const up = (e) => { if (!dragging) return; dragging = false; el.releasePointerCapture(e.pointerId); el.style.transition = ''; if (Math.abs(dx) > 120) { el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`; el.style.opacity = '0'; setTimeout(() => this.close(true), 250); } else { el.style.transform = ''; el.style.opacity = ''; this._resume(); } dx = dy = 0; }; el.addEventListener('pointerdown', down); el.addEventListener('pointermove', move); el.addEventListener('pointerup', up); el.addEventListener('pointercancel', up); this._cleanups.push(() => { el.removeEventListener('pointerdown', down); el.removeEventListener('pointermove', move); el.removeEventListener('pointerup', up); el.removeEventListener('pointercancel', up); }); } _startTimer(resuming = false) { if ((this.config.duration || 0) <= 0) return; if (!resuming) { this.startedAt = Date.now(); this.remaining = this.config.duration || 0; } const tick = () => { if (this.paused || this.closing) return; try { const elapsed = Date.now() - this.startedAt; this.remaining = Math.max(0, (this.config.duration || 0) - elapsed); if (this.barEl) { const ratio = this.remaining / (this.config.duration || 1); const t = this.config.progressDirection === 'vertical' ? `scaleY(${ratio})` : `scaleX(${ratio})`; this.barEl.style.transform = t; } if (this.remaining <= 0) { this.close(); return; } } catch (e) { console.error('Timer tick error:', e); if (Toast._onError) { try { Toast._onError({ source: 'timer', error: e, toast: this }); } catch (_e) { /* noop */ } } } this.rafId = requestAnimationFrame(tick); }; this.rafId = requestAnimationFrame(tick); } _pause() { if (this.paused || (this.config.duration || 0) <= 0) return; this.paused = true; if (this.rafId !== null) cancelAnimationFrame(this.rafId); this.remaining = Math.max(0, (this.config.duration || 0) - (Date.now() - this.startedAt)); } _resume() { if (!this.paused) return; this.paused = false; this.startedAt = Date.now() - ((this.config.duration || 0) - this.remaining); this._startTimer(true); } update(partial) { Toast.trigger('beforeUpdate', this); const typeChanged = partial.type && partial.type !== this.type; if (partial.type) this.type = partial.type; if (partial.title !== undefined) this.title = partial.title; if (partial.message !== undefined) this.message = partial.message; if (partial.html !== undefined) this.html = partial.html; if (!this.el) { Toast.trigger('afterUpdate', this); return this; } // 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色 if (typeChanged) { const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default']; typeClasses.forEach(c => this.el.classList.remove(c)); this.el.classList.add(`met-${this.type}`); if (this.barEl) { const c = TYPE_COLORS[this.type] || TYPE_COLORS.default; this.barEl.style.background = c.fg; } const side = this.el.querySelector('.met-side'); if (side) { const c = TYPE_COLORS[this.type] || TYPE_COLORS.default; side.style.background = c.fg; } } const content = this.el.querySelector('.met-content'); if (content) { const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; const safeMessage = this.html ? `
${this.html}
` : (this.message ? `
${escapeHTML(this.message)}
` : ''); content.innerHTML = `${safeTitle}${safeMessage}`; } // resetTimerOnUpdate: 更新内容后重置计时器 if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) { if (this.rafId !== null) cancelAnimationFrame(this.rafId); this.startedAt = Date.now(); this.remaining = this.config.duration || 0; this._startTimer(); } if (typeof this.config.onUpdate === 'function') { try { this.config.onUpdate(this); } catch (e) { console.error('onUpdate callback error:', e); } } Toast.trigger('afterUpdate', this); return this; } /** * 运行时移动 Toast 到新位置 */ updatePosition(position) { if (!position || !this.el) return this; const oldContainer = this.el.parentNode; this.config.position = position; const newContainer = this._getContainer(); if (oldContainer === newContainer) return this; // 从旧容器移除,加入新容器 if (oldContainer && this.el.parentNode === oldContainer) { oldContainer.removeChild(this.el); } const pos = position; if (pos.startsWith('top')) { newContainer.insertBefore(this.el, newContainer.firstChild); } else { newContainer.appendChild(this.el); } return this; } /** * 立即从 DOM 和内存中移除(不触发离场动画) */ remove() { if (this.rafId !== null) { cancelAnimationFrame(this.rafId); this.rafId = null; } this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } }); this._cleanups = []; if (this.el && this.el.parentNode) { this.el.parentNode.removeChild(this.el); } this.el = null; if (Toast._removeToast) Toast._removeToast(this.id); } close(immediate = false) { if (this.closing) return; this.closing = true; Toast.trigger('beforeClose', this); if (this.rafId !== null) cancelAnimationFrame(this.rafId); this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } }); this._cleanups = []; const el = this.el; if (!el) { if (Toast._removeToast) Toast._removeToast(this.id); return; } // 立即脱离文档流 const container = el.parentNode; if (container && !immediate) { const elRect = el.getBoundingClientRect(); const containerRect = container.getBoundingClientRect(); el.style.position = 'absolute'; el.style.top = (elRect.top - containerRect.top) + 'px'; el.style.left = (elRect.left - containerRect.left) + 'px'; el.style.width = elRect.width + 'px'; el.style.margin = '0'; } el.classList.add('met-leaving'); el.classList.remove('met-show'); const pos = this.config.position || 'top-right'; const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl'; let transform = 'scale(.96)'; if (pos.includes('right')) transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)'; else if (pos.includes('left')) transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)'; else if (pos.startsWith('top')) transform = 'translateY(-20px)'; else transform = 'translateY(20px)'; el.style.transform = transform; el.style.opacity = '0'; setTimeout(() => this._destroy(), immediate ? 0 : 300); } _destroy() { if (this.el && this.el.parentNode) { this.el.parentNode.removeChild(this.el); } this.el = null; if (typeof this.config.onClose === 'function') { try { this.config.onClose(this); } catch (e) { console.error('onClose callback error:', e); } } Toast.trigger('afterClose', this); if (Toast._removeToast) Toast._removeToast(this.id); } } // 静态钩子系统 Toast._hooks = new Map(); // 由 api.ts 注入的回调,通过 setCallbacks() 设置 Toast._onError = null; Toast._removeToast = null; /** * 模块级共享容器缓存,所有 Toast 实例共用 */ const _containerCache = new Map(); /** * MetonaToast Templates — HTML 模板辅助函数 * @module templates * @version 0.2.1 * @description confirm / prompt / progress / action 的 DOM 模板 */ /** * 按钮行模板 — confirm/prompt 共用 */ const btnRow = (buttons) => `
${buttons}
`; /** * 确认对话框模板 */ const confirmHTML = (opts) => { const confirmText = opts.confirmText || t('confirm'); const cancelText = opts.cancelText || t('cancel'); const confirmColor = opts.confirmColor || '#10b981'; const cancelColor = opts.cancelColor || '#6b7280'; return btnRow(` `); }; /** * 输入对话框模板 */ const promptHTML = (opts) => { const submitText = opts.submitText || t('confirm'); const cancelText = opts.cancelText || t('cancel'); const submitColor = opts.submitColor || '#3b82f6'; const cancelColor = opts.cancelColor || '#6b7280'; return ` ${btnRow(` `)} `; }; /** * 进度条模板 */ const progressHTML = (opts) => { const color = opts.progressColor || '#3b82f6'; return `
0%
`; }; /** * Action Toast 模板 */ const actionHTML = (actions) => { if (!Array.isArray(actions) || actions.length === 0) return ''; const buttons = actions.map((a, i) => { const bg = a.style?.background || a.color || '#6366f1'; const cls = `met-action-btn-${i}`; return ``; }).join(''); return btnRow(buttons); }; /** * MetonaToast Plugins — 插件系统 * @module plugins * @version 0.2.1 */ /** * 插件管理器 */ class PluginManager { constructor() { this.plugins = new Map(); this.initialized = false; } register(name, plugin) { 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, 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) { 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) { return this.plugins.get(name) || null; } has(name) { return this.plugins.has(name); } getAll() { return Array.from(this.plugins.values()); } getNames() { return Array.from(this.plugins.keys()); } enable(name) { const p = this.plugins.get(name); if (p) p.enabled = true; return this; } disable(name) { const p = this.plugins.get(name); if (p) p.enabled = false; return this; } isEnabled(name) { const p = this.plugins.get(name); return p ? !!p.enabled : false; } destroy() { this.plugins.forEach((p) => { if (p.destroy) { try { p.destroy(this); } catch (_e) { /* noop */ } } }); this.plugins.clear(); } } /** * 预设插件 */ const presetPlugins = { /** * 键盘快捷键插件 — ESC 关闭所有 Toast */ keyboard: { name: 'keyboard', version: '1.0.0', description: 'ESC 关闭所有 Toast', _handler: null, install() { if (typeof document === 'undefined') return; const handler = (e) => { if (e.key === 'Escape') { document.querySelectorAll('.met-toast .met-close').forEach(btn => { try { btn.click(); } catch (_e) { /* noop */ } }); } }; this._handler = handler; document.addEventListener('keydown', handler); }, uninstall() { const handler = this._handler; if (handler && typeof document !== 'undefined') { document.removeEventListener('keydown', handler); this._handler = null; } }, }, /** * 持久化插件 — 自动保存/加载配置到 localStorage */ persistence: { name: 'persistence', version: '1.0.0', description: '自动持久化配置到 localStorage', storageKey: 'metona-toast-config', install() { if (typeof localStorage !== 'undefined') { try { const key = this.storageKey; const saved = localStorage.getItem(key); return saved ? JSON.parse(saved) : null; } catch (_e) { return null; } } return null; }, save(config) { if (typeof localStorage !== 'undefined') { try { const key = this.storageKey; localStorage.setItem(key, JSON.stringify(config)); } catch (_e) { /* noop */ } } }, uninstall() { if (typeof localStorage !== 'undefined') { try { const key = this.storageKey; localStorage.removeItem(key); } catch (_e) { /* noop */ } } }, }, /** * 无障碍插件 — 屏幕阅读器公告 */ accessibility: { name: 'accessibility', version: '1.0.0', description: '通过屏幕阅读器朗读 Toast 内容', announce(toast) { 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 = { createManager() { return new PluginManager(); }, register(name, plugin) { return defaultPluginManager.register(name, plugin); }, unregister(name) { return defaultPluginManager.unregister(name); }, get(name) { return defaultPluginManager.get(name); }, has(name) { return defaultPluginManager.has(name); }, getAll() { return defaultPluginManager.getAll(); }, getNames() { return defaultPluginManager.getNames(); }, enable(name) { return defaultPluginManager.enable(name); }, disable(name) { return defaultPluginManager.disable(name); }, isEnabled(name) { return defaultPluginManager.isEnabled(name); }, getPreset(name) { return presetPlugins[name] || null; }, getAllPresets() { return { ...presetPlugins }; }, createPlugin(config) { 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) { const errors = []; 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(); /** * MetonaToast API — meToast 核心 API 对象 * @module api * @version 0.2.1 */ /** * 参数标准化工具 */ const normalizeArgs = (args, defaultType = 'default') => { const [first, second] = args; if (typeof first === 'string') { return { ...(second || {}), type: second?.type || defaultType, message: first, }; } if (first && typeof first === 'object') { return { ...first, type: first.type || defaultType, }; } return { type: defaultType, message: '', }; }; /** 版本号常量 */ const VERSION$1 = '0.2.1'; /** * meToast API 对象 */ const meToast = { _toasts: new Map(), _config: { ...DEFAULTS }, version: '0.2.1', configure(opts) { if (!opts || typeof opts !== 'object') return this; Object.assign(this._config, opts); if (opts.theme) { applyTheme(opts.theme); } if (opts.locale) { setCurrentLocale(opts.locale); } return this; }, _emit(opts) { const merged = { ...this._config, ...opts }; if (merged.content && !merged.message) merged.message = merged.content; const t = new Toast(merged); t.create(); this._toasts.set(t.id, t); return t; }, _remove(id) { this._toasts.delete(id); }, find(id) { if (!id) return undefined; return this._toasts.get(id); }, show(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'default'); return this._emit(normalized); }, success(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'success'); return this._emit(normalized); }, error(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'error'); return this._emit(normalized); }, warning(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'warning'); return this._emit(normalized); }, info(messageOrOpts, opts = {}) { const normalized = normalizeArgs([messageOrOpts, opts], 'info'); return this._emit(normalized); }, loading(messageOrOpts, opts = {}) { 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, o = {}) => this._resolve(toast, 'success', msg || t('success'), o), error: (msg, o = {}) => this._resolve(toast, 'error', msg || t('error'), o), info: (msg, o = {}) => this._resolve(toast, 'info', msg || t('info'), o), warning: (msg, o = {}) => this._resolve(toast, 'warning', msg || t('warning'), o), update: (p) => { toast.update(p); return this; }, dismiss: () => toast.close(), }; }, promise(promise, opts = {}) { if (!promise || typeof promise.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) => { ctrl.success(successMsg); return data; }) .catch((err) => { ctrl.error(errorMsg); throw err; }); }, _resolve(loadingToast, type, message, opts) { const id = loadingToast.id; const old = this._toasts.get(id); if (!old) return null; const position = old.config.position; old.close(); return this._emit({ ...opts, type, message, position }); }, confirm(message, opts = {}) { if (typeof message !== 'string') { console.error('MeToast.confirm: message must be a string'); return Promise.resolve(false); } return new Promise((resolve) => { let resolved = false; const safeResolve = (val) => { 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'); const cancelBtn = toast.el?.querySelector('.met-cancel-btn'); if (confirmBtn) { confirmBtn.addEventListener('click', () => { toast.close(); safeResolve(true); }); } if (cancelBtn) { cancelBtn.addEventListener('click', () => { toast.close(); safeResolve(false); }); } }, 0); }); }, prompt(message, opts = {}) { if (typeof message !== 'string') { console.error('MeToast.prompt: message must be a string'); return Promise.resolve(null); } return new Promise((resolve) => { let resolved = false; const safeResolve = (val) => { 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'); const submitBtn = toast.el?.querySelector('.met-submit-btn'); const cancelBtn = toast.el?.querySelector('.met-cancel-btn'); if (input) { input.focus(); input.addEventListener('keydown', (e) => { 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, opts = {}) { 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) { const fill = toast.el?.querySelector('.met-progress-fill'); const text = toast.el?.querySelector('.met-progress-text'); if (fill) { fill.style.width = `${Math.min(100, Math.max(0, percent))}%`; } if (text) { text.textContent = `${Math.round(percent)}%`; } }, complete(message) { this.setProgress(100); setTimeout(() => { toast.update({ type: 'success', message: message || t('success'), html: '', }); setTimeout(() => toast.close(), 1000); }, 300); }, error(message) { toast.update({ type: 'error', message: message || t('error'), html: '', }); setTimeout(() => toast.close(), 2000); }, dismiss() { toast.close(); }, }; }, countdown(message, seconds = 10, opts = {}) { 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 = 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 = () => { 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, actions = [], opts = {}) { 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}`); if (btn && typeof a.onClick === 'function') { btn.addEventListener('click', () => { try { a.onClick(toast); } catch (e) { console.error('Action onClick error:', e); } if (a.close !== false) toast.close(); }); } }); }, 0); } return { id: toast.id, toast, dismiss: () => toast.close() }; }, queue(messages, opts = {}) { 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((resolve) => { let index = 0; const delay = opts.delay || 1000; const userOnClose = opts.onClose; const showNext = () => { if (cancelled || index >= messages.length) { resolve(); return; } const message = messages[index]; index++; const msg = typeof message === 'string' ? message : (message?.message || ''); const msgObj = (typeof message === 'object' && message !== null) ? message : {}; const msgOnClose = msgObj.onClose; this._emit({ ...opts, ...msgObj, message: msg, duration: msgObj.duration || opts.duration || 3000, onClose: (toast) => { if (typeof msgOnClose === 'function') { try { msgOnClose(toast); } catch (_e) { /* noop */ } } if (typeof userOnClose === 'function') { try { userOnClose(toast); } catch (_e) { /* noop */ } } setTimeout(showNext, delay); }, }); }; showNext(); }); return { then: (fn, rj) => promise.then(fn, rj), catch: (rj) => promise.catch(rj), cancel: () => { cancelled = true; }, }; }, stack(messages, opts = {}) { 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?.message || ''); this._emit({ ...opts, ...((typeof message === 'object' && message !== null) ? message : {}), message: msg, }); }, index * (opts.stagger || 100)); }); }, dismiss(id) { if (id) { const t = this._toasts.get(id); if (t) t.close(); return; } this._toasts.forEach(t => t.close()); }, /** * 立即移除指定 Toast(不触发离场动画) */ removeToast(id) { if (!id) return; const t = this._toasts.get(id); if (t) t.remove(); }, clear(position) { this._toasts.forEach(t => { if (!position || t.config.position === position) t.close(); }); }, getAll() { return new Map(this._toasts); }, count() { return this._toasts.size; }, group(name) { const self = this; const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action']; const g = { _group: name }; methods.forEach(m => { g[m] = (...args) => { const lastArg = args[args.length - 1]; const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg); const selfMethods = self; if (isObj && args.length === 1) { return selfMethods[m]({ ...lastArg, group: name }); } if (isObj) { args.pop(); return selfMethods[m](...args, { ...lastArg, group: name }); } return selfMethods[m](...args, { group: name }); }; }); g.dismiss = () => self.dismissGroup(name); g.count = () => self._groupCount(name); return g; }, dismissGroup(name) { this._toasts.forEach(t => { if (t.group === name) t.close(); }); }, _groupCount(name) { let c = 0; this._toasts.forEach(t => { if (t.group === name) c++; }); return c; }, getToasts() { return Array.from(this._toasts.values()); }, hasToasts() { return this._toasts.size > 0; }, getToast(id) { if (!id) return null; return this._toasts.get(id) || null; }, closeAll() { this._toasts.forEach(t => t.close()); }, clearAll() { this._toasts.forEach(t => t.close()); }, pauseAll() { this._toasts.forEach(t => t._pause()); }, resumeAll() { this._toasts.forEach(t => t._resume()); }, updateAll(partial) { if (partial && typeof partial === 'object') { this._toasts.forEach(t => t.update(partial)); } }, findToasts(predicate) { if (typeof predicate !== 'function') return []; return Array.from(this._toasts.values()).filter(predicate); }, findByType(type) { return this.findToasts(t => t.type === type); }, findByPosition(position) { return this.findToasts(t => t.config.position === position); }, // ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ====== init(options = {}) { 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); }); } return this; }, getStatus() { return { version: VERSION$1, 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() { return { ...this._config }; }, updateConfig(config) { if (config && typeof config === 'object') { Object.assign(this._config, config); } return this; }, resetConfig() { const keys = Object.keys(this._config); keys.forEach(k => delete this._config[k]); Object.assign(this._config, DEFAULTS); return this; }, use(plugin, options = {}) { 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 }); // 连接插件钩子 if (plugin === 'accessibility') { Toast.on('afterShow', (toast) => { const acc = preset; if (typeof acc.announce === 'function') acc.announce(toast); }); } if (plugin === 'persistence') { const saved = typeof preset.install === 'function' ? preset.install(this.plugins) : null; if (saved) this.configure(saved); Toast.on('afterClose', () => { const p = preset; if (typeof p.save === 'function') p.save(this.getConfig()); }); } } else if (plugin && typeof plugin === 'object') { this.plugins.register(plugin.name || 'custom', { ...plugin, ...options }); } return this; }, destroy() { if (this._destroyed) return; this._destroyed = true; this.dismiss(); // 清理子模块 if (this.plugins && typeof this.plugins.destroy === 'function') { this.plugins.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.cancelAll === 'function') { this.animations.cancelAll(); } // 清理 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); } this._toasts.clear(); _containerCache.clear(); }, // 子模块引用(由 index.ts 注入实际实现) animations: null, themes: null, i18n: null, plugins: null, presetPlugins: {}, }; // 连接 Toast 静态回调到 meToast 实例 Toast.setCallbacks({ onError: (info) => { const onError = meToast._config.onError; if (typeof onError === 'function') { try { onError(info); } catch (_e) { /* noop */ } } }, removeToast: (id) => { meToast._toasts.delete(id); }, }); /** * 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 * @author thzxx * @license MIT */ // 版本信息 const VERSION = '0.2.1'; /** * 增强的 MeToast 对象 — 在 meToast 基础上注入子模块 */ const enhancedMeToast = { ...meToast, version: VERSION, animations: animationUtils, themes: themeUtils, i18n: i18nUtils, plugins: pluginUtils, presetPlugins, }; // 初始化主题和国际化 if (typeof themeUtils.initTheme === 'function') { themeUtils.initTheme(); } if (typeof i18nUtils.initI18n === 'function') { i18nUtils.initI18n(); } // 浏览器环境全局注册 if (typeof window !== 'undefined') { window.MeToast = enhancedMeToast; window.Met = enhancedMeToast; // Notification API — 页面不可见时自动发送系统通知 const sendNotification = (toast) => { try { new Notification(toast.title || toast.type, { body: toast.message }); } catch (_e) { /* noop */ } }; enhancedMeToast._notify = (toast) => { if (typeof Notification === 'undefined') return; if (Notification.permission === 'denied') return; if (!document.hidden) return; if (Notification.permission === 'default') { Notification.requestPermission().then(p => { if (p === 'granted') sendNotification(toast); }); return; } sendNotification(toast); }; Toast.on('afterShow', (toast) => { const config = toast.config; if (config.notifyWhenHidden) { (enhancedMeToast._notify)(toast); } }); } exports.MeToast = enhancedMeToast; exports.Met = enhancedMeToast; exports.Toast = Toast; exports.VERSION = VERSION; exports.default = enhancedMeToast; exports.meToast = enhancedMeToast; Object.defineProperty(exports, '__esModule', { value: true }); })); //# sourceMappingURL=metona-toast.js.map