Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd061025e0 | ||
|
|
1f718c4ef8 | ||
|
|
bbff631f4f | ||
|
|
2937ce5e2d |
+2
-2
@@ -14,9 +14,9 @@
|
|||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": ["eslint:recommended"],
|
"extends": ["eslint:recommended"],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": "warn",
|
"no-console": "off",
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
|
||||||
"@typescript-eslint/no-explicit-any": "warn",
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
"prefer-const": "warn",
|
"prefer-const": "warn",
|
||||||
"no-undef": "off",
|
"no-undef": "off",
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# 统一文本文件行尾为 LF(跨平台协作)
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# 二进制文件不进行行尾转换
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.ico binary
|
||||||
|
*.gif binary
|
||||||
|
*.svg text
|
||||||
@@ -31,10 +31,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Lint
|
- name: Lint
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
continue-on-error: true
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests (coverage gate: lines >= 95%)
|
||||||
run: npx jest --forceExit --maxWorkers=2 --no-cache
|
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||||
env:
|
env:
|
||||||
NODE_OPTIONS: --max-old-space-size=4096
|
NODE_OPTIONS: --max-old-space-size=4096
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
name: Publish
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags: ['v*']
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: debian-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js 20
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
registry-url: https://git.metona.cn/api/packages/MetonaTeam/npm/
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
|
||||||
|
- name: Publish to Gitea npm registry
|
||||||
|
run: npm publish --access public
|
||||||
|
env:
|
||||||
|
NODE_AUTH_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
||||||
@@ -2,6 +2,63 @@
|
|||||||
|
|
||||||
All notable changes to MetonaToast will be documented in this file.
|
All notable changes to MetonaToast will be documented in this file.
|
||||||
|
|
||||||
|
## [0.5.0] - 2026-08-08
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **覆盖率 96.42%(行)**:新增 59 个测试用例(共 385),覆盖 DOM 交互事件(click/drag/hover/animation)、confirm/prompt/action 按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
|
||||||
|
- **测试环境重构**:SSR 分支测试用 `Object.defineProperty` 真正覆盖 document/window(jsdom getter 使此前赋值无效);新增 `tests/dom.test.ts`(jsdom 真实 DOM 事件驱动)、`tests/setup.ts`(TextEncoder 补丁)
|
||||||
|
- **jest 覆盖率门禁**:`lines >= 95%`,未达标测试即失败
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **`Toast.off` 空数组残留**:移除全部钩子后删除 Map key(此前空数组残留导致 `_hooks.has()` 误报 true)
|
||||||
|
- **`Toast.on` 返回的取消函数 this 绑定**:改用类名引用,任意调用上下文可用
|
||||||
|
- **action 按钮 `close:false` 失效**:按钮 click 冒泡到 toast 的 closeOnClick 处理器导致总是关闭,已 stopPropagation 修复
|
||||||
|
- **`use()` 重复注册钩子**:accessibility/persistence 预设的钩子注册改为防重复(先卸载旧钩子),并在插件 `uninstall` 中一并清理
|
||||||
|
- **i18n catch 引用错误**:`catch {}` 中残留 `e` 引用导致的 ReferenceError(saveLocale/loadLocale/notifyLocaleListeners)
|
||||||
|
- **删除残缺死代码**:`styles.autoApplySystemTheme`(无实际效果的占位实现)
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- **lint 零警告**:`no-console` 规则关闭(库的错误报告依赖 console,生产构建已由 terser `drop_console` 剥离);catch 变量通过 `caughtErrorsIgnorePattern` 豁免
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **Gitea Actions 发布自动化**:`publish.yml` — 推送 `v*` tag 时自动 测试+构建+发布到 Gitea npm registry(需配置 `PACKAGES_TOKEN` secret)
|
||||||
|
- **CI 强化**:lint 改为必过(移除 continue-on-error);测试命令带覆盖率门禁
|
||||||
|
|
||||||
|
## [0.4.0] - 2026-08-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **React 适配器子包**:`@metona-team/metona-toast/react` — `useToast()` hook(组件卸载时自动移除本组件创建的 Toast)+ 声明式 `<Toast />` 组件(props 变化更新、卸载自动清理)。主包保持零依赖,`react` 为 optional peerDependency
|
||||||
|
- **`dedupe` 预设插件**:相同 type+message 的 Toast 自动去重(更新已有实例而非重复弹出),支持 uninstall
|
||||||
|
- **`dragThreshold` 配置项**:拖拽关闭阈值可配置(默认 120px,原为硬编码)
|
||||||
|
- **RTL 完整适配**:容器设置 `dir="rtl"`,进度条/side 条/类型色条/圆角正确镜像
|
||||||
|
- **`update({ duration })` 支持**:动态变更显示时长并自动重启/停止计时器(此前 update 忽略 duration)
|
||||||
|
- **幽灵实例修复**:`_emit` 检查 `create()` 结果,beforeShow 拦截后的 toast 不再注册到 `_toasts`(此前会残留幽灵记录,影响 count 与去重类插件)
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **loading 链式转换 id 稳定**:`loading.success/error/info/warning` 由"关闭旧 + 新建"改为原地 `update()` 同一实例,返回的 Toast id 与 loading id 一致;转换后 duration 自动恢复默认值(自动关闭)。注:转换不再产生新的入场动画,改为平滑更新;关闭按钮/进度条保持 loading 配置(如需默认外观,请通过 opts 覆盖)
|
||||||
|
|
||||||
|
## [0.3.0] - 2026-08-08
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **自定义动画真实生效**:`animations.register()` 注册的动画此前仅写入内存 Map,实际渲染始终 fallback 到 slide。现在注册表驱动 `_buildClassName` 并动态注入对应 `@keyframes` + 类规则,README 的注册教程名实相符
|
||||||
|
- **`max` 超限真正关闭最早的 toast**:此前超限时仅从内存 Map 删除记录,DOM 上的旧 toast 依然显示。现在通过实例注册表找到最早的 toast 并真正 `close()`(触发离场动画与生命周期回调)
|
||||||
|
- **`destroy()` 全量清理**:新增清理 `Toast._hooks` 钩子、`Toast._registry` 实例注册表、`metona-toast-theme-css` / `metona-toast-custom-styles` / `metona-toast-anim-styles` 样式标签;动画注册表改为真正清空
|
||||||
|
- **`applyThemeVariables` 重复注入**:修复多次调用追加相同 id 的 style 标签问题(改为先移除旧元素)
|
||||||
|
- **`init()` 复活**:`destroy()` 后调用 `init()` 重置 `_destroyed` 标志,恢复可初始化状态
|
||||||
|
- **失效脚本清理**:删除 `docs`(typedoc 未安装)、`example`(examples/ 目录不存在)脚本,新增 `serve` 脚本
|
||||||
|
- **`AnimationConfig.enter/leave` 类型放宽**:支持 `opacity: 0` 等数值属性(此前仅 `string`,README 示例在 TS 严格模式下报错)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- **钩子系统完整化**:此前 `HOOK_NAMES` 声明 20+ 钩子但多数永不触发。现在全部声明均有触发点:
|
||||||
|
- 拦截语义:`beforeClose` / `beforeUpdate` 返回 `false` 可分别阻止关闭、更新
|
||||||
|
- 新触发点:`click` / `hover` / `dragStart` / `dragEnd` / `animationStart` / `animationEnd` / `progressStart` / `progressEnd` / `configChange` / `themeChange` / `localeChange` / `beforeInit` / `afterInit` / `beforeDestroy` / `afterDestroy`
|
||||||
|
- **`init({ plugins })` 支持插件对象**:与 `use()` 一致,可传 `string | Plugin`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **VERSION 唯一来源**:版本号统一在 `src/constants.ts` 维护,`api.ts` / `index.ts` 引用,消除三处硬编码漂移
|
||||||
|
- **`HOOK_NAMES` 收敛**:移除永不触发的 `progressUpdate` / `custom` 声明
|
||||||
|
- 版本升级至 **v0.3.0**(含行为增强,按 semver minor 处理)
|
||||||
|
|
||||||
## [0.2.1] - 2026-07-25
|
## [0.2.1] - 2026-07-25
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -5,18 +5,21 @@
|
|||||||
[](https://www.npmjs.com/package/@metona-team/metona-toast)
|
[](https://www.npmjs.com/package/@metona-team/metona-toast)
|
||||||
[](https://bundlephobia.com/package/@metona-team/metona-toast)
|
[](https://bundlephobia.com/package/@metona-team/metona-toast)
|
||||||
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
||||||
|
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB
|
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB,无任何运行时依赖
|
||||||
- **107 种图标类型** — success/error/warning/info/loading 及更多扩展类型,覆盖所有常见场景
|
- **107 种图标类型** — success/error/warning/info/loading 及 100+ 扩展类型,覆盖所有常见场景
|
||||||
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,每种效果明显不同
|
- **11 种动画 + 自定义** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,`animations.register()` 可注册任意自定义动画(动态注入 keyframes,真实生效)
|
||||||
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
|
- **主题系统** — light/dark/auto/warm 四种内置主题,`registerTheme()` 注册自定义主题,跟随系统主题自动切换
|
||||||
- **国际化** — 内置 zh-CN / en-US 完整翻译(去重优化),可通过 `addTranslations()` 扩展
|
- **国际化** — 内置 zh-CN / en-US 完整翻译,`addTranslations()` 扩展任意语言,RTL 语言完整适配
|
||||||
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
|
- **插件系统** — 4 款预设插件 (keyboard / persistence / accessibility / dedupe) + 自定义插件
|
||||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
|
- **完整钩子系统** — 17 个生命周期与事件钩子,`beforeShow` / `beforeClose` / `beforeUpdate` 支持返回 `false` 拦截
|
||||||
- **TypeScript** — 严格模式,源码级类型安全,类型从源码自动生成无手动维护
|
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭,阈值 `dragThreshold` 可配置
|
||||||
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入监控系统
|
- **React 适配器** — `@metona-team/metona-toast/react` 子路径导入,`useToast()` hook + 声明式 `<Toast />` 组件,主包保持零依赖
|
||||||
|
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统
|
||||||
|
- **工程质量** — 385 个测试全过,行覆盖率 96.42%,lint 零警告,CI 含覆盖率门禁(≥95%)
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
@@ -59,13 +62,13 @@ const MeToast = require('@metona-team/metona-toast');
|
|||||||
| iOS Safari | 11+ |
|
| iOS Safari | 11+ |
|
||||||
| Android Chrome | 64+ |
|
| Android Chrome | 64+ |
|
||||||
| Node.js | 16+(ESM & CJS 双格式) |
|
| Node.js | 16+(ESM & CJS 双格式) |
|
||||||
| TypeScript | 4.5+(完整类型声明) |
|
| TypeScript | 4.5+(完整类型声明,类型从源码自动生成) |
|
||||||
|
|
||||||
核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
|
核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
浏览器中 `window` 上会注册两个全局变量:`MeToast` 和 `Met`,两者完全等价,任选其一使用。
|
浏览器 `window` 上注册两个全局变量:`MeToast` 和 `Met`,完全等价。
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import MeToast from '@metona-team/metona-toast';
|
import MeToast from '@metona-team/metona-toast';
|
||||||
@@ -82,11 +85,11 @@ MeToast.info('系统维护中');
|
|||||||
// 带标题
|
// 带标题
|
||||||
MeToast.success({ title: '保存成功', message: '文件已同步到云端' });
|
MeToast.success({ title: '保存成功', message: '文件已同步到云端' });
|
||||||
|
|
||||||
// 加载 → 成功链式转换
|
// 加载 → 链式转换(原地更新同一实例,id 稳定)
|
||||||
const loading = MeToast.loading('正在提交...');
|
const loading = MeToast.loading('正在提交...');
|
||||||
setTimeout(() => loading.success('提交成功!'), 2000);
|
setTimeout(() => loading.success('提交成功!'), 2000);
|
||||||
|
|
||||||
// Promise 风格
|
// Promise 风格 — 自动 loading → success/error
|
||||||
await MeToast.promise(fetch('/api/data'), {
|
await MeToast.promise(fetch('/api/data'), {
|
||||||
loading: '加载中...',
|
loading: '加载中...',
|
||||||
success: '加载完成!',
|
success: '加载完成!',
|
||||||
@@ -100,15 +103,22 @@ await MeToast.promise(fetch('/api/data'), {
|
|||||||
MeToast.configure({
|
MeToast.configure({
|
||||||
position: 'top-right', // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
|
position: 'top-right', // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
|
||||||
duration: 4000, // 显示时长(ms),0 为不自动关闭
|
duration: 4000, // 显示时长(ms),0 为不自动关闭
|
||||||
max: 6, // 同时最多显示条数
|
max: 6, // 同一位置最多显示条数,超出关闭最早的
|
||||||
theme: 'auto', // light | dark | auto | warm
|
gap: 12, // Toast 间距(px)
|
||||||
animation: 'slide', // slide | fade | scale | bounce | flip | rotate | zoom | slideUp | slideDown | slideLeft | slideRight
|
offset: 24, // 容器距屏幕边缘(px)
|
||||||
|
theme: 'auto', // light | dark | auto(跟随系统)| warm
|
||||||
|
animation: 'slide', // 11 种内置动画或自定义动画名
|
||||||
pauseOnHover: true, // 悬停暂停计时
|
pauseOnHover: true, // 悬停暂停计时
|
||||||
closeOnClick: true, // 点击关闭
|
closeOnClick: true, // 点击关闭
|
||||||
showProgress: true, // 显示进度条
|
|
||||||
draggable: true, // 允许拖拽关闭
|
draggable: true, // 允许拖拽关闭
|
||||||
|
dragThreshold: 120, // 拖拽关闭阈值(px)
|
||||||
|
showProgress: true, // 显示进度条
|
||||||
|
progressDirection: 'horizontal', // horizontal | vertical
|
||||||
|
zIndex: 9999,
|
||||||
|
width: 360,
|
||||||
locale: 'zh-CN', // 语言
|
locale: 'zh-CN', // 语言
|
||||||
onError: ({ hook, error, toast }) => { // 全局错误回调(钩子/定时器异常)
|
onError: ({ hook, source, error, toast }) => {
|
||||||
|
// 钩子/定时器异常全局捕获,可接入监控系统
|
||||||
console.error('Toast error:', hook, error);
|
console.error('Toast error:', hook, error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -116,40 +126,42 @@ MeToast.configure({
|
|||||||
|
|
||||||
## 动画效果
|
## 动画效果
|
||||||
|
|
||||||
11 种动画,每种感官差异明显:
|
11 种内置动画,每种感官差异明显:
|
||||||
|
|
||||||
| 动画 | 效果描述 |
|
| 动画 | 效果描述 | 时长 |
|
||||||
|------|----------|
|
|------|----------|------|
|
||||||
| `slide` | 从右侧滑入 + 轻微过冲回弹 |
|
| `slide` | 从右侧滑入 + 轻微过冲回弹 | 400ms |
|
||||||
| `fade` | 纯淡入(blur→清晰),从容优雅 |
|
| `fade` | 纯淡入(blur→清晰),从容优雅 | 500ms |
|
||||||
| `scale` | 弹性放大(0.55→1.07→1.0) |
|
| `scale` | 弹性放大(0.55→1.07→1.0) | 450ms |
|
||||||
| `bounce` | 从天而降四段弹跳 |
|
| `bounce` | 从天而降四段弹跳 | 650ms |
|
||||||
| `flip` | 3D 翻转入场 + 回摆 |
|
| `flip` | 3D 翻转入场 + 回摆 | 500ms |
|
||||||
| `rotate` | 旋转摇摆进入 |
|
| `rotate` | 旋转摇摆进入 | 500ms |
|
||||||
| `zoom` | 从中心爆发式弹出 |
|
| `zoom` | 从中心爆发式弹出 | 500ms |
|
||||||
| `slideUp` | 从下方弹入 |
|
| `slideUp` | 从下方弹入 | 400ms |
|
||||||
| `slideDown` | 从上方弹入 |
|
| `slideDown` | 从上方弹入 | 400ms |
|
||||||
| `slideLeft` | 从左侧滑入 |
|
| `slideLeft` | 从左侧滑入 | 400ms |
|
||||||
| `slideRight` | 从右侧滑入 |
|
| `slideRight` | 从右侧滑入 | 400ms |
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
MeToast.success('弹跳动画', { animation: 'bounce' });
|
MeToast.success('弹跳动画', { animation: 'bounce' });
|
||||||
|
|
||||||
// 注册自定义动画
|
// 注册自定义动画 — 动态注入 keyframes,真实生效
|
||||||
MeToast.animations.register('myAnim', {
|
MeToast.animations.register('myAnim', {
|
||||||
enter: { transform: 'rotate(-30deg) scale(0.5)', opacity: 0 },
|
enter: { transform: 'rotate(-30deg) scale(0.5)', opacity: 0 },
|
||||||
leave: { transform: 'rotate(0) scale(1)', opacity: 1 },
|
leave: { transform: 'rotate(0) scale(1)', opacity: 1 },
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
});
|
});
|
||||||
|
MeToast.success('自定义动画', { animation: 'myAnim' });
|
||||||
```
|
```
|
||||||
|
|
||||||
## 主题
|
## 主题
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
// 切换主题
|
// 切换主题(自动持久化到 localStorage)
|
||||||
MeToast.themes.switchTheme('dark');
|
MeToast.themes.switchTheme('dark');
|
||||||
MeToast.themes.switchTheme('warm');
|
MeToast.themes.switchTheme('warm');
|
||||||
|
MeToast.themes.toggleTheme(); // light ↔ dark
|
||||||
|
|
||||||
// 注册自定义主题
|
// 注册自定义主题
|
||||||
MeToast.themes.registerTheme('ocean', {
|
MeToast.themes.registerTheme('ocean', {
|
||||||
@@ -188,6 +200,18 @@ await MeToast.queue(['步骤一', '步骤二', '步骤三'], { delay: 800 });
|
|||||||
|
|
||||||
// 堆叠展示(同时错峰)
|
// 堆叠展示(同时错峰)
|
||||||
MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
||||||
|
|
||||||
|
// Action Toast — 内嵌操作按钮(close:false 可多次点击不关闭)
|
||||||
|
MeToast.action('文件已删除', [
|
||||||
|
{ text: '撤销', onClick: () => restore(), color: '#3b82f6' },
|
||||||
|
{ text: '查看', onClick: () => open(), color: '#10b981', close: false },
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 分组管理
|
||||||
|
const orders = MeToast.group('orders');
|
||||||
|
orders.success('订单已创建');
|
||||||
|
orders.count(); // 该组 toast 数量
|
||||||
|
orders.dismiss(); // 一键关闭整组
|
||||||
```
|
```
|
||||||
|
|
||||||
## 插件
|
## 插件
|
||||||
@@ -197,8 +221,9 @@ MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
|||||||
MeToast.use('keyboard'); // ESC 关闭所有 Toast
|
MeToast.use('keyboard'); // ESC 关闭所有 Toast
|
||||||
MeToast.use('persistence'); // 配置持久化到 localStorage
|
MeToast.use('persistence'); // 配置持久化到 localStorage
|
||||||
MeToast.use('accessibility'); // 屏幕阅读器朗读公告
|
MeToast.use('accessibility'); // 屏幕阅读器朗读公告
|
||||||
|
MeToast.use('dedupe'); // 相同 type+message 自动去重(更新已有实例)
|
||||||
|
|
||||||
// 自定义插件(通过 install 中注册 Toast 生命周期钩子)
|
// 自定义插件(install 中注册生命周期钩子)
|
||||||
import MeToast, { Toast } from '@metona-team/metona-toast';
|
import MeToast, { Toast } from '@metona-team/metona-toast';
|
||||||
|
|
||||||
MeToast.use({
|
MeToast.use({
|
||||||
@@ -208,6 +233,36 @@ MeToast.use({
|
|||||||
console.log('Toast shown:', toast.message);
|
console.log('Toast shown:', toast.message);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
uninstall() {
|
||||||
|
console.log('plugin removed');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 钩子系统
|
||||||
|
|
||||||
|
`Toast.on(name, handler)` 注册钩子,返回取消函数。`beforeShow` / `beforeClose` / `beforeUpdate` 的 handler 返回 `false` 可拦截对应操作。
|
||||||
|
|
||||||
|
| 钩子 | 触发时机 | 拦截 |
|
||||||
|
|------|----------|------|
|
||||||
|
| `beforeInit` / `afterInit` | `init()` 前后 | — |
|
||||||
|
| `beforeDestroy` / `afterDestroy` | `destroy()` 前后 | — |
|
||||||
|
| `beforeShow` / `afterShow` | Toast 显示前后 | ✅ 返回 false 阻止显示 |
|
||||||
|
| `beforeClose` / `afterClose` | Toast 关闭前后 | ✅ 返回 false 阻止关闭 |
|
||||||
|
| `beforeUpdate` / `afterUpdate` | `update()` 前后 | ✅ 返回 false 阻止更新 |
|
||||||
|
| `configChange` | `configure`/`updateConfig`/`resetConfig` | — |
|
||||||
|
| `themeChange` | 主题切换 | — |
|
||||||
|
| `localeChange` | 语言切换 | — |
|
||||||
|
| `click` | 点击 Toast 主体 | — |
|
||||||
|
| `hover` | 鼠标悬停进出 | — |
|
||||||
|
| `dragStart` / `dragEnd` | 拖拽开始/结束 | — |
|
||||||
|
| `animationStart` / `animationEnd` | 入场动画开始/结束 | — |
|
||||||
|
| `progressStart` / `progressEnd` | 倒计时开始/归零 | — |
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 拦截示例:指定类型禁止显示
|
||||||
|
Toast.on('beforeShow', (toast) => {
|
||||||
|
if (toast.type === 'error' && !isAllowed) return false;
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -216,7 +271,7 @@ MeToast.use({
|
|||||||
```javascript
|
```javascript
|
||||||
MeToast.i18n.switchLocale('en-US');
|
MeToast.i18n.switchLocale('en-US');
|
||||||
|
|
||||||
// 添加语言
|
// 添加语言(嵌套结构支持深度合并)
|
||||||
MeToast.i18n.addTranslations('ja', {
|
MeToast.i18n.addTranslations('ja', {
|
||||||
success: '成功',
|
success: '成功',
|
||||||
error: 'エラー',
|
error: 'エラー',
|
||||||
@@ -224,9 +279,11 @@ MeToast.i18n.addTranslations('ja', {
|
|||||||
cancel: 'キャンセル',
|
cancel: 'キャンセル',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 格式化工具
|
// 格式化工具(基于 Intl)
|
||||||
MeToast.i18n.formatNumber(1234567); // "1,234,567"
|
MeToast.i18n.formatNumber(1234567); // "1,234,567"
|
||||||
MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00"
|
MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00"
|
||||||
|
MeToast.i18n.formatDate(new Date());
|
||||||
|
MeToast.i18n.formatRelativeTime(Date.now() + 3600000); // "1 小时后"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Toast 管理
|
## Toast 管理
|
||||||
@@ -235,11 +292,17 @@ MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00"
|
|||||||
MeToast.count(); // 当前数量
|
MeToast.count(); // 当前数量
|
||||||
MeToast.getAll(); // 获取原生 Map<id, ToastInstance>
|
MeToast.getAll(); // 获取原生 Map<id, ToastInstance>
|
||||||
MeToast.getToasts(); // 获取所有实例数组
|
MeToast.getToasts(); // 获取所有实例数组
|
||||||
|
MeToast.find(id); // 查找实例(不存在返回 undefined)
|
||||||
|
MeToast.findByType(type); // 按类型过滤
|
||||||
|
MeToast.findByPosition(pos);// 按位置过滤
|
||||||
MeToast.dismiss(); // 关闭所有
|
MeToast.dismiss(); // 关闭所有
|
||||||
MeToast.dismiss(id); // 关闭指定
|
MeToast.dismiss(id); // 关闭指定
|
||||||
|
MeToast.removeToast(id); // 立即移除(无离场动画)
|
||||||
|
MeToast.clear(position?); // 按位置清除
|
||||||
MeToast.pauseAll(); // 暂停所有计时
|
MeToast.pauseAll(); // 暂停所有计时
|
||||||
MeToast.resumeAll(); // 恢复所有计时
|
MeToast.resumeAll(); // 恢复所有计时
|
||||||
MeToast.destroy(); // 完全销毁(支持重复调用)
|
MeToast.updateAll(partial); // 批量更新
|
||||||
|
MeToast.destroy(); // 完全销毁(支持重复调用,init() 可恢复)
|
||||||
```
|
```
|
||||||
|
|
||||||
## 回调
|
## 回调
|
||||||
@@ -247,6 +310,7 @@ MeToast.destroy(); // 完全销毁(支持重复调用)
|
|||||||
```javascript
|
```javascript
|
||||||
MeToast.success({
|
MeToast.success({
|
||||||
message: '操作成功',
|
message: '操作成功',
|
||||||
|
onBeforeShow: (toast) => true, // 返回 false 阻止显示
|
||||||
onShow: (toast) => console.log('显示:', toast.id),
|
onShow: (toast) => console.log('显示:', toast.id),
|
||||||
onClose: (toast) => console.log('关闭:', toast.id),
|
onClose: (toast) => console.log('关闭:', toast.id),
|
||||||
onClick: (toast) => console.log('点击:', toast.id),
|
onClick: (toast) => console.log('点击:', toast.id),
|
||||||
@@ -256,12 +320,53 @@ MeToast.success({
|
|||||||
// 全局错误回调 — 捕获钩子异常和定时器错误
|
// 全局错误回调 — 捕获钩子异常和定时器错误
|
||||||
MeToast.configure({
|
MeToast.configure({
|
||||||
onError: ({ hook, source, error, toast }) => {
|
onError: ({ hook, source, error, toast }) => {
|
||||||
// 接入监控系统 (Sentry, DataDog 等)
|
|
||||||
reportError(error, { hook, source, toastId: toast?.id });
|
reportError(error, { hook, source, toastId: toast?.id });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## React 适配器
|
||||||
|
|
||||||
|
主包保持零依赖;React 适配器通过子路径导入,`react` 为 optional peerDependency,未使用 React 的项目不受影响。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @metona-team/metona-toast react
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useToast, Toast } from '@metona-team/metona-toast/react';
|
||||||
|
|
||||||
|
function SubmitButton() {
|
||||||
|
// 本组件创建的 Toast 会在组件卸载时自动移除
|
||||||
|
const toast = useToast();
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
const loading = toast.loading('正在提交...');
|
||||||
|
try {
|
||||||
|
await api.submit();
|
||||||
|
loading.success('提交成功!');
|
||||||
|
} catch (e) {
|
||||||
|
loading.error('提交失败');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return <button onClick={submit}>提交</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 声明式 Toast — props 变化时更新内容,组件卸载时自动移除(autoClose 默认 true)
|
||||||
|
function SaveIndicator({ saving }: { saving: boolean }) {
|
||||||
|
return saving ? <Toast type="info" message="正在保存..." /> : null;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工程质量
|
||||||
|
|
||||||
|
- **385 个测试** 全部通过(单元 + DOM 交互 + React 渲染 + SSR 环境隔离)
|
||||||
|
- **行覆盖率 96.42%**,jest 覆盖率门禁 `lines >= 95%`,未达标构建即失败
|
||||||
|
- **lint 零警告**(TypeScript ESLint 严格检查)
|
||||||
|
- **CI**(Gitea Actions):Node 18/20/22/24 矩阵 — typecheck → lint → 测试+覆盖率门禁 → 构建 → bundle size 检查
|
||||||
|
- **自动发布**:推送 `v*` tag 触发测试、构建并发布到 Gitea npm registry
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|||||||
Vendored
+356
-120
@@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Utils — 工具函数
|
* MetonaToast Utils — 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 生成唯一ID
|
* 生成唯一ID
|
||||||
@@ -42,7 +42,7 @@ const prefersDark = () => {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Icons — 图标SVG定义
|
* MetonaToast Icons — 图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 107 个内置 SVG 图标
|
* @description 107 个内置 SVG 图标
|
||||||
*/
|
*/
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -539,7 +539,7 @@ const ICONS = {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Locales — 国际化翻译数据
|
* MetonaToast Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 内置 zh-CN / en-US 完整翻译
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
const LOCALES = {
|
const LOCALES = {
|
||||||
@@ -1026,8 +1026,12 @@ const LOCALES = {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Constants — 常量定义
|
* MetonaToast Constants — 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 版本号 — 唯一来源,发布时只需修改此处
|
||||||
|
*/
|
||||||
|
const VERSION = '0.5.0';
|
||||||
/**
|
/**
|
||||||
* 默认配置
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
@@ -1040,6 +1044,7 @@ const DEFAULTS = Object.freeze({
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
dragThreshold: 120,
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
progressDirection: 'horizontal',
|
progressDirection: 'horizontal',
|
||||||
icon: true,
|
icon: true,
|
||||||
@@ -1285,11 +1290,15 @@ const THEMES = {
|
|||||||
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 动画类型
|
||||||
|
*/
|
||||||
|
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetonaToast Styles — 样式管理
|
* MetonaToast Styles — 样式管理
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 样式缓存
|
// 样式缓存
|
||||||
let styleElement = null;
|
let styleElement = null;
|
||||||
@@ -1647,6 +1656,41 @@ const generateCSS = () => {
|
|||||||
transition: transform 0.1s linear;
|
transition: transform 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||||
|
[dir="rtl"] .met-bar {
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-side {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-progress-v {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-success {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-error {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #ef4444;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-warning {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-info {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-loading {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
.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 { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||||
@@ -1743,7 +1787,7 @@ const injectStyles = () => {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Themes — 主题管理
|
* MetonaToast Themes — 主题管理
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 当前主题状态
|
// 当前主题状态
|
||||||
let currentTheme = 'auto';
|
let currentTheme = 'auto';
|
||||||
@@ -2087,7 +2131,7 @@ const themeUtils = {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast i18n — 国际化管理
|
* MetonaToast i18n — 国际化管理
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 当前语言状态
|
// 当前语言状态
|
||||||
let currentLocale = 'zh-CN';
|
let currentLocale = 'zh-CN';
|
||||||
@@ -2455,7 +2499,7 @@ const formatNumber = (number, options = {}) => {
|
|||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2470,7 +2514,7 @@ const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
|||||||
...options,
|
...options,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return amount.toString();
|
return amount.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2486,7 +2530,7 @@ const formatPercent = (value, options = {}) => {
|
|||||||
...options,
|
...options,
|
||||||
}).format(value / 100);
|
}).format(value / 100);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return `${value}%`;
|
return `${value}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2498,7 +2542,7 @@ const formatDate = (date, options = {}) => {
|
|||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2542,7 +2586,7 @@ const formatRelativeTime = (date, options = {}) => {
|
|||||||
}
|
}
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2569,7 +2613,7 @@ const formatPlural = (count, options = {}) => {
|
|||||||
try {
|
try {
|
||||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return count === 1 ? 'one' : 'other';
|
return count === 1 ? 'one' : 'other';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2643,10 +2687,126 @@ const i18nUtils = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetonaToast Animations — 动画管理
|
||||||
|
* @module animations
|
||||||
|
* @version 0.2.1
|
||||||
|
*/
|
||||||
|
// 动画缓存
|
||||||
|
const animationMap = new Map();
|
||||||
|
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
||||||
|
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
|
||||||
|
// 动态注入的自定义动画样式(惰性创建)
|
||||||
|
let animStyleElement = null;
|
||||||
|
const injectedAnimations = new Set();
|
||||||
|
/**
|
||||||
|
* 将 CSS 属性对象转换为内联 CSS 字符串
|
||||||
|
*/
|
||||||
|
const toCss = (props) => Object.entries(props)
|
||||||
|
.map(([key, value]) => `${key}:${value}`)
|
||||||
|
.join(';');
|
||||||
|
/**
|
||||||
|
* 为自定义动画生成入场 keyframes + 类规则
|
||||||
|
*/
|
||||||
|
const buildAnimationCSS = (name, config) => {
|
||||||
|
const enter = toCss(config.enter);
|
||||||
|
return `
|
||||||
|
@keyframes met-${name}-in {
|
||||||
|
from { ${enter}; }
|
||||||
|
}
|
||||||
|
.met-anim-${name}.met-toast { opacity: 0; }
|
||||||
|
.met-anim-${name}.met-toast.met-show {
|
||||||
|
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
||||||
|
*/
|
||||||
|
const ensureAnimationCSS = (name) => {
|
||||||
|
if (typeof document === 'undefined')
|
||||||
|
return;
|
||||||
|
if (BUILTIN_ANIMATIONS.has(name))
|
||||||
|
return;
|
||||||
|
const config = animationMap.get(name);
|
||||||
|
if (!config || injectedAnimations.has(name))
|
||||||
|
return;
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.getElementById('metona-toast-anim-styles');
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.createElement('style');
|
||||||
|
animStyleElement.id = 'metona-toast-anim-styles';
|
||||||
|
document.head.appendChild(animStyleElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animStyleElement.textContent += buildAnimationCSS(name, config);
|
||||||
|
injectedAnimations.add(name);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 检查动画是否存在(内置或自定义)
|
||||||
|
*/
|
||||||
|
const hasAnimation = (name) => animationMap.has(name);
|
||||||
|
// 注册默认动画
|
||||||
|
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();
|
||||||
|
injectedAnimations.clear();
|
||||||
|
animStyleElement = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetonaToast Toast — Toast 类
|
* MetonaToast Toast — Toast 类
|
||||||
* @module toast
|
* @module toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Toast 类 — 核心通知组件
|
* Toast 类 — 核心通知组件
|
||||||
@@ -2665,17 +2825,25 @@ class Toast {
|
|||||||
if (!this._hooks.has(name))
|
if (!this._hooks.has(name))
|
||||||
this._hooks.set(name, []);
|
this._hooks.set(name, []);
|
||||||
this._hooks.get(name).push(fn);
|
this._hooks.get(name).push(fn);
|
||||||
return () => this.off(name, fn);
|
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||||
|
return () => Toast.off(name, fn);
|
||||||
}
|
}
|
||||||
static off(name, fn) {
|
static off(name, fn) {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (list)
|
if (list) {
|
||||||
this._hooks.set(name, list.filter(f => f !== fn));
|
const next = list.filter(f => f !== fn);
|
||||||
|
// 空数组直接删除 key,避免残留
|
||||||
|
if (next.length === 0)
|
||||||
|
this._hooks.delete(name);
|
||||||
|
else
|
||||||
|
this._hooks.set(name, next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||||
|
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||||
*/
|
*/
|
||||||
static trigger(name, toast) {
|
static trigger(name, toast = null) {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (!list || list.length === 0)
|
if (!list || list.length === 0)
|
||||||
return true;
|
return true;
|
||||||
@@ -2690,7 +2858,7 @@ class Toast {
|
|||||||
console.error('Hook error:', name, e);
|
console.error('Hook error:', name, e);
|
||||||
if (Toast._onError) {
|
if (Toast._onError) {
|
||||||
try {
|
try {
|
||||||
Toast._onError({ hook: name, error: e, toast });
|
Toast._onError({ hook: name, error: e, toast: toast });
|
||||||
}
|
}
|
||||||
catch (_e) { /* noop */ }
|
catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
@@ -2745,6 +2913,7 @@ class Toast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
ensureAnimationCSS(this.config.animation || 'slide');
|
||||||
const container = this._getContainer();
|
const container = this._getContainer();
|
||||||
const { theme, c, t } = this._palette();
|
const { theme, c, t } = this._palette();
|
||||||
this._limitToasts(container);
|
this._limitToasts(container);
|
||||||
@@ -2764,6 +2933,7 @@ class Toast {
|
|||||||
else {
|
else {
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
}
|
}
|
||||||
|
Toast._registry.set(this.id, this);
|
||||||
this._bindEvents(el);
|
this._bindEvents(el);
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
// 进入动画
|
// 进入动画
|
||||||
@@ -2804,6 +2974,9 @@ class Toast {
|
|||||||
el.className = `met-container ${position}`;
|
el.className = `met-container ${position}`;
|
||||||
el.setAttribute('aria-label', 'Notifications');
|
el.setAttribute('aria-label', 'Notifications');
|
||||||
el.setAttribute('role', 'region');
|
el.setAttribute('role', 'region');
|
||||||
|
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||||
|
if (isRTL)
|
||||||
|
el.setAttribute('dir', 'rtl');
|
||||||
const posStyles = {
|
const posStyles = {
|
||||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||||
'top-center': 'top:0;left:0;right:0;align-items:center',
|
'top-center': 'top:0;left:0;right:0;align-items:center',
|
||||||
@@ -2831,18 +3004,22 @@ class Toast {
|
|||||||
_limitToasts(container) {
|
_limitToasts(container) {
|
||||||
const max = this.config.max || 6;
|
const max = this.config.max || 6;
|
||||||
const list = Array.from(container.querySelectorAll('.met-toast'));
|
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||||
if (list.length >= max) {
|
while (list.length >= max) {
|
||||||
const first = list[0];
|
const el = list.shift();
|
||||||
const id = first?.dataset.id;
|
const id = el?.dataset.id;
|
||||||
if (id && Toast._removeToast) {
|
if (!id)
|
||||||
|
continue;
|
||||||
|
const existing = Toast._registry.get(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.close(true);
|
||||||
|
}
|
||||||
|
else if (Toast._removeToast) {
|
||||||
Toast._removeToast(id);
|
Toast._removeToast(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_buildClassName(theme) {
|
_buildClassName(theme) {
|
||||||
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
|
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
|
||||||
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
|
||||||
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
|
|
||||||
return [
|
return [
|
||||||
'met-toast',
|
'met-toast',
|
||||||
`met-${this.type}`,
|
`met-${this.type}`,
|
||||||
@@ -2919,6 +3096,7 @@ class Toast {
|
|||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Toast.trigger('click', this);
|
||||||
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||||
if (typeof this.config.onClick === 'function') {
|
if (typeof this.config.onClick === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -2935,8 +3113,14 @@ class Toast {
|
|||||||
el.addEventListener('click', eventHandler);
|
el.addEventListener('click', eventHandler);
|
||||||
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||||
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||||
const mouseEnter = () => this._pause();
|
const mouseEnter = () => {
|
||||||
const mouseLeave = () => this._resume();
|
Toast.trigger('hover', this);
|
||||||
|
this._pause();
|
||||||
|
};
|
||||||
|
const mouseLeave = () => {
|
||||||
|
Toast.trigger('hover', this);
|
||||||
|
this._resume();
|
||||||
|
};
|
||||||
el.addEventListener('mouseenter', mouseEnter);
|
el.addEventListener('mouseenter', mouseEnter);
|
||||||
el.addEventListener('mouseleave', mouseLeave);
|
el.addEventListener('mouseleave', mouseLeave);
|
||||||
this._cleanups.push(() => {
|
this._cleanups.push(() => {
|
||||||
@@ -2944,6 +3128,15 @@ class Toast {
|
|||||||
el.removeEventListener('mouseleave', mouseLeave);
|
el.removeEventListener('mouseleave', mouseLeave);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
||||||
|
const animStart = () => Toast.trigger('animationStart', this);
|
||||||
|
const animEnd = () => Toast.trigger('animationEnd', this);
|
||||||
|
el.addEventListener('animationstart', animStart);
|
||||||
|
el.addEventListener('animationend', animEnd);
|
||||||
|
this._cleanups.push(() => {
|
||||||
|
el.removeEventListener('animationstart', animStart);
|
||||||
|
el.removeEventListener('animationend', animEnd);
|
||||||
|
});
|
||||||
if (this.config.draggable) {
|
if (this.config.draggable) {
|
||||||
this._bindDrag(el);
|
this._bindDrag(el);
|
||||||
}
|
}
|
||||||
@@ -2959,6 +3152,7 @@ class Toast {
|
|||||||
el.setPointerCapture(e.pointerId);
|
el.setPointerCapture(e.pointerId);
|
||||||
el.style.transition = 'none';
|
el.style.transition = 'none';
|
||||||
this._pause();
|
this._pause();
|
||||||
|
Toast.trigger('dragStart', this);
|
||||||
};
|
};
|
||||||
const move = (e) => {
|
const move = (e) => {
|
||||||
if (!dragging)
|
if (!dragging)
|
||||||
@@ -2974,7 +3168,8 @@ class Toast {
|
|||||||
dragging = false;
|
dragging = false;
|
||||||
el.releasePointerCapture(e.pointerId);
|
el.releasePointerCapture(e.pointerId);
|
||||||
el.style.transition = '';
|
el.style.transition = '';
|
||||||
if (Math.abs(dx) > 120) {
|
const threshold = this.config.dragThreshold ?? 120;
|
||||||
|
if (Math.abs(dx) > threshold) {
|
||||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
setTimeout(() => this.close(true), 250);
|
setTimeout(() => this.close(true), 250);
|
||||||
@@ -2985,6 +3180,7 @@ class Toast {
|
|||||||
this._resume();
|
this._resume();
|
||||||
}
|
}
|
||||||
dx = dy = 0;
|
dx = dy = 0;
|
||||||
|
Toast.trigger('dragEnd', this);
|
||||||
};
|
};
|
||||||
el.addEventListener('pointerdown', down);
|
el.addEventListener('pointerdown', down);
|
||||||
el.addEventListener('pointermove', move);
|
el.addEventListener('pointermove', move);
|
||||||
@@ -3003,6 +3199,7 @@ class Toast {
|
|||||||
if (!resuming) {
|
if (!resuming) {
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
this.remaining = this.config.duration || 0;
|
this.remaining = this.config.duration || 0;
|
||||||
|
Toast.trigger('progressStart', this);
|
||||||
}
|
}
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (this.paused || this.closing)
|
if (this.paused || this.closing)
|
||||||
@@ -3017,6 +3214,7 @@ class Toast {
|
|||||||
this.barEl.style.transform = t;
|
this.barEl.style.transform = t;
|
||||||
}
|
}
|
||||||
if (this.remaining <= 0) {
|
if (this.remaining <= 0) {
|
||||||
|
Toast.trigger('progressEnd', this);
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3050,7 +3248,9 @@ class Toast {
|
|||||||
this._startTimer(true);
|
this._startTimer(true);
|
||||||
}
|
}
|
||||||
update(partial) {
|
update(partial) {
|
||||||
Toast.trigger('beforeUpdate', this);
|
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
||||||
|
if (!Toast.trigger('beforeUpdate', this))
|
||||||
|
return this;
|
||||||
const typeChanged = partial.type && partial.type !== this.type;
|
const typeChanged = partial.type && partial.type !== this.type;
|
||||||
if (partial.type)
|
if (partial.type)
|
||||||
this.type = partial.type;
|
this.type = partial.type;
|
||||||
@@ -3087,6 +3287,18 @@ class Toast {
|
|||||||
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||||
}
|
}
|
||||||
|
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||||
|
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||||
|
this.config.duration = partial.duration;
|
||||||
|
if (this.rafId !== null)
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
this.remaining = Math.max(0, partial.duration);
|
||||||
|
if (partial.duration > 0 && !this.paused) {
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||||
if (this.rafId !== null)
|
if (this.rafId !== null)
|
||||||
@@ -3147,14 +3359,17 @@ class Toast {
|
|||||||
this.el.parentNode.removeChild(this.el);
|
this.el.parentNode.removeChild(this.el);
|
||||||
}
|
}
|
||||||
this.el = null;
|
this.el = null;
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast)
|
if (Toast._removeToast)
|
||||||
Toast._removeToast(this.id);
|
Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
close(immediate = false) {
|
close(immediate = false) {
|
||||||
if (this.closing)
|
if (this.closing)
|
||||||
return;
|
return;
|
||||||
|
// beforeClose 钩子 — 返回 false 可阻止关闭
|
||||||
|
if (!Toast.trigger('beforeClose', this))
|
||||||
|
return;
|
||||||
this.closing = true;
|
this.closing = true;
|
||||||
Toast.trigger('beforeClose', this);
|
|
||||||
if (this.rafId !== null)
|
if (this.rafId !== null)
|
||||||
cancelAnimationFrame(this.rafId);
|
cancelAnimationFrame(this.rafId);
|
||||||
this._cleanups.forEach(fn => { try {
|
this._cleanups.forEach(fn => { try {
|
||||||
@@ -3210,12 +3425,15 @@ class Toast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toast.trigger('afterClose', this);
|
Toast.trigger('afterClose', this);
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast)
|
if (Toast._removeToast)
|
||||||
Toast._removeToast(this.id);
|
Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 静态钩子系统
|
// 静态钩子系统
|
||||||
Toast._hooks = new Map();
|
Toast._hooks = new Map();
|
||||||
|
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
||||||
|
Toast._registry = new Map();
|
||||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||||
Toast._onError = null;
|
Toast._onError = null;
|
||||||
Toast._removeToast = null;
|
Toast._removeToast = null;
|
||||||
@@ -3227,7 +3445,7 @@ const _containerCache = new Map();
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Templates — HTML 模板辅助函数
|
* MetonaToast Templates — HTML 模板辅助函数
|
||||||
* @module templates
|
* @module templates
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
@@ -3299,7 +3517,7 @@ const actionHTML = (actions) => {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Plugins — 插件系统
|
* MetonaToast Plugins — 插件系统
|
||||||
* @module plugins
|
* @module plugins
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 插件管理器
|
* 插件管理器
|
||||||
@@ -3454,6 +3672,12 @@ const presetPlugins = {
|
|||||||
}
|
}
|
||||||
catch (_e) { /* noop */ }
|
catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
|
// 卸载 use() 注册的配置保存钩子
|
||||||
|
const meta = this;
|
||||||
|
if (typeof meta._saveOff === 'function') {
|
||||||
|
meta._saveOff();
|
||||||
|
meta._saveOff = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -3476,6 +3700,40 @@ const presetPlugins = {
|
|||||||
setTimeout(() => { if (el.parentNode)
|
setTimeout(() => { if (el.parentNode)
|
||||||
el.parentNode.removeChild(el); }, 3000);
|
el.parentNode.removeChild(el); }, 3000);
|
||||||
},
|
},
|
||||||
|
uninstall() {
|
||||||
|
// 卸载 use() 注册的朗读钩子
|
||||||
|
const meta = this;
|
||||||
|
if (typeof meta._off === 'function') {
|
||||||
|
meta._off();
|
||||||
|
meta._off = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||||
|
*/
|
||||||
|
dedupe: {
|
||||||
|
name: 'dedupe',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||||
|
_off: null,
|
||||||
|
install() {
|
||||||
|
const off = Toast.on('beforeShow', (toast) => {
|
||||||
|
const existing = Array.from(Toast._registry.values()).find(t => !t.closing && t.type === toast.type && t.message === toast.message);
|
||||||
|
if (existing) {
|
||||||
|
existing.update({ title: toast.title || existing.title });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
this._off = off;
|
||||||
|
},
|
||||||
|
uninstall() {
|
||||||
|
const off = this._off;
|
||||||
|
if (off)
|
||||||
|
off();
|
||||||
|
this._off = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -3519,7 +3777,7 @@ const defaultPluginManager = new PluginManager();
|
|||||||
/**
|
/**
|
||||||
* MetonaToast API — meToast 核心 API 对象
|
* MetonaToast API — meToast 核心 API 对象
|
||||||
* @module api
|
* @module api
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 参数标准化工具
|
* 参数标准化工具
|
||||||
@@ -3544,15 +3802,13 @@ const normalizeArgs = (args, defaultType = 'default') => {
|
|||||||
message: '',
|
message: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** 版本号常量 */
|
|
||||||
const VERSION$1 = '0.2.1';
|
|
||||||
/**
|
/**
|
||||||
* meToast API 对象
|
* meToast API 对象
|
||||||
*/
|
*/
|
||||||
const meToast = {
|
const meToast = {
|
||||||
_toasts: new Map(),
|
_toasts: new Map(),
|
||||||
_config: { ...DEFAULTS },
|
_config: { ...DEFAULTS },
|
||||||
version: '0.2.1',
|
version: VERSION,
|
||||||
configure(opts) {
|
configure(opts) {
|
||||||
if (!opts || typeof opts !== 'object')
|
if (!opts || typeof opts !== 'object')
|
||||||
return this;
|
return this;
|
||||||
@@ -3563,6 +3819,7 @@ const meToast = {
|
|||||||
if (opts.locale) {
|
if (opts.locale) {
|
||||||
setCurrentLocale(opts.locale);
|
setCurrentLocale(opts.locale);
|
||||||
}
|
}
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
_emit(opts) {
|
_emit(opts) {
|
||||||
@@ -3571,7 +3828,10 @@ const meToast = {
|
|||||||
merged.message = merged.content;
|
merged.message = merged.content;
|
||||||
const t = new Toast(merged);
|
const t = new Toast(merged);
|
||||||
t.create();
|
t.create();
|
||||||
|
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||||
|
if (t.el !== null) {
|
||||||
this._toasts.set(t.id, t);
|
this._toasts.set(t.id, t);
|
||||||
|
}
|
||||||
return t;
|
return t;
|
||||||
},
|
},
|
||||||
_remove(id) {
|
_remove(id) {
|
||||||
@@ -3637,14 +3897,21 @@ const meToast = {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||||
|
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||||
|
*/
|
||||||
_resolve(loadingToast, type, message, opts) {
|
_resolve(loadingToast, type, message, opts) {
|
||||||
const id = loadingToast.id;
|
const old = this._toasts.get(loadingToast.id);
|
||||||
const old = this._toasts.get(id);
|
|
||||||
if (!old)
|
if (!old)
|
||||||
return null;
|
return null;
|
||||||
const position = old.config.position;
|
old.update({
|
||||||
old.close();
|
...opts,
|
||||||
return this._emit({ ...opts, type, message, position });
|
type,
|
||||||
|
message,
|
||||||
|
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||||
|
});
|
||||||
|
return old;
|
||||||
},
|
},
|
||||||
confirm(message, opts = {}) {
|
confirm(message, opts = {}) {
|
||||||
if (typeof message !== 'string') {
|
if (typeof message !== 'string') {
|
||||||
@@ -3846,12 +4113,14 @@ const meToast = {
|
|||||||
actions.forEach((a, i) => {
|
actions.forEach((a, i) => {
|
||||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||||
if (btn && typeof a.onClick === 'function') {
|
if (btn && typeof a.onClick === 'function') {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', (e) => {
|
||||||
|
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||||
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
a.onClick(toast);
|
a.onClick(toast);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (err) {
|
||||||
console.error('Action onClick error:', e);
|
console.error('Action onClick error:', err);
|
||||||
}
|
}
|
||||||
if (a.close !== false)
|
if (a.close !== false)
|
||||||
toast.close();
|
toast.close();
|
||||||
@@ -4034,6 +4303,8 @@ const meToast = {
|
|||||||
},
|
},
|
||||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||||
init(options = {}) {
|
init(options = {}) {
|
||||||
|
Toast.trigger('beforeInit');
|
||||||
|
this._destroyed = false;
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
this.configure(options.config);
|
this.configure(options.config);
|
||||||
}
|
}
|
||||||
@@ -4048,11 +4319,12 @@ const meToast = {
|
|||||||
this.use(p);
|
this.use(p);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Toast.trigger('afterInit');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return {
|
return {
|
||||||
version: VERSION$1,
|
version: VERSION,
|
||||||
toasts: this._toasts.size,
|
toasts: this._toasts.size,
|
||||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||||
@@ -4066,6 +4338,7 @@ const meToast = {
|
|||||||
updateConfig(config) {
|
updateConfig(config) {
|
||||||
if (config && typeof config === 'object') {
|
if (config && typeof config === 'object') {
|
||||||
Object.assign(this._config, config);
|
Object.assign(this._config, config);
|
||||||
|
Toast.trigger('configChange');
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -4073,6 +4346,7 @@ const meToast = {
|
|||||||
const keys = Object.keys(this._config);
|
const keys = Object.keys(this._config);
|
||||||
keys.forEach(k => delete this._config[k]);
|
keys.forEach(k => delete this._config[k]);
|
||||||
Object.assign(this._config, DEFAULTS);
|
Object.assign(this._config, DEFAULTS);
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
use(plugin, options = {}) {
|
use(plugin, options = {}) {
|
||||||
@@ -4083,27 +4357,39 @@ const meToast = {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
this.plugins.register(plugin, { ...preset, ...options });
|
this.plugins.register(plugin, { ...preset, ...options });
|
||||||
// 连接插件钩子
|
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||||
|
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||||
|
const stored = this.plugins.get(plugin);
|
||||||
if (plugin === 'accessibility') {
|
if (plugin === 'accessibility') {
|
||||||
Toast.on('afterShow', (toast) => {
|
if (stored && typeof stored._off === 'function') {
|
||||||
|
stored._off();
|
||||||
|
}
|
||||||
const acc = preset;
|
const acc = preset;
|
||||||
|
if (stored) {
|
||||||
|
stored._off = Toast.on('afterShow', (toast) => {
|
||||||
if (typeof acc.announce === 'function')
|
if (typeof acc.announce === 'function')
|
||||||
acc.announce(toast);
|
acc.announce(toast);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (plugin === 'persistence') {
|
if (plugin === 'persistence') {
|
||||||
|
if (stored && typeof stored._saveOff === 'function') {
|
||||||
|
stored._saveOff();
|
||||||
|
}
|
||||||
const saved = typeof preset.install === 'function'
|
const saved = typeof preset.install === 'function'
|
||||||
? preset.install(this.plugins)
|
? preset.install(this.plugins)
|
||||||
: null;
|
: null;
|
||||||
if (saved)
|
if (saved)
|
||||||
this.configure(saved);
|
this.configure(saved);
|
||||||
Toast.on('afterClose', () => {
|
|
||||||
const p = preset;
|
const p = preset;
|
||||||
|
if (stored) {
|
||||||
|
stored._saveOff = Toast.on('afterClose', () => {
|
||||||
if (typeof p.save === 'function')
|
if (typeof p.save === 'function')
|
||||||
p.save(this.getConfig());
|
p.save(this.getConfig());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (plugin && typeof plugin === 'object') {
|
else if (plugin && typeof plugin === 'object') {
|
||||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
||||||
}
|
}
|
||||||
@@ -4113,6 +4399,7 @@ const meToast = {
|
|||||||
if (this._destroyed)
|
if (this._destroyed)
|
||||||
return;
|
return;
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
|
Toast.trigger('beforeDestroy');
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
// 清理子模块
|
// 清理子模块
|
||||||
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
||||||
@@ -4129,20 +4416,26 @@ const meToast = {
|
|||||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||||
this.i18n.clearLocaleListeners();
|
this.i18n.clearLocaleListeners();
|
||||||
}
|
}
|
||||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
if (this.animations && typeof this.animations.destroy === 'function') {
|
||||||
this.animations.cancelAll();
|
this.animations.destroy();
|
||||||
}
|
}
|
||||||
// 清理 DOM
|
// 清理 DOM 与静态资源
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const containers = document.querySelectorAll('.met-container');
|
const containers = document.querySelectorAll('.met-container');
|
||||||
containers.forEach((c) => { if (c.parentNode)
|
containers.forEach((c) => { if (c.parentNode)
|
||||||
c.parentNode.removeChild(c); });
|
c.parentNode.removeChild(c); });
|
||||||
const style = document.getElementById('metona-toast-styles');
|
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
||||||
if (style && style.parentNode)
|
const el = document.getElementById(id);
|
||||||
style.parentNode.removeChild(style);
|
if (el && el.parentNode)
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._toasts.clear();
|
this._toasts.clear();
|
||||||
_containerCache.clear();
|
_containerCache.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
// afterDestroy 必须在清理 hooks 之前触发
|
||||||
|
Toast.trigger('afterDestroy');
|
||||||
|
Toast._hooks.clear();
|
||||||
},
|
},
|
||||||
// 子模块引用(由 index.ts 注入实际实现)
|
// 子模块引用(由 index.ts 注入实际实现)
|
||||||
animations: null,
|
animations: null,
|
||||||
@@ -4167,77 +4460,13 @@ Toast.setCallbacks({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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通知库
|
* MetonaToast — 轻量级Toast通知库
|
||||||
* @module metona-toast
|
* @module metona-toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @author thzxx
|
* @author thzxx
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
// 版本信息
|
|
||||||
const VERSION = '0.2.1';
|
|
||||||
/**
|
/**
|
||||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||||
*/
|
*/
|
||||||
@@ -4257,6 +4486,13 @@ if (typeof themeUtils.initTheme === 'function') {
|
|||||||
if (typeof i18nUtils.initI18n === 'function') {
|
if (typeof i18nUtils.initI18n === 'function') {
|
||||||
i18nUtils.initI18n();
|
i18nUtils.initI18n();
|
||||||
}
|
}
|
||||||
|
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
||||||
|
if (typeof themeUtils.addThemeListener === 'function') {
|
||||||
|
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
||||||
|
}
|
||||||
|
if (typeof i18nUtils.addLocaleListener === 'function') {
|
||||||
|
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
||||||
|
}
|
||||||
// 浏览器环境全局注册
|
// 浏览器环境全局注册
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.MeToast = enhancedMeToast;
|
window.MeToast = enhancedMeToast;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+19
-6
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast — 核心类型定义
|
* MetonaToast — 核心类型定义
|
||||||
* @module types
|
* @module types
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/** Toast 通知类型(107种内置图标类型) */
|
/** Toast 通知类型(107种内置图标类型) */
|
||||||
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | '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';
|
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | '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';
|
||||||
@@ -26,6 +26,7 @@ interface ToastConfig {
|
|||||||
pauseOnHover?: boolean;
|
pauseOnHover?: boolean;
|
||||||
closeOnClick?: boolean;
|
closeOnClick?: boolean;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
|
dragThreshold?: number;
|
||||||
showProgress?: boolean;
|
showProgress?: boolean;
|
||||||
progressDirection?: ToastProgressDirection;
|
progressDirection?: ToastProgressDirection;
|
||||||
icon?: boolean;
|
icon?: boolean;
|
||||||
@@ -193,8 +194,8 @@ interface StatusInfo {
|
|||||||
animations: number;
|
animations: number;
|
||||||
}
|
}
|
||||||
interface AnimationConfig {
|
interface AnimationConfig {
|
||||||
enter: Record<string, string>;
|
enter: Record<string, string | number>;
|
||||||
leave: Record<string, string>;
|
leave: Record<string, string | number>;
|
||||||
duration: number;
|
duration: number;
|
||||||
easing: string;
|
easing: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -417,7 +418,7 @@ declare global {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Toast — Toast 类
|
* MetonaToast Toast — Toast 类
|
||||||
* @module toast
|
* @module toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -425,6 +426,7 @@ declare global {
|
|||||||
*/
|
*/
|
||||||
declare class Toast implements ToastInstance {
|
declare class Toast implements ToastInstance {
|
||||||
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>>;
|
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>>;
|
||||||
|
static _registry: Map<string, Toast>;
|
||||||
private static _onError;
|
private static _onError;
|
||||||
private static _removeToast;
|
private static _removeToast;
|
||||||
/**
|
/**
|
||||||
@@ -438,8 +440,9 @@ declare class Toast implements ToastInstance {
|
|||||||
static off(name: string, fn: (toast: Toast) => boolean | void): void;
|
static off(name: string, fn: (toast: Toast) => boolean | void): void;
|
||||||
/**
|
/**
|
||||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||||
|
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||||
*/
|
*/
|
||||||
static trigger(name: string, toast: Toast): boolean;
|
static trigger(name: string, toast?: Toast | null): boolean;
|
||||||
id: string;
|
id: string;
|
||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -486,7 +489,17 @@ declare class Toast implements ToastInstance {
|
|||||||
_destroy(): void;
|
_destroy(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare const VERSION = "0.2.1";
|
/**
|
||||||
|
* MetonaToast Constants — 常量定义
|
||||||
|
* @module constants
|
||||||
|
* @version 0.5.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号 — 唯一来源,发布时只需修改此处
|
||||||
|
*/
|
||||||
|
declare const VERSION = "0.5.0";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||||
*/
|
*/
|
||||||
|
|||||||
Vendored
+356
-120
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Utils — 工具函数
|
* MetonaToast Utils — 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 生成唯一ID
|
* 生成唯一ID
|
||||||
@@ -38,7 +38,7 @@ const prefersDark = () => {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Icons — 图标SVG定义
|
* MetonaToast Icons — 图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 107 个内置 SVG 图标
|
* @description 107 个内置 SVG 图标
|
||||||
*/
|
*/
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -535,7 +535,7 @@ const ICONS = {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Locales — 国际化翻译数据
|
* MetonaToast Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 内置 zh-CN / en-US 完整翻译
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
const LOCALES = {
|
const LOCALES = {
|
||||||
@@ -1022,8 +1022,12 @@ const LOCALES = {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Constants — 常量定义
|
* MetonaToast Constants — 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 版本号 — 唯一来源,发布时只需修改此处
|
||||||
|
*/
|
||||||
|
const VERSION = '0.5.0';
|
||||||
/**
|
/**
|
||||||
* 默认配置
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
@@ -1036,6 +1040,7 @@ const DEFAULTS = Object.freeze({
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
dragThreshold: 120,
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
progressDirection: 'horizontal',
|
progressDirection: 'horizontal',
|
||||||
icon: true,
|
icon: true,
|
||||||
@@ -1281,11 +1286,15 @@ const THEMES = {
|
|||||||
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 动画类型
|
||||||
|
*/
|
||||||
|
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetonaToast Styles — 样式管理
|
* MetonaToast Styles — 样式管理
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 样式缓存
|
// 样式缓存
|
||||||
let styleElement = null;
|
let styleElement = null;
|
||||||
@@ -1643,6 +1652,41 @@ const generateCSS = () => {
|
|||||||
transition: transform 0.1s linear;
|
transition: transform 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||||
|
[dir="rtl"] .met-bar {
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-side {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-progress-v {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-success {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-error {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #ef4444;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-warning {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-info {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-loading {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
.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 { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||||
@@ -1739,7 +1783,7 @@ const injectStyles = () => {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Themes — 主题管理
|
* MetonaToast Themes — 主题管理
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 当前主题状态
|
// 当前主题状态
|
||||||
let currentTheme = 'auto';
|
let currentTheme = 'auto';
|
||||||
@@ -2083,7 +2127,7 @@ const themeUtils = {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast i18n — 国际化管理
|
* MetonaToast i18n — 国际化管理
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 当前语言状态
|
// 当前语言状态
|
||||||
let currentLocale = 'zh-CN';
|
let currentLocale = 'zh-CN';
|
||||||
@@ -2451,7 +2495,7 @@ const formatNumber = (number, options = {}) => {
|
|||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2466,7 +2510,7 @@ const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
|||||||
...options,
|
...options,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return amount.toString();
|
return amount.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2482,7 +2526,7 @@ const formatPercent = (value, options = {}) => {
|
|||||||
...options,
|
...options,
|
||||||
}).format(value / 100);
|
}).format(value / 100);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return `${value}%`;
|
return `${value}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2494,7 +2538,7 @@ const formatDate = (date, options = {}) => {
|
|||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2538,7 +2582,7 @@ const formatRelativeTime = (date, options = {}) => {
|
|||||||
}
|
}
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2565,7 +2609,7 @@ const formatPlural = (count, options = {}) => {
|
|||||||
try {
|
try {
|
||||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return count === 1 ? 'one' : 'other';
|
return count === 1 ? 'one' : 'other';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2639,10 +2683,126 @@ const i18nUtils = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetonaToast Animations — 动画管理
|
||||||
|
* @module animations
|
||||||
|
* @version 0.2.1
|
||||||
|
*/
|
||||||
|
// 动画缓存
|
||||||
|
const animationMap = new Map();
|
||||||
|
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
||||||
|
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
|
||||||
|
// 动态注入的自定义动画样式(惰性创建)
|
||||||
|
let animStyleElement = null;
|
||||||
|
const injectedAnimations = new Set();
|
||||||
|
/**
|
||||||
|
* 将 CSS 属性对象转换为内联 CSS 字符串
|
||||||
|
*/
|
||||||
|
const toCss = (props) => Object.entries(props)
|
||||||
|
.map(([key, value]) => `${key}:${value}`)
|
||||||
|
.join(';');
|
||||||
|
/**
|
||||||
|
* 为自定义动画生成入场 keyframes + 类规则
|
||||||
|
*/
|
||||||
|
const buildAnimationCSS = (name, config) => {
|
||||||
|
const enter = toCss(config.enter);
|
||||||
|
return `
|
||||||
|
@keyframes met-${name}-in {
|
||||||
|
from { ${enter}; }
|
||||||
|
}
|
||||||
|
.met-anim-${name}.met-toast { opacity: 0; }
|
||||||
|
.met-anim-${name}.met-toast.met-show {
|
||||||
|
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
||||||
|
*/
|
||||||
|
const ensureAnimationCSS = (name) => {
|
||||||
|
if (typeof document === 'undefined')
|
||||||
|
return;
|
||||||
|
if (BUILTIN_ANIMATIONS.has(name))
|
||||||
|
return;
|
||||||
|
const config = animationMap.get(name);
|
||||||
|
if (!config || injectedAnimations.has(name))
|
||||||
|
return;
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.getElementById('metona-toast-anim-styles');
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.createElement('style');
|
||||||
|
animStyleElement.id = 'metona-toast-anim-styles';
|
||||||
|
document.head.appendChild(animStyleElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animStyleElement.textContent += buildAnimationCSS(name, config);
|
||||||
|
injectedAnimations.add(name);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 检查动画是否存在(内置或自定义)
|
||||||
|
*/
|
||||||
|
const hasAnimation = (name) => animationMap.has(name);
|
||||||
|
// 注册默认动画
|
||||||
|
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();
|
||||||
|
injectedAnimations.clear();
|
||||||
|
animStyleElement = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetonaToast Toast — Toast 类
|
* MetonaToast Toast — Toast 类
|
||||||
* @module toast
|
* @module toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Toast 类 — 核心通知组件
|
* Toast 类 — 核心通知组件
|
||||||
@@ -2661,17 +2821,25 @@ class Toast {
|
|||||||
if (!this._hooks.has(name))
|
if (!this._hooks.has(name))
|
||||||
this._hooks.set(name, []);
|
this._hooks.set(name, []);
|
||||||
this._hooks.get(name).push(fn);
|
this._hooks.get(name).push(fn);
|
||||||
return () => this.off(name, fn);
|
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||||
|
return () => Toast.off(name, fn);
|
||||||
}
|
}
|
||||||
static off(name, fn) {
|
static off(name, fn) {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (list)
|
if (list) {
|
||||||
this._hooks.set(name, list.filter(f => f !== fn));
|
const next = list.filter(f => f !== fn);
|
||||||
|
// 空数组直接删除 key,避免残留
|
||||||
|
if (next.length === 0)
|
||||||
|
this._hooks.delete(name);
|
||||||
|
else
|
||||||
|
this._hooks.set(name, next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||||
|
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||||
*/
|
*/
|
||||||
static trigger(name, toast) {
|
static trigger(name, toast = null) {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (!list || list.length === 0)
|
if (!list || list.length === 0)
|
||||||
return true;
|
return true;
|
||||||
@@ -2686,7 +2854,7 @@ class Toast {
|
|||||||
console.error('Hook error:', name, e);
|
console.error('Hook error:', name, e);
|
||||||
if (Toast._onError) {
|
if (Toast._onError) {
|
||||||
try {
|
try {
|
||||||
Toast._onError({ hook: name, error: e, toast });
|
Toast._onError({ hook: name, error: e, toast: toast });
|
||||||
}
|
}
|
||||||
catch (_e) { /* noop */ }
|
catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
@@ -2741,6 +2909,7 @@ class Toast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
ensureAnimationCSS(this.config.animation || 'slide');
|
||||||
const container = this._getContainer();
|
const container = this._getContainer();
|
||||||
const { theme, c, t } = this._palette();
|
const { theme, c, t } = this._palette();
|
||||||
this._limitToasts(container);
|
this._limitToasts(container);
|
||||||
@@ -2760,6 +2929,7 @@ class Toast {
|
|||||||
else {
|
else {
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
}
|
}
|
||||||
|
Toast._registry.set(this.id, this);
|
||||||
this._bindEvents(el);
|
this._bindEvents(el);
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
// 进入动画
|
// 进入动画
|
||||||
@@ -2800,6 +2970,9 @@ class Toast {
|
|||||||
el.className = `met-container ${position}`;
|
el.className = `met-container ${position}`;
|
||||||
el.setAttribute('aria-label', 'Notifications');
|
el.setAttribute('aria-label', 'Notifications');
|
||||||
el.setAttribute('role', 'region');
|
el.setAttribute('role', 'region');
|
||||||
|
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||||
|
if (isRTL)
|
||||||
|
el.setAttribute('dir', 'rtl');
|
||||||
const posStyles = {
|
const posStyles = {
|
||||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||||
'top-center': 'top:0;left:0;right:0;align-items:center',
|
'top-center': 'top:0;left:0;right:0;align-items:center',
|
||||||
@@ -2827,18 +3000,22 @@ class Toast {
|
|||||||
_limitToasts(container) {
|
_limitToasts(container) {
|
||||||
const max = this.config.max || 6;
|
const max = this.config.max || 6;
|
||||||
const list = Array.from(container.querySelectorAll('.met-toast'));
|
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||||
if (list.length >= max) {
|
while (list.length >= max) {
|
||||||
const first = list[0];
|
const el = list.shift();
|
||||||
const id = first?.dataset.id;
|
const id = el?.dataset.id;
|
||||||
if (id && Toast._removeToast) {
|
if (!id)
|
||||||
|
continue;
|
||||||
|
const existing = Toast._registry.get(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.close(true);
|
||||||
|
}
|
||||||
|
else if (Toast._removeToast) {
|
||||||
Toast._removeToast(id);
|
Toast._removeToast(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_buildClassName(theme) {
|
_buildClassName(theme) {
|
||||||
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
|
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
|
||||||
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
|
||||||
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
|
|
||||||
return [
|
return [
|
||||||
'met-toast',
|
'met-toast',
|
||||||
`met-${this.type}`,
|
`met-${this.type}`,
|
||||||
@@ -2915,6 +3092,7 @@ class Toast {
|
|||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Toast.trigger('click', this);
|
||||||
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||||
if (typeof this.config.onClick === 'function') {
|
if (typeof this.config.onClick === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -2931,8 +3109,14 @@ class Toast {
|
|||||||
el.addEventListener('click', eventHandler);
|
el.addEventListener('click', eventHandler);
|
||||||
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||||
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||||
const mouseEnter = () => this._pause();
|
const mouseEnter = () => {
|
||||||
const mouseLeave = () => this._resume();
|
Toast.trigger('hover', this);
|
||||||
|
this._pause();
|
||||||
|
};
|
||||||
|
const mouseLeave = () => {
|
||||||
|
Toast.trigger('hover', this);
|
||||||
|
this._resume();
|
||||||
|
};
|
||||||
el.addEventListener('mouseenter', mouseEnter);
|
el.addEventListener('mouseenter', mouseEnter);
|
||||||
el.addEventListener('mouseleave', mouseLeave);
|
el.addEventListener('mouseleave', mouseLeave);
|
||||||
this._cleanups.push(() => {
|
this._cleanups.push(() => {
|
||||||
@@ -2940,6 +3124,15 @@ class Toast {
|
|||||||
el.removeEventListener('mouseleave', mouseLeave);
|
el.removeEventListener('mouseleave', mouseLeave);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
||||||
|
const animStart = () => Toast.trigger('animationStart', this);
|
||||||
|
const animEnd = () => Toast.trigger('animationEnd', this);
|
||||||
|
el.addEventListener('animationstart', animStart);
|
||||||
|
el.addEventListener('animationend', animEnd);
|
||||||
|
this._cleanups.push(() => {
|
||||||
|
el.removeEventListener('animationstart', animStart);
|
||||||
|
el.removeEventListener('animationend', animEnd);
|
||||||
|
});
|
||||||
if (this.config.draggable) {
|
if (this.config.draggable) {
|
||||||
this._bindDrag(el);
|
this._bindDrag(el);
|
||||||
}
|
}
|
||||||
@@ -2955,6 +3148,7 @@ class Toast {
|
|||||||
el.setPointerCapture(e.pointerId);
|
el.setPointerCapture(e.pointerId);
|
||||||
el.style.transition = 'none';
|
el.style.transition = 'none';
|
||||||
this._pause();
|
this._pause();
|
||||||
|
Toast.trigger('dragStart', this);
|
||||||
};
|
};
|
||||||
const move = (e) => {
|
const move = (e) => {
|
||||||
if (!dragging)
|
if (!dragging)
|
||||||
@@ -2970,7 +3164,8 @@ class Toast {
|
|||||||
dragging = false;
|
dragging = false;
|
||||||
el.releasePointerCapture(e.pointerId);
|
el.releasePointerCapture(e.pointerId);
|
||||||
el.style.transition = '';
|
el.style.transition = '';
|
||||||
if (Math.abs(dx) > 120) {
|
const threshold = this.config.dragThreshold ?? 120;
|
||||||
|
if (Math.abs(dx) > threshold) {
|
||||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
setTimeout(() => this.close(true), 250);
|
setTimeout(() => this.close(true), 250);
|
||||||
@@ -2981,6 +3176,7 @@ class Toast {
|
|||||||
this._resume();
|
this._resume();
|
||||||
}
|
}
|
||||||
dx = dy = 0;
|
dx = dy = 0;
|
||||||
|
Toast.trigger('dragEnd', this);
|
||||||
};
|
};
|
||||||
el.addEventListener('pointerdown', down);
|
el.addEventListener('pointerdown', down);
|
||||||
el.addEventListener('pointermove', move);
|
el.addEventListener('pointermove', move);
|
||||||
@@ -2999,6 +3195,7 @@ class Toast {
|
|||||||
if (!resuming) {
|
if (!resuming) {
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
this.remaining = this.config.duration || 0;
|
this.remaining = this.config.duration || 0;
|
||||||
|
Toast.trigger('progressStart', this);
|
||||||
}
|
}
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (this.paused || this.closing)
|
if (this.paused || this.closing)
|
||||||
@@ -3013,6 +3210,7 @@ class Toast {
|
|||||||
this.barEl.style.transform = t;
|
this.barEl.style.transform = t;
|
||||||
}
|
}
|
||||||
if (this.remaining <= 0) {
|
if (this.remaining <= 0) {
|
||||||
|
Toast.trigger('progressEnd', this);
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3046,7 +3244,9 @@ class Toast {
|
|||||||
this._startTimer(true);
|
this._startTimer(true);
|
||||||
}
|
}
|
||||||
update(partial) {
|
update(partial) {
|
||||||
Toast.trigger('beforeUpdate', this);
|
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
||||||
|
if (!Toast.trigger('beforeUpdate', this))
|
||||||
|
return this;
|
||||||
const typeChanged = partial.type && partial.type !== this.type;
|
const typeChanged = partial.type && partial.type !== this.type;
|
||||||
if (partial.type)
|
if (partial.type)
|
||||||
this.type = partial.type;
|
this.type = partial.type;
|
||||||
@@ -3083,6 +3283,18 @@ class Toast {
|
|||||||
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||||
}
|
}
|
||||||
|
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||||
|
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||||
|
this.config.duration = partial.duration;
|
||||||
|
if (this.rafId !== null)
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
this.remaining = Math.max(0, partial.duration);
|
||||||
|
if (partial.duration > 0 && !this.paused) {
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||||
if (this.rafId !== null)
|
if (this.rafId !== null)
|
||||||
@@ -3143,14 +3355,17 @@ class Toast {
|
|||||||
this.el.parentNode.removeChild(this.el);
|
this.el.parentNode.removeChild(this.el);
|
||||||
}
|
}
|
||||||
this.el = null;
|
this.el = null;
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast)
|
if (Toast._removeToast)
|
||||||
Toast._removeToast(this.id);
|
Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
close(immediate = false) {
|
close(immediate = false) {
|
||||||
if (this.closing)
|
if (this.closing)
|
||||||
return;
|
return;
|
||||||
|
// beforeClose 钩子 — 返回 false 可阻止关闭
|
||||||
|
if (!Toast.trigger('beforeClose', this))
|
||||||
|
return;
|
||||||
this.closing = true;
|
this.closing = true;
|
||||||
Toast.trigger('beforeClose', this);
|
|
||||||
if (this.rafId !== null)
|
if (this.rafId !== null)
|
||||||
cancelAnimationFrame(this.rafId);
|
cancelAnimationFrame(this.rafId);
|
||||||
this._cleanups.forEach(fn => { try {
|
this._cleanups.forEach(fn => { try {
|
||||||
@@ -3206,12 +3421,15 @@ class Toast {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toast.trigger('afterClose', this);
|
Toast.trigger('afterClose', this);
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast)
|
if (Toast._removeToast)
|
||||||
Toast._removeToast(this.id);
|
Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 静态钩子系统
|
// 静态钩子系统
|
||||||
Toast._hooks = new Map();
|
Toast._hooks = new Map();
|
||||||
|
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
||||||
|
Toast._registry = new Map();
|
||||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||||
Toast._onError = null;
|
Toast._onError = null;
|
||||||
Toast._removeToast = null;
|
Toast._removeToast = null;
|
||||||
@@ -3223,7 +3441,7 @@ const _containerCache = new Map();
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Templates — HTML 模板辅助函数
|
* MetonaToast Templates — HTML 模板辅助函数
|
||||||
* @module templates
|
* @module templates
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
@@ -3295,7 +3513,7 @@ const actionHTML = (actions) => {
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Plugins — 插件系统
|
* MetonaToast Plugins — 插件系统
|
||||||
* @module plugins
|
* @module plugins
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 插件管理器
|
* 插件管理器
|
||||||
@@ -3450,6 +3668,12 @@ const presetPlugins = {
|
|||||||
}
|
}
|
||||||
catch (_e) { /* noop */ }
|
catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
|
// 卸载 use() 注册的配置保存钩子
|
||||||
|
const meta = this;
|
||||||
|
if (typeof meta._saveOff === 'function') {
|
||||||
|
meta._saveOff();
|
||||||
|
meta._saveOff = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -3472,6 +3696,40 @@ const presetPlugins = {
|
|||||||
setTimeout(() => { if (el.parentNode)
|
setTimeout(() => { if (el.parentNode)
|
||||||
el.parentNode.removeChild(el); }, 3000);
|
el.parentNode.removeChild(el); }, 3000);
|
||||||
},
|
},
|
||||||
|
uninstall() {
|
||||||
|
// 卸载 use() 注册的朗读钩子
|
||||||
|
const meta = this;
|
||||||
|
if (typeof meta._off === 'function') {
|
||||||
|
meta._off();
|
||||||
|
meta._off = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||||
|
*/
|
||||||
|
dedupe: {
|
||||||
|
name: 'dedupe',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||||
|
_off: null,
|
||||||
|
install() {
|
||||||
|
const off = Toast.on('beforeShow', (toast) => {
|
||||||
|
const existing = Array.from(Toast._registry.values()).find(t => !t.closing && t.type === toast.type && t.message === toast.message);
|
||||||
|
if (existing) {
|
||||||
|
existing.update({ title: toast.title || existing.title });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
this._off = off;
|
||||||
|
},
|
||||||
|
uninstall() {
|
||||||
|
const off = this._off;
|
||||||
|
if (off)
|
||||||
|
off();
|
||||||
|
this._off = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -3515,7 +3773,7 @@ const defaultPluginManager = new PluginManager();
|
|||||||
/**
|
/**
|
||||||
* MetonaToast API — meToast 核心 API 对象
|
* MetonaToast API — meToast 核心 API 对象
|
||||||
* @module api
|
* @module api
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 参数标准化工具
|
* 参数标准化工具
|
||||||
@@ -3540,15 +3798,13 @@ const normalizeArgs = (args, defaultType = 'default') => {
|
|||||||
message: '',
|
message: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** 版本号常量 */
|
|
||||||
const VERSION$1 = '0.2.1';
|
|
||||||
/**
|
/**
|
||||||
* meToast API 对象
|
* meToast API 对象
|
||||||
*/
|
*/
|
||||||
const meToast = {
|
const meToast = {
|
||||||
_toasts: new Map(),
|
_toasts: new Map(),
|
||||||
_config: { ...DEFAULTS },
|
_config: { ...DEFAULTS },
|
||||||
version: '0.2.1',
|
version: VERSION,
|
||||||
configure(opts) {
|
configure(opts) {
|
||||||
if (!opts || typeof opts !== 'object')
|
if (!opts || typeof opts !== 'object')
|
||||||
return this;
|
return this;
|
||||||
@@ -3559,6 +3815,7 @@ const meToast = {
|
|||||||
if (opts.locale) {
|
if (opts.locale) {
|
||||||
setCurrentLocale(opts.locale);
|
setCurrentLocale(opts.locale);
|
||||||
}
|
}
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
_emit(opts) {
|
_emit(opts) {
|
||||||
@@ -3567,7 +3824,10 @@ const meToast = {
|
|||||||
merged.message = merged.content;
|
merged.message = merged.content;
|
||||||
const t = new Toast(merged);
|
const t = new Toast(merged);
|
||||||
t.create();
|
t.create();
|
||||||
|
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||||
|
if (t.el !== null) {
|
||||||
this._toasts.set(t.id, t);
|
this._toasts.set(t.id, t);
|
||||||
|
}
|
||||||
return t;
|
return t;
|
||||||
},
|
},
|
||||||
_remove(id) {
|
_remove(id) {
|
||||||
@@ -3633,14 +3893,21 @@ const meToast = {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||||
|
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||||
|
*/
|
||||||
_resolve(loadingToast, type, message, opts) {
|
_resolve(loadingToast, type, message, opts) {
|
||||||
const id = loadingToast.id;
|
const old = this._toasts.get(loadingToast.id);
|
||||||
const old = this._toasts.get(id);
|
|
||||||
if (!old)
|
if (!old)
|
||||||
return null;
|
return null;
|
||||||
const position = old.config.position;
|
old.update({
|
||||||
old.close();
|
...opts,
|
||||||
return this._emit({ ...opts, type, message, position });
|
type,
|
||||||
|
message,
|
||||||
|
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||||
|
});
|
||||||
|
return old;
|
||||||
},
|
},
|
||||||
confirm(message, opts = {}) {
|
confirm(message, opts = {}) {
|
||||||
if (typeof message !== 'string') {
|
if (typeof message !== 'string') {
|
||||||
@@ -3842,12 +4109,14 @@ const meToast = {
|
|||||||
actions.forEach((a, i) => {
|
actions.forEach((a, i) => {
|
||||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||||
if (btn && typeof a.onClick === 'function') {
|
if (btn && typeof a.onClick === 'function') {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', (e) => {
|
||||||
|
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||||
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
a.onClick(toast);
|
a.onClick(toast);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (err) {
|
||||||
console.error('Action onClick error:', e);
|
console.error('Action onClick error:', err);
|
||||||
}
|
}
|
||||||
if (a.close !== false)
|
if (a.close !== false)
|
||||||
toast.close();
|
toast.close();
|
||||||
@@ -4030,6 +4299,8 @@ const meToast = {
|
|||||||
},
|
},
|
||||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||||
init(options = {}) {
|
init(options = {}) {
|
||||||
|
Toast.trigger('beforeInit');
|
||||||
|
this._destroyed = false;
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
this.configure(options.config);
|
this.configure(options.config);
|
||||||
}
|
}
|
||||||
@@ -4044,11 +4315,12 @@ const meToast = {
|
|||||||
this.use(p);
|
this.use(p);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Toast.trigger('afterInit');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return {
|
return {
|
||||||
version: VERSION$1,
|
version: VERSION,
|
||||||
toasts: this._toasts.size,
|
toasts: this._toasts.size,
|
||||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||||
@@ -4062,6 +4334,7 @@ const meToast = {
|
|||||||
updateConfig(config) {
|
updateConfig(config) {
|
||||||
if (config && typeof config === 'object') {
|
if (config && typeof config === 'object') {
|
||||||
Object.assign(this._config, config);
|
Object.assign(this._config, config);
|
||||||
|
Toast.trigger('configChange');
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -4069,6 +4342,7 @@ const meToast = {
|
|||||||
const keys = Object.keys(this._config);
|
const keys = Object.keys(this._config);
|
||||||
keys.forEach(k => delete this._config[k]);
|
keys.forEach(k => delete this._config[k]);
|
||||||
Object.assign(this._config, DEFAULTS);
|
Object.assign(this._config, DEFAULTS);
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
use(plugin, options = {}) {
|
use(plugin, options = {}) {
|
||||||
@@ -4079,27 +4353,39 @@ const meToast = {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
this.plugins.register(plugin, { ...preset, ...options });
|
this.plugins.register(plugin, { ...preset, ...options });
|
||||||
// 连接插件钩子
|
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||||
|
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||||
|
const stored = this.plugins.get(plugin);
|
||||||
if (plugin === 'accessibility') {
|
if (plugin === 'accessibility') {
|
||||||
Toast.on('afterShow', (toast) => {
|
if (stored && typeof stored._off === 'function') {
|
||||||
|
stored._off();
|
||||||
|
}
|
||||||
const acc = preset;
|
const acc = preset;
|
||||||
|
if (stored) {
|
||||||
|
stored._off = Toast.on('afterShow', (toast) => {
|
||||||
if (typeof acc.announce === 'function')
|
if (typeof acc.announce === 'function')
|
||||||
acc.announce(toast);
|
acc.announce(toast);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (plugin === 'persistence') {
|
if (plugin === 'persistence') {
|
||||||
|
if (stored && typeof stored._saveOff === 'function') {
|
||||||
|
stored._saveOff();
|
||||||
|
}
|
||||||
const saved = typeof preset.install === 'function'
|
const saved = typeof preset.install === 'function'
|
||||||
? preset.install(this.plugins)
|
? preset.install(this.plugins)
|
||||||
: null;
|
: null;
|
||||||
if (saved)
|
if (saved)
|
||||||
this.configure(saved);
|
this.configure(saved);
|
||||||
Toast.on('afterClose', () => {
|
|
||||||
const p = preset;
|
const p = preset;
|
||||||
|
if (stored) {
|
||||||
|
stored._saveOff = Toast.on('afterClose', () => {
|
||||||
if (typeof p.save === 'function')
|
if (typeof p.save === 'function')
|
||||||
p.save(this.getConfig());
|
p.save(this.getConfig());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (plugin && typeof plugin === 'object') {
|
else if (plugin && typeof plugin === 'object') {
|
||||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
||||||
}
|
}
|
||||||
@@ -4109,6 +4395,7 @@ const meToast = {
|
|||||||
if (this._destroyed)
|
if (this._destroyed)
|
||||||
return;
|
return;
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
|
Toast.trigger('beforeDestroy');
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
// 清理子模块
|
// 清理子模块
|
||||||
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
||||||
@@ -4125,20 +4412,26 @@ const meToast = {
|
|||||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||||
this.i18n.clearLocaleListeners();
|
this.i18n.clearLocaleListeners();
|
||||||
}
|
}
|
||||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
if (this.animations && typeof this.animations.destroy === 'function') {
|
||||||
this.animations.cancelAll();
|
this.animations.destroy();
|
||||||
}
|
}
|
||||||
// 清理 DOM
|
// 清理 DOM 与静态资源
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const containers = document.querySelectorAll('.met-container');
|
const containers = document.querySelectorAll('.met-container');
|
||||||
containers.forEach((c) => { if (c.parentNode)
|
containers.forEach((c) => { if (c.parentNode)
|
||||||
c.parentNode.removeChild(c); });
|
c.parentNode.removeChild(c); });
|
||||||
const style = document.getElementById('metona-toast-styles');
|
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
||||||
if (style && style.parentNode)
|
const el = document.getElementById(id);
|
||||||
style.parentNode.removeChild(style);
|
if (el && el.parentNode)
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._toasts.clear();
|
this._toasts.clear();
|
||||||
_containerCache.clear();
|
_containerCache.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
// afterDestroy 必须在清理 hooks 之前触发
|
||||||
|
Toast.trigger('afterDestroy');
|
||||||
|
Toast._hooks.clear();
|
||||||
},
|
},
|
||||||
// 子模块引用(由 index.ts 注入实际实现)
|
// 子模块引用(由 index.ts 注入实际实现)
|
||||||
animations: null,
|
animations: null,
|
||||||
@@ -4163,77 +4456,13 @@ Toast.setCallbacks({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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通知库
|
* MetonaToast — 轻量级Toast通知库
|
||||||
* @module metona-toast
|
* @module metona-toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @author thzxx
|
* @author thzxx
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
// 版本信息
|
|
||||||
const VERSION = '0.2.1';
|
|
||||||
/**
|
/**
|
||||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||||
*/
|
*/
|
||||||
@@ -4253,6 +4482,13 @@ if (typeof themeUtils.initTheme === 'function') {
|
|||||||
if (typeof i18nUtils.initI18n === 'function') {
|
if (typeof i18nUtils.initI18n === 'function') {
|
||||||
i18nUtils.initI18n();
|
i18nUtils.initI18n();
|
||||||
}
|
}
|
||||||
|
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
||||||
|
if (typeof themeUtils.addThemeListener === 'function') {
|
||||||
|
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
||||||
|
}
|
||||||
|
if (typeof i18nUtils.addLocaleListener === 'function') {
|
||||||
|
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
||||||
|
}
|
||||||
// 浏览器环境全局注册
|
// 浏览器环境全局注册
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.MeToast = enhancedMeToast;
|
window.MeToast = enhancedMeToast;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+356
-120
@@ -7,7 +7,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Utils — 工具函数
|
* MetonaToast Utils — 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 生成唯一ID
|
* 生成唯一ID
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Icons — 图标SVG定义
|
* MetonaToast Icons — 图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 107 个内置 SVG 图标
|
* @description 107 个内置 SVG 图标
|
||||||
*/
|
*/
|
||||||
const ICONS = {
|
const ICONS = {
|
||||||
@@ -541,7 +541,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Locales — 国际化翻译数据
|
* MetonaToast Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 内置 zh-CN / en-US 完整翻译
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
const LOCALES = {
|
const LOCALES = {
|
||||||
@@ -1028,8 +1028,12 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Constants — 常量定义
|
* MetonaToast Constants — 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* 版本号 — 唯一来源,发布时只需修改此处
|
||||||
|
*/
|
||||||
|
const VERSION = '0.5.0';
|
||||||
/**
|
/**
|
||||||
* 默认配置
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
@@ -1042,6 +1046,7 @@
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
dragThreshold: 120,
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
progressDirection: 'horizontal',
|
progressDirection: 'horizontal',
|
||||||
icon: true,
|
icon: true,
|
||||||
@@ -1287,11 +1292,15 @@
|
|||||||
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
closeHoverBg: 'rgba(245, 158, 11, 0.1)',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
/**
|
||||||
|
* 动画类型
|
||||||
|
*/
|
||||||
|
const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetonaToast Styles — 样式管理
|
* MetonaToast Styles — 样式管理
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 样式缓存
|
// 样式缓存
|
||||||
let styleElement = null;
|
let styleElement = null;
|
||||||
@@ -1649,6 +1658,41 @@
|
|||||||
transition: transform 0.1s linear;
|
transition: transform 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||||
|
[dir="rtl"] .met-bar {
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-side {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-progress-v {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-success {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-error {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #ef4444;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-warning {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-info {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-loading {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
.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 { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||||
@@ -1745,7 +1789,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Themes — 主题管理
|
* MetonaToast Themes — 主题管理
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 当前主题状态
|
// 当前主题状态
|
||||||
let currentTheme = 'auto';
|
let currentTheme = 'auto';
|
||||||
@@ -2089,7 +2133,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast i18n — 国际化管理
|
* MetonaToast i18n — 国际化管理
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
// 当前语言状态
|
// 当前语言状态
|
||||||
let currentLocale = 'zh-CN';
|
let currentLocale = 'zh-CN';
|
||||||
@@ -2457,7 +2501,7 @@
|
|||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2472,7 +2516,7 @@
|
|||||||
...options,
|
...options,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return amount.toString();
|
return amount.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2488,7 +2532,7 @@
|
|||||||
...options,
|
...options,
|
||||||
}).format(value / 100);
|
}).format(value / 100);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return `${value}%`;
|
return `${value}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2500,7 +2544,7 @@
|
|||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2544,7 +2588,7 @@
|
|||||||
}
|
}
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2571,7 +2615,7 @@
|
|||||||
try {
|
try {
|
||||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch {
|
||||||
return count === 1 ? 'one' : 'other';
|
return count === 1 ? 'one' : 'other';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -2645,10 +2689,126 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetonaToast Animations — 动画管理
|
||||||
|
* @module animations
|
||||||
|
* @version 0.2.1
|
||||||
|
*/
|
||||||
|
// 动画缓存
|
||||||
|
const animationMap = new Map();
|
||||||
|
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
||||||
|
const BUILTIN_ANIMATIONS = new Set(ANIMATION_TYPES);
|
||||||
|
// 动态注入的自定义动画样式(惰性创建)
|
||||||
|
let animStyleElement = null;
|
||||||
|
const injectedAnimations = new Set();
|
||||||
|
/**
|
||||||
|
* 将 CSS 属性对象转换为内联 CSS 字符串
|
||||||
|
*/
|
||||||
|
const toCss = (props) => Object.entries(props)
|
||||||
|
.map(([key, value]) => `${key}:${value}`)
|
||||||
|
.join(';');
|
||||||
|
/**
|
||||||
|
* 为自定义动画生成入场 keyframes + 类规则
|
||||||
|
*/
|
||||||
|
const buildAnimationCSS = (name, config) => {
|
||||||
|
const enter = toCss(config.enter);
|
||||||
|
return `
|
||||||
|
@keyframes met-${name}-in {
|
||||||
|
from { ${enter}; }
|
||||||
|
}
|
||||||
|
.met-anim-${name}.met-toast { opacity: 0; }
|
||||||
|
.met-anim-${name}.met-toast.met-show {
|
||||||
|
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
||||||
|
*/
|
||||||
|
const ensureAnimationCSS = (name) => {
|
||||||
|
if (typeof document === 'undefined')
|
||||||
|
return;
|
||||||
|
if (BUILTIN_ANIMATIONS.has(name))
|
||||||
|
return;
|
||||||
|
const config = animationMap.get(name);
|
||||||
|
if (!config || injectedAnimations.has(name))
|
||||||
|
return;
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.getElementById('metona-toast-anim-styles');
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.createElement('style');
|
||||||
|
animStyleElement.id = 'metona-toast-anim-styles';
|
||||||
|
document.head.appendChild(animStyleElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
animStyleElement.textContent += buildAnimationCSS(name, config);
|
||||||
|
injectedAnimations.add(name);
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* 检查动画是否存在(内置或自定义)
|
||||||
|
*/
|
||||||
|
const hasAnimation = (name) => animationMap.has(name);
|
||||||
|
// 注册默认动画
|
||||||
|
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();
|
||||||
|
injectedAnimations.clear();
|
||||||
|
animStyleElement = null;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* MetonaToast Toast — Toast 类
|
* MetonaToast Toast — Toast 类
|
||||||
* @module toast
|
* @module toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* Toast 类 — 核心通知组件
|
* Toast 类 — 核心通知组件
|
||||||
@@ -2667,17 +2827,25 @@
|
|||||||
if (!this._hooks.has(name))
|
if (!this._hooks.has(name))
|
||||||
this._hooks.set(name, []);
|
this._hooks.set(name, []);
|
||||||
this._hooks.get(name).push(fn);
|
this._hooks.get(name).push(fn);
|
||||||
return () => this.off(name, fn);
|
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||||
|
return () => Toast.off(name, fn);
|
||||||
}
|
}
|
||||||
static off(name, fn) {
|
static off(name, fn) {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (list)
|
if (list) {
|
||||||
this._hooks.set(name, list.filter(f => f !== fn));
|
const next = list.filter(f => f !== fn);
|
||||||
|
// 空数组直接删除 key,避免残留
|
||||||
|
if (next.length === 0)
|
||||||
|
this._hooks.delete(name);
|
||||||
|
else
|
||||||
|
this._hooks.set(name, next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||||
|
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||||
*/
|
*/
|
||||||
static trigger(name, toast) {
|
static trigger(name, toast = null) {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (!list || list.length === 0)
|
if (!list || list.length === 0)
|
||||||
return true;
|
return true;
|
||||||
@@ -2692,7 +2860,7 @@
|
|||||||
console.error('Hook error:', name, e);
|
console.error('Hook error:', name, e);
|
||||||
if (Toast._onError) {
|
if (Toast._onError) {
|
||||||
try {
|
try {
|
||||||
Toast._onError({ hook: name, error: e, toast });
|
Toast._onError({ hook: name, error: e, toast: toast });
|
||||||
}
|
}
|
||||||
catch (_e) { /* noop */ }
|
catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
@@ -2747,6 +2915,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
ensureAnimationCSS(this.config.animation || 'slide');
|
||||||
const container = this._getContainer();
|
const container = this._getContainer();
|
||||||
const { theme, c, t } = this._palette();
|
const { theme, c, t } = this._palette();
|
||||||
this._limitToasts(container);
|
this._limitToasts(container);
|
||||||
@@ -2766,6 +2935,7 @@
|
|||||||
else {
|
else {
|
||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
}
|
}
|
||||||
|
Toast._registry.set(this.id, this);
|
||||||
this._bindEvents(el);
|
this._bindEvents(el);
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
// 进入动画
|
// 进入动画
|
||||||
@@ -2806,6 +2976,9 @@
|
|||||||
el.className = `met-container ${position}`;
|
el.className = `met-container ${position}`;
|
||||||
el.setAttribute('aria-label', 'Notifications');
|
el.setAttribute('aria-label', 'Notifications');
|
||||||
el.setAttribute('role', 'region');
|
el.setAttribute('role', 'region');
|
||||||
|
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||||
|
if (isRTL)
|
||||||
|
el.setAttribute('dir', 'rtl');
|
||||||
const posStyles = {
|
const posStyles = {
|
||||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||||
'top-center': 'top:0;left:0;right:0;align-items:center',
|
'top-center': 'top:0;left:0;right:0;align-items:center',
|
||||||
@@ -2833,18 +3006,22 @@
|
|||||||
_limitToasts(container) {
|
_limitToasts(container) {
|
||||||
const max = this.config.max || 6;
|
const max = this.config.max || 6;
|
||||||
const list = Array.from(container.querySelectorAll('.met-toast'));
|
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||||
if (list.length >= max) {
|
while (list.length >= max) {
|
||||||
const first = list[0];
|
const el = list.shift();
|
||||||
const id = first?.dataset.id;
|
const id = el?.dataset.id;
|
||||||
if (id && Toast._removeToast) {
|
if (!id)
|
||||||
|
continue;
|
||||||
|
const existing = Toast._registry.get(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.close(true);
|
||||||
|
}
|
||||||
|
else if (Toast._removeToast) {
|
||||||
Toast._removeToast(id);
|
Toast._removeToast(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_buildClassName(theme) {
|
_buildClassName(theme) {
|
||||||
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
|
const anim = hasAnimation(this.config.animation || '') ? this.config.animation : 'slide';
|
||||||
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
|
||||||
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
|
|
||||||
return [
|
return [
|
||||||
'met-toast',
|
'met-toast',
|
||||||
`met-${this.type}`,
|
`met-${this.type}`,
|
||||||
@@ -2921,6 +3098,7 @@
|
|||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
Toast.trigger('click', this);
|
||||||
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||||
if (typeof this.config.onClick === 'function') {
|
if (typeof this.config.onClick === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -2937,8 +3115,14 @@
|
|||||||
el.addEventListener('click', eventHandler);
|
el.addEventListener('click', eventHandler);
|
||||||
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||||
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||||
const mouseEnter = () => this._pause();
|
const mouseEnter = () => {
|
||||||
const mouseLeave = () => this._resume();
|
Toast.trigger('hover', this);
|
||||||
|
this._pause();
|
||||||
|
};
|
||||||
|
const mouseLeave = () => {
|
||||||
|
Toast.trigger('hover', this);
|
||||||
|
this._resume();
|
||||||
|
};
|
||||||
el.addEventListener('mouseenter', mouseEnter);
|
el.addEventListener('mouseenter', mouseEnter);
|
||||||
el.addEventListener('mouseleave', mouseLeave);
|
el.addEventListener('mouseleave', mouseLeave);
|
||||||
this._cleanups.push(() => {
|
this._cleanups.push(() => {
|
||||||
@@ -2946,6 +3130,15 @@
|
|||||||
el.removeEventListener('mouseleave', mouseLeave);
|
el.removeEventListener('mouseleave', mouseLeave);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
||||||
|
const animStart = () => Toast.trigger('animationStart', this);
|
||||||
|
const animEnd = () => Toast.trigger('animationEnd', this);
|
||||||
|
el.addEventListener('animationstart', animStart);
|
||||||
|
el.addEventListener('animationend', animEnd);
|
||||||
|
this._cleanups.push(() => {
|
||||||
|
el.removeEventListener('animationstart', animStart);
|
||||||
|
el.removeEventListener('animationend', animEnd);
|
||||||
|
});
|
||||||
if (this.config.draggable) {
|
if (this.config.draggable) {
|
||||||
this._bindDrag(el);
|
this._bindDrag(el);
|
||||||
}
|
}
|
||||||
@@ -2961,6 +3154,7 @@
|
|||||||
el.setPointerCapture(e.pointerId);
|
el.setPointerCapture(e.pointerId);
|
||||||
el.style.transition = 'none';
|
el.style.transition = 'none';
|
||||||
this._pause();
|
this._pause();
|
||||||
|
Toast.trigger('dragStart', this);
|
||||||
};
|
};
|
||||||
const move = (e) => {
|
const move = (e) => {
|
||||||
if (!dragging)
|
if (!dragging)
|
||||||
@@ -2976,7 +3170,8 @@
|
|||||||
dragging = false;
|
dragging = false;
|
||||||
el.releasePointerCapture(e.pointerId);
|
el.releasePointerCapture(e.pointerId);
|
||||||
el.style.transition = '';
|
el.style.transition = '';
|
||||||
if (Math.abs(dx) > 120) {
|
const threshold = this.config.dragThreshold ?? 120;
|
||||||
|
if (Math.abs(dx) > threshold) {
|
||||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
setTimeout(() => this.close(true), 250);
|
setTimeout(() => this.close(true), 250);
|
||||||
@@ -2987,6 +3182,7 @@
|
|||||||
this._resume();
|
this._resume();
|
||||||
}
|
}
|
||||||
dx = dy = 0;
|
dx = dy = 0;
|
||||||
|
Toast.trigger('dragEnd', this);
|
||||||
};
|
};
|
||||||
el.addEventListener('pointerdown', down);
|
el.addEventListener('pointerdown', down);
|
||||||
el.addEventListener('pointermove', move);
|
el.addEventListener('pointermove', move);
|
||||||
@@ -3005,6 +3201,7 @@
|
|||||||
if (!resuming) {
|
if (!resuming) {
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
this.remaining = this.config.duration || 0;
|
this.remaining = this.config.duration || 0;
|
||||||
|
Toast.trigger('progressStart', this);
|
||||||
}
|
}
|
||||||
const tick = () => {
|
const tick = () => {
|
||||||
if (this.paused || this.closing)
|
if (this.paused || this.closing)
|
||||||
@@ -3019,6 +3216,7 @@
|
|||||||
this.barEl.style.transform = t;
|
this.barEl.style.transform = t;
|
||||||
}
|
}
|
||||||
if (this.remaining <= 0) {
|
if (this.remaining <= 0) {
|
||||||
|
Toast.trigger('progressEnd', this);
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -3052,7 +3250,9 @@
|
|||||||
this._startTimer(true);
|
this._startTimer(true);
|
||||||
}
|
}
|
||||||
update(partial) {
|
update(partial) {
|
||||||
Toast.trigger('beforeUpdate', this);
|
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
||||||
|
if (!Toast.trigger('beforeUpdate', this))
|
||||||
|
return this;
|
||||||
const typeChanged = partial.type && partial.type !== this.type;
|
const typeChanged = partial.type && partial.type !== this.type;
|
||||||
if (partial.type)
|
if (partial.type)
|
||||||
this.type = partial.type;
|
this.type = partial.type;
|
||||||
@@ -3089,6 +3289,18 @@
|
|||||||
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||||
}
|
}
|
||||||
|
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||||
|
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||||
|
this.config.duration = partial.duration;
|
||||||
|
if (this.rafId !== null)
|
||||||
|
cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
this.remaining = Math.max(0, partial.duration);
|
||||||
|
if (partial.duration > 0 && !this.paused) {
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||||
if (this.rafId !== null)
|
if (this.rafId !== null)
|
||||||
@@ -3149,14 +3361,17 @@
|
|||||||
this.el.parentNode.removeChild(this.el);
|
this.el.parentNode.removeChild(this.el);
|
||||||
}
|
}
|
||||||
this.el = null;
|
this.el = null;
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast)
|
if (Toast._removeToast)
|
||||||
Toast._removeToast(this.id);
|
Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
close(immediate = false) {
|
close(immediate = false) {
|
||||||
if (this.closing)
|
if (this.closing)
|
||||||
return;
|
return;
|
||||||
|
// beforeClose 钩子 — 返回 false 可阻止关闭
|
||||||
|
if (!Toast.trigger('beforeClose', this))
|
||||||
|
return;
|
||||||
this.closing = true;
|
this.closing = true;
|
||||||
Toast.trigger('beforeClose', this);
|
|
||||||
if (this.rafId !== null)
|
if (this.rafId !== null)
|
||||||
cancelAnimationFrame(this.rafId);
|
cancelAnimationFrame(this.rafId);
|
||||||
this._cleanups.forEach(fn => { try {
|
this._cleanups.forEach(fn => { try {
|
||||||
@@ -3212,12 +3427,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Toast.trigger('afterClose', this);
|
Toast.trigger('afterClose', this);
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast)
|
if (Toast._removeToast)
|
||||||
Toast._removeToast(this.id);
|
Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 静态钩子系统
|
// 静态钩子系统
|
||||||
Toast._hooks = new Map();
|
Toast._hooks = new Map();
|
||||||
|
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
||||||
|
Toast._registry = new Map();
|
||||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||||
Toast._onError = null;
|
Toast._onError = null;
|
||||||
Toast._removeToast = null;
|
Toast._removeToast = null;
|
||||||
@@ -3229,7 +3447,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Templates — HTML 模板辅助函数
|
* MetonaToast Templates — HTML 模板辅助函数
|
||||||
* @module templates
|
* @module templates
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
@@ -3301,7 +3519,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Plugins — 插件系统
|
* MetonaToast Plugins — 插件系统
|
||||||
* @module plugins
|
* @module plugins
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 插件管理器
|
* 插件管理器
|
||||||
@@ -3456,6 +3674,12 @@
|
|||||||
}
|
}
|
||||||
catch (_e) { /* noop */ }
|
catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
|
// 卸载 use() 注册的配置保存钩子
|
||||||
|
const meta = this;
|
||||||
|
if (typeof meta._saveOff === 'function') {
|
||||||
|
meta._saveOff();
|
||||||
|
meta._saveOff = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -3478,6 +3702,40 @@
|
|||||||
setTimeout(() => { if (el.parentNode)
|
setTimeout(() => { if (el.parentNode)
|
||||||
el.parentNode.removeChild(el); }, 3000);
|
el.parentNode.removeChild(el); }, 3000);
|
||||||
},
|
},
|
||||||
|
uninstall() {
|
||||||
|
// 卸载 use() 注册的朗读钩子
|
||||||
|
const meta = this;
|
||||||
|
if (typeof meta._off === 'function') {
|
||||||
|
meta._off();
|
||||||
|
meta._off = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||||
|
*/
|
||||||
|
dedupe: {
|
||||||
|
name: 'dedupe',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||||
|
_off: null,
|
||||||
|
install() {
|
||||||
|
const off = Toast.on('beforeShow', (toast) => {
|
||||||
|
const existing = Array.from(Toast._registry.values()).find(t => !t.closing && t.type === toast.type && t.message === toast.message);
|
||||||
|
if (existing) {
|
||||||
|
existing.update({ title: toast.title || existing.title });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
this._off = off;
|
||||||
|
},
|
||||||
|
uninstall() {
|
||||||
|
const off = this._off;
|
||||||
|
if (off)
|
||||||
|
off();
|
||||||
|
this._off = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
@@ -3521,7 +3779,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast API — meToast 核心 API 对象
|
* MetonaToast API — meToast 核心 API 对象
|
||||||
* @module api
|
* @module api
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 参数标准化工具
|
* 参数标准化工具
|
||||||
@@ -3546,15 +3804,13 @@
|
|||||||
message: '',
|
message: '',
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** 版本号常量 */
|
|
||||||
const VERSION$1 = '0.2.1';
|
|
||||||
/**
|
/**
|
||||||
* meToast API 对象
|
* meToast API 对象
|
||||||
*/
|
*/
|
||||||
const meToast = {
|
const meToast = {
|
||||||
_toasts: new Map(),
|
_toasts: new Map(),
|
||||||
_config: { ...DEFAULTS },
|
_config: { ...DEFAULTS },
|
||||||
version: '0.2.1',
|
version: VERSION,
|
||||||
configure(opts) {
|
configure(opts) {
|
||||||
if (!opts || typeof opts !== 'object')
|
if (!opts || typeof opts !== 'object')
|
||||||
return this;
|
return this;
|
||||||
@@ -3565,6 +3821,7 @@
|
|||||||
if (opts.locale) {
|
if (opts.locale) {
|
||||||
setCurrentLocale(opts.locale);
|
setCurrentLocale(opts.locale);
|
||||||
}
|
}
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
_emit(opts) {
|
_emit(opts) {
|
||||||
@@ -3573,7 +3830,10 @@
|
|||||||
merged.message = merged.content;
|
merged.message = merged.content;
|
||||||
const t = new Toast(merged);
|
const t = new Toast(merged);
|
||||||
t.create();
|
t.create();
|
||||||
|
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||||
|
if (t.el !== null) {
|
||||||
this._toasts.set(t.id, t);
|
this._toasts.set(t.id, t);
|
||||||
|
}
|
||||||
return t;
|
return t;
|
||||||
},
|
},
|
||||||
_remove(id) {
|
_remove(id) {
|
||||||
@@ -3639,14 +3899,21 @@
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||||
|
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||||
|
*/
|
||||||
_resolve(loadingToast, type, message, opts) {
|
_resolve(loadingToast, type, message, opts) {
|
||||||
const id = loadingToast.id;
|
const old = this._toasts.get(loadingToast.id);
|
||||||
const old = this._toasts.get(id);
|
|
||||||
if (!old)
|
if (!old)
|
||||||
return null;
|
return null;
|
||||||
const position = old.config.position;
|
old.update({
|
||||||
old.close();
|
...opts,
|
||||||
return this._emit({ ...opts, type, message, position });
|
type,
|
||||||
|
message,
|
||||||
|
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||||
|
});
|
||||||
|
return old;
|
||||||
},
|
},
|
||||||
confirm(message, opts = {}) {
|
confirm(message, opts = {}) {
|
||||||
if (typeof message !== 'string') {
|
if (typeof message !== 'string') {
|
||||||
@@ -3848,12 +4115,14 @@
|
|||||||
actions.forEach((a, i) => {
|
actions.forEach((a, i) => {
|
||||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
const btn = toast.el?.querySelector(`.met-action-btn-${i}`);
|
||||||
if (btn && typeof a.onClick === 'function') {
|
if (btn && typeof a.onClick === 'function') {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', (e) => {
|
||||||
|
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||||
|
e.stopPropagation();
|
||||||
try {
|
try {
|
||||||
a.onClick(toast);
|
a.onClick(toast);
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (err) {
|
||||||
console.error('Action onClick error:', e);
|
console.error('Action onClick error:', err);
|
||||||
}
|
}
|
||||||
if (a.close !== false)
|
if (a.close !== false)
|
||||||
toast.close();
|
toast.close();
|
||||||
@@ -4036,6 +4305,8 @@
|
|||||||
},
|
},
|
||||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||||
init(options = {}) {
|
init(options = {}) {
|
||||||
|
Toast.trigger('beforeInit');
|
||||||
|
this._destroyed = false;
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
this.configure(options.config);
|
this.configure(options.config);
|
||||||
}
|
}
|
||||||
@@ -4050,11 +4321,12 @@
|
|||||||
this.use(p);
|
this.use(p);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
Toast.trigger('afterInit');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
getStatus() {
|
getStatus() {
|
||||||
return {
|
return {
|
||||||
version: VERSION$1,
|
version: VERSION,
|
||||||
toasts: this._toasts.size,
|
toasts: this._toasts.size,
|
||||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||||
@@ -4068,6 +4340,7 @@
|
|||||||
updateConfig(config) {
|
updateConfig(config) {
|
||||||
if (config && typeof config === 'object') {
|
if (config && typeof config === 'object') {
|
||||||
Object.assign(this._config, config);
|
Object.assign(this._config, config);
|
||||||
|
Toast.trigger('configChange');
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -4075,6 +4348,7 @@
|
|||||||
const keys = Object.keys(this._config);
|
const keys = Object.keys(this._config);
|
||||||
keys.forEach(k => delete this._config[k]);
|
keys.forEach(k => delete this._config[k]);
|
||||||
Object.assign(this._config, DEFAULTS);
|
Object.assign(this._config, DEFAULTS);
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
use(plugin, options = {}) {
|
use(plugin, options = {}) {
|
||||||
@@ -4085,27 +4359,39 @@
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
this.plugins.register(plugin, { ...preset, ...options });
|
this.plugins.register(plugin, { ...preset, ...options });
|
||||||
// 连接插件钩子
|
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||||
|
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||||
|
const stored = this.plugins.get(plugin);
|
||||||
if (plugin === 'accessibility') {
|
if (plugin === 'accessibility') {
|
||||||
Toast.on('afterShow', (toast) => {
|
if (stored && typeof stored._off === 'function') {
|
||||||
|
stored._off();
|
||||||
|
}
|
||||||
const acc = preset;
|
const acc = preset;
|
||||||
|
if (stored) {
|
||||||
|
stored._off = Toast.on('afterShow', (toast) => {
|
||||||
if (typeof acc.announce === 'function')
|
if (typeof acc.announce === 'function')
|
||||||
acc.announce(toast);
|
acc.announce(toast);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (plugin === 'persistence') {
|
if (plugin === 'persistence') {
|
||||||
|
if (stored && typeof stored._saveOff === 'function') {
|
||||||
|
stored._saveOff();
|
||||||
|
}
|
||||||
const saved = typeof preset.install === 'function'
|
const saved = typeof preset.install === 'function'
|
||||||
? preset.install(this.plugins)
|
? preset.install(this.plugins)
|
||||||
: null;
|
: null;
|
||||||
if (saved)
|
if (saved)
|
||||||
this.configure(saved);
|
this.configure(saved);
|
||||||
Toast.on('afterClose', () => {
|
|
||||||
const p = preset;
|
const p = preset;
|
||||||
|
if (stored) {
|
||||||
|
stored._saveOff = Toast.on('afterClose', () => {
|
||||||
if (typeof p.save === 'function')
|
if (typeof p.save === 'function')
|
||||||
p.save(this.getConfig());
|
p.save(this.getConfig());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
else if (plugin && typeof plugin === 'object') {
|
else if (plugin && typeof plugin === 'object') {
|
||||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options });
|
||||||
}
|
}
|
||||||
@@ -4115,6 +4401,7 @@
|
|||||||
if (this._destroyed)
|
if (this._destroyed)
|
||||||
return;
|
return;
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
|
Toast.trigger('beforeDestroy');
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
// 清理子模块
|
// 清理子模块
|
||||||
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
||||||
@@ -4131,20 +4418,26 @@
|
|||||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||||
this.i18n.clearLocaleListeners();
|
this.i18n.clearLocaleListeners();
|
||||||
}
|
}
|
||||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
if (this.animations && typeof this.animations.destroy === 'function') {
|
||||||
this.animations.cancelAll();
|
this.animations.destroy();
|
||||||
}
|
}
|
||||||
// 清理 DOM
|
// 清理 DOM 与静态资源
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const containers = document.querySelectorAll('.met-container');
|
const containers = document.querySelectorAll('.met-container');
|
||||||
containers.forEach((c) => { if (c.parentNode)
|
containers.forEach((c) => { if (c.parentNode)
|
||||||
c.parentNode.removeChild(c); });
|
c.parentNode.removeChild(c); });
|
||||||
const style = document.getElementById('metona-toast-styles');
|
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
||||||
if (style && style.parentNode)
|
const el = document.getElementById(id);
|
||||||
style.parentNode.removeChild(style);
|
if (el && el.parentNode)
|
||||||
|
el.parentNode.removeChild(el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this._toasts.clear();
|
this._toasts.clear();
|
||||||
_containerCache.clear();
|
_containerCache.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
// afterDestroy 必须在清理 hooks 之前触发
|
||||||
|
Toast.trigger('afterDestroy');
|
||||||
|
Toast._hooks.clear();
|
||||||
},
|
},
|
||||||
// 子模块引用(由 index.ts 注入实际实现)
|
// 子模块引用(由 index.ts 注入实际实现)
|
||||||
animations: null,
|
animations: null,
|
||||||
@@ -4169,77 +4462,13 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 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通知库
|
* MetonaToast — 轻量级Toast通知库
|
||||||
* @module metona-toast
|
* @module metona-toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @author thzxx
|
* @author thzxx
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
// 版本信息
|
|
||||||
const VERSION = '0.2.1';
|
|
||||||
/**
|
/**
|
||||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||||
*/
|
*/
|
||||||
@@ -4259,6 +4488,13 @@
|
|||||||
if (typeof i18nUtils.initI18n === 'function') {
|
if (typeof i18nUtils.initI18n === 'function') {
|
||||||
i18nUtils.initI18n();
|
i18nUtils.initI18n();
|
||||||
}
|
}
|
||||||
|
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
||||||
|
if (typeof themeUtils.addThemeListener === 'function') {
|
||||||
|
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
||||||
|
}
|
||||||
|
if (typeof i18nUtils.addLocaleListener === 'function') {
|
||||||
|
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
||||||
|
}
|
||||||
// 浏览器环境全局注册
|
// 浏览器环境全局注册
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.MeToast = enhancedMeToast;
|
window.MeToast = enhancedMeToast;
|
||||||
|
|||||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+4617
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+509
@@ -0,0 +1,509 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast — 核心类型定义
|
||||||
|
* @module types
|
||||||
|
* @version 0.5.0
|
||||||
|
*/
|
||||||
|
/** Toast 通知类型(107种内置图标类型) */
|
||||||
|
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | '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';
|
||||||
|
/** Toast 显示位置 */
|
||||||
|
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||||
|
/** Toast 主题 */
|
||||||
|
type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
|
||||||
|
/** Toast 动画类型 */
|
||||||
|
type ToastAnimation = 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | (string & {});
|
||||||
|
/** 进度条方向 */
|
||||||
|
type ToastProgressDirection = 'horizontal' | 'vertical';
|
||||||
|
interface TypeColor {
|
||||||
|
fg: string;
|
||||||
|
bg: string;
|
||||||
|
}
|
||||||
|
interface ToastConfig {
|
||||||
|
position?: ToastPosition;
|
||||||
|
duration?: number;
|
||||||
|
max?: number;
|
||||||
|
gap?: number;
|
||||||
|
offset?: number;
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
closeOnClick?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
dragThreshold?: number;
|
||||||
|
showProgress?: boolean;
|
||||||
|
progressDirection?: ToastProgressDirection;
|
||||||
|
icon?: boolean;
|
||||||
|
closeButton?: boolean;
|
||||||
|
theme?: ToastTheme;
|
||||||
|
animation?: ToastAnimation;
|
||||||
|
zIndex?: number;
|
||||||
|
width?: number | string;
|
||||||
|
className?: string;
|
||||||
|
style?: Record<string, string>;
|
||||||
|
onShow?: ((toast: ToastInstance) => void) | null;
|
||||||
|
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
|
||||||
|
onClose?: ((toast: ToastInstance) => void) | null;
|
||||||
|
onClick?: ((toast: ToastInstance) => void) | null;
|
||||||
|
onUpdate?: ((toast: ToastInstance) => void) | null;
|
||||||
|
render?: ((toast: ToastInstance) => string) | null;
|
||||||
|
resetTimerOnUpdate?: boolean;
|
||||||
|
notifyWhenHidden?: boolean;
|
||||||
|
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||||
|
group?: string | null;
|
||||||
|
locale?: string;
|
||||||
|
plugins?: Array<string | Plugin>;
|
||||||
|
}
|
||||||
|
interface ErrorInfo {
|
||||||
|
hook?: string;
|
||||||
|
source?: string;
|
||||||
|
error: Error;
|
||||||
|
toast?: ToastInstance;
|
||||||
|
}
|
||||||
|
interface ToastOptions extends Partial<ToastConfig> {
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
content?: string;
|
||||||
|
html?: string;
|
||||||
|
iconHTML?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
interface ToastInstance {
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
html: string;
|
||||||
|
iconHTML: string;
|
||||||
|
config: ToastConfig;
|
||||||
|
el: HTMLElement | null;
|
||||||
|
barEl: HTMLElement | null;
|
||||||
|
rafId: number | null;
|
||||||
|
remaining: number;
|
||||||
|
startedAt: number;
|
||||||
|
paused: boolean;
|
||||||
|
closing: boolean;
|
||||||
|
group: string | null;
|
||||||
|
_cleanups: Array<() => void>;
|
||||||
|
_palette(): {
|
||||||
|
theme: string;
|
||||||
|
c: TypeColor;
|
||||||
|
t: Partial<ThemeConfig>;
|
||||||
|
};
|
||||||
|
create(): this;
|
||||||
|
update(partial: Partial<ToastOptions>): this;
|
||||||
|
updatePosition(position: string): this;
|
||||||
|
remove(): void;
|
||||||
|
close(immediate?: boolean): void;
|
||||||
|
_pause(): void;
|
||||||
|
_resume(): void;
|
||||||
|
_destroy(): void;
|
||||||
|
}
|
||||||
|
interface LoadingControl {
|
||||||
|
id: string;
|
||||||
|
success(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||||
|
error(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||||
|
info(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||||
|
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||||
|
update(partial: Partial<ToastOptions>): unknown;
|
||||||
|
dismiss(): void;
|
||||||
|
}
|
||||||
|
interface ProgressControl {
|
||||||
|
id: string;
|
||||||
|
setProgress(percent: number): void;
|
||||||
|
complete(message?: string): void;
|
||||||
|
error(message?: string): void;
|
||||||
|
dismiss(): void;
|
||||||
|
}
|
||||||
|
interface CountdownControl {
|
||||||
|
id: string;
|
||||||
|
cancel(): void;
|
||||||
|
pause(): void;
|
||||||
|
resume(): void;
|
||||||
|
}
|
||||||
|
interface ActionControl {
|
||||||
|
id: string;
|
||||||
|
toast: ToastInstance;
|
||||||
|
dismiss(): void;
|
||||||
|
}
|
||||||
|
interface QueueControl {
|
||||||
|
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
||||||
|
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
|
||||||
|
cancel(): void;
|
||||||
|
}
|
||||||
|
interface GroupAPI {
|
||||||
|
_group: string;
|
||||||
|
show: (...args: unknown[]) => ToastInstance;
|
||||||
|
success: (...args: unknown[]) => ToastInstance;
|
||||||
|
error: (...args: unknown[]) => ToastInstance;
|
||||||
|
warning: (...args: unknown[]) => ToastInstance;
|
||||||
|
info: (...args: unknown[]) => ToastInstance;
|
||||||
|
loading: (...args: unknown[]) => LoadingControl;
|
||||||
|
action: (...args: unknown[]) => ActionControl;
|
||||||
|
dismiss(): void;
|
||||||
|
count(): number;
|
||||||
|
}
|
||||||
|
interface ActionButton {
|
||||||
|
text: string;
|
||||||
|
onClick: (toast: ToastInstance) => void;
|
||||||
|
color?: string;
|
||||||
|
style?: Record<string, string>;
|
||||||
|
close?: boolean;
|
||||||
|
}
|
||||||
|
interface PromiseOptions extends ToastOptions {
|
||||||
|
loading?: string;
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
interface ConfirmOptions extends ToastOptions {
|
||||||
|
confirmText?: string;
|
||||||
|
confirmColor?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
cancelColor?: string;
|
||||||
|
}
|
||||||
|
interface PromptOptions extends ToastOptions {
|
||||||
|
inputType?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
submitText?: string;
|
||||||
|
submitColor?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
cancelColor?: string;
|
||||||
|
}
|
||||||
|
interface ProgressOptions extends ToastOptions {
|
||||||
|
progressColor?: string;
|
||||||
|
}
|
||||||
|
interface CountdownOptions extends ToastOptions {
|
||||||
|
onComplete?: (() => void) | null;
|
||||||
|
}
|
||||||
|
interface QueueOptions extends ToastOptions {
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
interface StackOptions extends ToastOptions {
|
||||||
|
stagger?: number;
|
||||||
|
}
|
||||||
|
interface InitOptions {
|
||||||
|
config?: Partial<ToastConfig>;
|
||||||
|
theme?: ToastTheme;
|
||||||
|
locale?: string;
|
||||||
|
plugins?: Array<string | Plugin>;
|
||||||
|
}
|
||||||
|
interface StatusInfo {
|
||||||
|
version: string;
|
||||||
|
toasts: number;
|
||||||
|
theme: string;
|
||||||
|
locale: string;
|
||||||
|
plugins: string[];
|
||||||
|
animations: number;
|
||||||
|
}
|
||||||
|
interface AnimationConfig {
|
||||||
|
enter: Record<string, string | number>;
|
||||||
|
leave: Record<string, string | number>;
|
||||||
|
duration: number;
|
||||||
|
easing: string;
|
||||||
|
name?: string;
|
||||||
|
delay?: number;
|
||||||
|
iterations?: number;
|
||||||
|
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
||||||
|
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
|
||||||
|
}
|
||||||
|
interface AnimationUtils {
|
||||||
|
register(name: string, config: Partial<AnimationConfig>): void;
|
||||||
|
unregister(name: string): void;
|
||||||
|
get(name: string): AnimationConfig | null;
|
||||||
|
getAnimationNames(): string[];
|
||||||
|
getActiveCount(): number;
|
||||||
|
cancelAll(): void;
|
||||||
|
reset(): void;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
interface ThemeConfig {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
shadow: string;
|
||||||
|
hoverShadow: string;
|
||||||
|
progressBg: string;
|
||||||
|
closeHoverBg: string;
|
||||||
|
backdropFilter?: string;
|
||||||
|
}
|
||||||
|
interface ThemePreview {
|
||||||
|
name: string;
|
||||||
|
resolved: string;
|
||||||
|
colors: {
|
||||||
|
background: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
};
|
||||||
|
isDark: boolean;
|
||||||
|
isLight: boolean;
|
||||||
|
isAuto: boolean;
|
||||||
|
}
|
||||||
|
interface ThemeUtils {
|
||||||
|
getSystemTheme(): 'light' | 'dark';
|
||||||
|
resolveTheme(theme: string): string;
|
||||||
|
getThemeConfig(theme: string): ThemeConfig;
|
||||||
|
applyTheme(theme: string): void;
|
||||||
|
getCurrentTheme(): string;
|
||||||
|
getResolvedTheme(): string;
|
||||||
|
switchTheme(theme: string): void;
|
||||||
|
toggleTheme(): void;
|
||||||
|
resetToAuto(): void;
|
||||||
|
initTheme(): void;
|
||||||
|
watchSystemTheme(): void;
|
||||||
|
unwatchSystemTheme(): void;
|
||||||
|
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
|
||||||
|
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
|
||||||
|
clearThemeListeners(): void;
|
||||||
|
registerTheme(name: string, config: ThemeConfig): void;
|
||||||
|
unregisterTheme(name: string): void;
|
||||||
|
getAllThemes(): Record<string, ThemeConfig>;
|
||||||
|
getThemeNames(): string[];
|
||||||
|
hasTheme(name: string): boolean;
|
||||||
|
getThemePreview(theme: string): ThemePreview;
|
||||||
|
generateThemeCSS(theme: string): string;
|
||||||
|
applyThemeCSS(theme: string): void;
|
||||||
|
removeThemeCSS(): void;
|
||||||
|
saveTheme(theme: string): void;
|
||||||
|
loadTheme(): string;
|
||||||
|
}
|
||||||
|
interface LocaleInfo {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
direction: 'ltr' | 'rtl';
|
||||||
|
isSupported: boolean;
|
||||||
|
isCurrent: boolean;
|
||||||
|
isFallback: boolean;
|
||||||
|
}
|
||||||
|
interface I18nUtils {
|
||||||
|
t(key: string, params?: Record<string, string | number>): string;
|
||||||
|
plural(key: string, count: number, params?: Record<string, string | number>): string;
|
||||||
|
getCurrentLocale(): string;
|
||||||
|
setCurrentLocale(locale: string): void;
|
||||||
|
switchLocale(locale: string): void;
|
||||||
|
getFallbackLocale(): string;
|
||||||
|
setFallbackLocale(locale: string): void;
|
||||||
|
hasTranslation(key: string): boolean;
|
||||||
|
getTranslations(locale: string): Record<string, unknown>;
|
||||||
|
addTranslations(locale: string, translations: Record<string, unknown>): void;
|
||||||
|
removeTranslation(locale: string, key: string): void;
|
||||||
|
clearTranslations(locale: string): void;
|
||||||
|
getSupportedLocales(): string[];
|
||||||
|
isLocaleSupported(locale: string): boolean;
|
||||||
|
getLocaleName(locale: string): string;
|
||||||
|
getLocaleDirection(locale: string): 'ltr' | 'rtl';
|
||||||
|
getLocaleInfo(locale: string): LocaleInfo;
|
||||||
|
getAllLocaleInfo(): LocaleInfo[];
|
||||||
|
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
|
||||||
|
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
||||||
|
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
|
||||||
|
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||||
|
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||||
|
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
|
||||||
|
formatList(list: string[], options?: Record<string, unknown>): string;
|
||||||
|
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
|
||||||
|
addLocaleListener(listener: (locale: string) => void): () => void;
|
||||||
|
removeLocaleListener(listener: (locale: string) => void): void;
|
||||||
|
clearLocaleListeners(): void;
|
||||||
|
initI18n(): void;
|
||||||
|
saveLocale(locale: string): void;
|
||||||
|
loadLocale(): string;
|
||||||
|
getDefaultLocale(): string;
|
||||||
|
}
|
||||||
|
interface Plugin {
|
||||||
|
name: string;
|
||||||
|
version?: string;
|
||||||
|
description?: string;
|
||||||
|
hooks?: Record<string, (...args: unknown[]) => void>;
|
||||||
|
middleware?: (...args: unknown[]) => unknown;
|
||||||
|
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
|
||||||
|
uninstall?: (manager: PluginManager) => void;
|
||||||
|
init?: (manager: PluginManager) => void;
|
||||||
|
destroy?: (manager: PluginManager) => void;
|
||||||
|
installed?: boolean;
|
||||||
|
enabled?: boolean;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
interface PluginManager {
|
||||||
|
register(name: string, plugin: Plugin): PluginManager;
|
||||||
|
unregister(name: string): PluginManager;
|
||||||
|
get(name: string): Plugin | null;
|
||||||
|
has(name: string): boolean;
|
||||||
|
getAll(): Plugin[];
|
||||||
|
getNames(): string[];
|
||||||
|
enable(name: string): PluginManager;
|
||||||
|
disable(name: string): PluginManager;
|
||||||
|
isEnabled(name: string): boolean;
|
||||||
|
destroy(): void;
|
||||||
|
}
|
||||||
|
interface PluginUtils {
|
||||||
|
createManager(): PluginManager;
|
||||||
|
register(name: string, plugin: Plugin): PluginManager;
|
||||||
|
unregister(name: string): PluginManager;
|
||||||
|
get(name: string): Plugin | null;
|
||||||
|
has(name: string): boolean;
|
||||||
|
getAll(): Plugin[];
|
||||||
|
getNames(): string[];
|
||||||
|
enable(name: string): PluginManager;
|
||||||
|
disable(name: string): PluginManager;
|
||||||
|
isEnabled(name: string): boolean;
|
||||||
|
getPreset(name: string): Plugin | null;
|
||||||
|
getAllPresets(): Record<string, Plugin>;
|
||||||
|
createPlugin(config: Partial<Plugin>): Plugin;
|
||||||
|
validatePlugin(plugin: Partial<Plugin>): {
|
||||||
|
valid: boolean;
|
||||||
|
errors: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
interface MeToast {
|
||||||
|
version: string;
|
||||||
|
_toasts: Map<string, ToastInstance>;
|
||||||
|
_config: ToastConfig;
|
||||||
|
_destroyed?: boolean;
|
||||||
|
_emit(opts: ToastOptions): ToastInstance;
|
||||||
|
_remove(id: string): void;
|
||||||
|
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||||
|
_groupCount(name: string): number;
|
||||||
|
configure(opts: Partial<ToastConfig>): MeToast;
|
||||||
|
init(options?: InitOptions): MeToast;
|
||||||
|
destroy(): void;
|
||||||
|
getStatus(): StatusInfo;
|
||||||
|
getConfig(): ToastConfig;
|
||||||
|
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||||
|
resetConfig(): MeToast;
|
||||||
|
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||||
|
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||||
|
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||||
|
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||||
|
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||||
|
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||||
|
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||||
|
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||||
|
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||||
|
group(name: string): GroupAPI;
|
||||||
|
dismissGroup(name: string): void;
|
||||||
|
find(id: string): ToastInstance | undefined;
|
||||||
|
dismiss(id?: string): void;
|
||||||
|
removeToast(id: string): void;
|
||||||
|
clear(position?: ToastPosition): void;
|
||||||
|
getToasts(): ToastInstance[];
|
||||||
|
count(): number;
|
||||||
|
hasToasts(): boolean;
|
||||||
|
getToast(id: string): ToastInstance | null;
|
||||||
|
closeAll(): void;
|
||||||
|
clearAll(): void;
|
||||||
|
pauseAll(): void;
|
||||||
|
resumeAll(): void;
|
||||||
|
updateAll(partial: Partial<ToastOptions>): void;
|
||||||
|
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||||
|
findByType(type: ToastType): ToastInstance[];
|
||||||
|
findByPosition(position: ToastPosition): ToastInstance[];
|
||||||
|
getAll(): Map<string, ToastInstance>;
|
||||||
|
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||||
|
animations: AnimationUtils;
|
||||||
|
themes: ThemeUtils;
|
||||||
|
i18n: I18nUtils;
|
||||||
|
plugins: PluginUtils;
|
||||||
|
presetPlugins: Record<string, Plugin>;
|
||||||
|
}
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
MeToast: MeToast;
|
||||||
|
Met: MeToast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||||
|
*/
|
||||||
|
declare const enhancedMeToast: {
|
||||||
|
version: string;
|
||||||
|
animations: AnimationUtils;
|
||||||
|
themes: ThemeUtils;
|
||||||
|
i18n: I18nUtils;
|
||||||
|
plugins: PluginUtils;
|
||||||
|
presetPlugins: Record<string, Plugin>;
|
||||||
|
_toasts: Map<string, ToastInstance>;
|
||||||
|
_config: ToastConfig;
|
||||||
|
_destroyed?: boolean;
|
||||||
|
_emit(opts: ToastOptions): ToastInstance;
|
||||||
|
_remove(id: string): void;
|
||||||
|
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||||
|
_groupCount(name: string): number;
|
||||||
|
configure(opts: Partial<ToastConfig>): MeToast;
|
||||||
|
init(options?: InitOptions): MeToast;
|
||||||
|
destroy(): void;
|
||||||
|
getStatus(): StatusInfo;
|
||||||
|
getConfig(): ToastConfig;
|
||||||
|
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||||
|
resetConfig(): MeToast;
|
||||||
|
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||||
|
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||||
|
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||||
|
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||||
|
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||||
|
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||||
|
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||||
|
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||||
|
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||||
|
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||||
|
group(name: string): GroupAPI;
|
||||||
|
dismissGroup(name: string): void;
|
||||||
|
find(id: string): ToastInstance | undefined;
|
||||||
|
dismiss(id?: string): void;
|
||||||
|
removeToast(id: string): void;
|
||||||
|
clear(position?: ToastPosition): void;
|
||||||
|
getToasts(): ToastInstance[];
|
||||||
|
count(): number;
|
||||||
|
hasToasts(): boolean;
|
||||||
|
getToast(id: string): ToastInstance | null;
|
||||||
|
closeAll(): void;
|
||||||
|
clearAll(): void;
|
||||||
|
pauseAll(): void;
|
||||||
|
resumeAll(): void;
|
||||||
|
updateAll(partial: Partial<ToastOptions>): void;
|
||||||
|
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||||
|
findByType(type: ToastType): ToastInstance[];
|
||||||
|
findByPosition(position: ToastPosition): ToastInstance[];
|
||||||
|
getAll(): Map<string, ToastInstance>;
|
||||||
|
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MetonaToast React — React 适配器
|
||||||
|
* @module react
|
||||||
|
* @version 0.5.0
|
||||||
|
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 支持声明式渲染的 Toast 类型 */
|
||||||
|
type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||||
|
interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||||
|
message: string;
|
||||||
|
type?: ReactToastType;
|
||||||
|
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||||
|
autoClose?: boolean;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||||
|
* 纯函数,便于独立测试与扩展。
|
||||||
|
*/
|
||||||
|
declare const createBoundApi: (cleanup: Set<string>) => Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||||
|
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||||
|
*/
|
||||||
|
declare const useToast: () => Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||||
|
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||||
|
*/
|
||||||
|
declare const Toast: (props: ToastProps) => null;
|
||||||
|
|
||||||
|
export { enhancedMeToast as MeToast, ReactToastType, Toast, ToastProps, createBoundApi, useToast };
|
||||||
Vendored
+4612
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -1,5 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
testEnvironment: 'jsdom',
|
testEnvironment: 'jsdom',
|
||||||
|
setupFiles: ['<rootDir>/tests/setup.ts'],
|
||||||
transform: {
|
transform: {
|
||||||
'^.+\\.ts$': 'babel-jest',
|
'^.+\\.ts$': 'babel-jest',
|
||||||
},
|
},
|
||||||
@@ -17,5 +18,13 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
coverageDirectory: 'coverage',
|
coverageDirectory: 'coverage',
|
||||||
coverageReporters: ['text', 'lcov'],
|
coverageReporters: ['text', 'lcov'],
|
||||||
|
coverageThreshold: {
|
||||||
|
global: {
|
||||||
|
statements: 85,
|
||||||
|
branches: 65,
|
||||||
|
functions: 85,
|
||||||
|
lines: 95,
|
||||||
|
},
|
||||||
|
},
|
||||||
verbose: true,
|
verbose: true,
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+99
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@metona-team/metona-toast",
|
"name": "@metona-team/metona-toast",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@metona-team/metona-toast",
|
"name": "@metona-team/metona-toast",
|
||||||
"version": "0.2.0",
|
"version": "0.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.0",
|
"@babel/core": "^7.22.0",
|
||||||
@@ -18,6 +18,8 @@
|
|||||||
"@rollup/plugin-terser": "^0.4.0",
|
"@rollup/plugin-terser": "^0.4.0",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
|
"@types/react": "^18.3.31",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"babel-jest": "^29.5.0",
|
"babel-jest": "^29.5.0",
|
||||||
@@ -25,6 +27,8 @@
|
|||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
"rollup": "^3.20.0",
|
"rollup": "^3.20.0",
|
||||||
"rollup-plugin-dts": "^5.3.0",
|
"rollup-plugin-dts": "^5.3.0",
|
||||||
"rollup-plugin-livereload": "^2.0.5",
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
@@ -34,6 +38,14 @@
|
|||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=16.0.0"
|
"node": ">=16.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -2722,6 +2734,34 @@
|
|||||||
"undici-types": ">=7.24.0 <7.24.7"
|
"undici-types": ">=7.24.0 <7.24.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/prop-types": {
|
||||||
|
"version": "15.7.15",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||||
|
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "18.3.31",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
|
||||||
|
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/prop-types": "*",
|
||||||
|
"csstype": "^3.2.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-dom": {
|
||||||
|
"version": "18.3.7",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||||
|
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.20.2",
|
"version": "1.20.2",
|
||||||
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz",
|
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz",
|
||||||
@@ -3810,6 +3850,13 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||||
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/data-urls": {
|
"node_modules/data-urls": {
|
||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz",
|
"resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz",
|
||||||
@@ -6103,6 +6150,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/loose-envify": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"loose-envify": "cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
|
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||||
@@ -6713,6 +6773,33 @@
|
|||||||
"safe-buffer": "^5.1.0"
|
"safe-buffer": "^5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-dom": {
|
||||||
|
"version": "18.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
|
||||||
|
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0",
|
||||||
|
"scheduler": "^0.23.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||||
@@ -7055,6 +7142,16 @@
|
|||||||
"node": ">=v12.22.7"
|
"node": ">=v12.22.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/scheduler": {
|
||||||
|
"version": "0.23.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
|
||||||
|
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"loose-envify": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
|
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
|
||||||
|
|||||||
+19
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@metona-team/metona-toast",
|
"name": "@metona-team/metona-toast",
|
||||||
"version": "0.2.1",
|
"version": "0.5.0",
|
||||||
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/metona-toast.cjs.js",
|
"main": "dist/metona-toast.cjs.js",
|
||||||
@@ -13,6 +13,19 @@
|
|||||||
"import": "./dist/metona-toast.esm.js",
|
"import": "./dist/metona-toast.esm.js",
|
||||||
"require": "./dist/metona-toast.cjs.js",
|
"require": "./dist/metona-toast.cjs.js",
|
||||||
"types": "./dist/metona-toast.d.ts"
|
"types": "./dist/metona-toast.d.ts"
|
||||||
|
},
|
||||||
|
"./react": {
|
||||||
|
"import": "./dist/react.js",
|
||||||
|
"require": "./dist/react.cjs.js",
|
||||||
|
"types": "./dist/react.d.ts"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
@@ -31,8 +44,7 @@
|
|||||||
"format": "prettier --write src/**/*.ts tests/**/*.ts",
|
"format": "prettier --write src/**/*.ts tests/**/*.ts",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
||||||
"docs": "typedoc src/ --out docs",
|
"serve": "bash serve.sh"
|
||||||
"example": "serve examples/"
|
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -65,6 +77,8 @@
|
|||||||
"@rollup/plugin-terser": "^0.4.0",
|
"@rollup/plugin-terser": "^0.4.0",
|
||||||
"@rollup/plugin-typescript": "^11.1.6",
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
|
"@types/react": "^18.3.31",
|
||||||
|
"@types/react-dom": "^18.3.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"babel-jest": "^29.5.0",
|
"babel-jest": "^29.5.0",
|
||||||
@@ -72,6 +86,8 @@
|
|||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
"prettier": "^2.8.0",
|
"prettier": "^2.8.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
"rollup": "^3.20.0",
|
"rollup": "^3.20.0",
|
||||||
"rollup-plugin-dts": "^5.3.0",
|
"rollup-plugin-dts": "^5.3.0",
|
||||||
"rollup-plugin-livereload": "^2.0.5",
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
|
|||||||
@@ -103,5 +103,34 @@ export default [
|
|||||||
},
|
},
|
||||||
plugins: [dts()],
|
plugins: [dts()],
|
||||||
},
|
},
|
||||||
|
// React 适配器(ESM + CJS),react 为 optional peer 外部依赖
|
||||||
|
{
|
||||||
|
input: 'src/react.ts',
|
||||||
|
external: ['react'],
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
file: 'dist/react.js',
|
||||||
|
format: 'es',
|
||||||
|
exports: 'named',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
file: 'dist/react.cjs.js',
|
||||||
|
format: 'cjs',
|
||||||
|
exports: 'named',
|
||||||
|
sourcemap: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: basePlugins,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
input: 'src/react.ts',
|
||||||
|
external: ['react'],
|
||||||
|
output: {
|
||||||
|
file: 'dist/react.d.ts',
|
||||||
|
format: 'es',
|
||||||
|
},
|
||||||
|
plugins: [dts()],
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
];
|
];
|
||||||
|
|||||||
+213
-112
@@ -7,51 +7,78 @@
|
|||||||
<title>在线演示 — MetonaToast</title>
|
<title>在线演示 — MetonaToast</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0b1121; --surface: #151d33; --border: #1e2d4d;
|
--bg: #070b14; --surface: rgba(255,255,255,0.03); --surface2: rgba(255,255,255,0.05);
|
||||||
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
|
--border: rgba(255,255,255,0.08); --text: #e8ecf4; --muted: #8b94a7;
|
||||||
|
--accent: #a5b4fc; --grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||||
--radius: 14px;
|
--radius: 14px;
|
||||||
}
|
}
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.6; min-height:100vh; }
|
body {
|
||||||
a { color:var(--accent2); text-decoration:none; }
|
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||||
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.85em; }
|
background:var(--bg); color:var(--text); line-height:1.65; min-height:100vh;
|
||||||
.nav { position:sticky; top:0; z-index:100; background:rgba(11,17,33,0.88); backdrop-filter:blur(14px); border-bottom:1px solid var(--border); }
|
background-image:
|
||||||
.nav-inner { max-width:1300px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:56px; }
|
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(99,102,241,0.12), transparent),
|
||||||
.nav-logo { font-size:18px; font-weight:700; background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
radial-gradient(ellipse 50% 40% at 90% 20%, rgba(168,85,247,0.07), transparent);
|
||||||
.nav-links { display:flex; gap:20px; }
|
}
|
||||||
.nav-links a { color:var(--muted); font-size:13px; font-weight:500; transition:.2s; }
|
a { color:var(--accent); text-decoration:none; }
|
||||||
.nav-links a:hover { color:var(--accent2); }
|
code { background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px; border-radius:5px; font-size:.85em; }
|
||||||
.hero { text-align:center; padding:40px 20px 30px; border-bottom:1px solid var(--border); background:radial-gradient(ellipse at 50% 0%,rgba(99,102,241,0.12) 0%,transparent 50%); }
|
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
|
||||||
.hero h1 { font-size:36px; font-weight:800; }
|
|
||||||
.hero h1 span { background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
.nav { position:sticky; top:0; z-index:100; background:rgba(7,11,20,0.72); backdrop-filter:blur(16px); border-bottom:1px solid var(--border); }
|
||||||
|
.nav-inner { max-width:1320px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 22px; height:58px; }
|
||||||
|
.nav-logo { font-size:18px; font-weight:800; }
|
||||||
|
.nav-links { display:flex; gap:22px; }
|
||||||
|
.nav-links a { color:var(--muted); font-size:13.5px; font-weight:500; transition:.2s; }
|
||||||
|
.nav-links a:hover { color:var(--accent); }
|
||||||
|
|
||||||
|
.hero { text-align:center; padding:38px 20px 28px; }
|
||||||
|
.hero h1 { font-size:34px; font-weight:800; }
|
||||||
.hero p { color:var(--muted); font-size:14px; margin-top:6px; }
|
.hero p { color:var(--muted); font-size:14px; margin-top:6px; }
|
||||||
|
|
||||||
.container { max-width:1300px; margin:0 auto; padding:24px 20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:18px; }
|
.container { max-width:1320px; margin:0 auto; padding:20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:16px; }
|
||||||
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
|
.card {
|
||||||
.card h2 { font-size:15px; margin-bottom:2px; }
|
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||||
.card > p { color:var(--muted); font-size:11px; margin-bottom:12px; }
|
padding:20px; transition:border-color .2s, transform .2s; position:relative; overflow:hidden;
|
||||||
|
}
|
||||||
|
.card:hover { border-color:rgba(129,140,248,.35); }
|
||||||
|
.card::before {
|
||||||
|
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||||
|
background:var(--grad); opacity:0; transition:opacity .25s;
|
||||||
|
}
|
||||||
|
.card:hover::before { opacity:.8; }
|
||||||
|
.card h2 { font-size:14.5px; margin-bottom:2px; font-weight:700; }
|
||||||
|
.card > p { color:var(--muted); font-size:11.5px; margin-bottom:12px; }
|
||||||
.btn-row { display:flex; flex-wrap:wrap; gap:6px; }
|
.btn-row { display:flex; flex-wrap:wrap; gap:6px; }
|
||||||
.btn { padding:8px 14px; border:none; border-radius:7px; font-size:12px; font-weight:500; cursor:pointer; transition:.15s; color:#fff; }
|
.btn {
|
||||||
.btn:hover { transform:translateY(-1px); filter:brightness(1.15); }
|
padding:8px 14px; border:none; border-radius:8px; font-size:12px; font-weight:600;
|
||||||
.btn-sm { padding:5px 10px; font-size:11px; border-radius:5px; }
|
cursor:pointer; transition:transform .15s, filter .15s, box-shadow .15s; color:#fff;
|
||||||
|
}
|
||||||
|
.btn:hover { transform:translateY(-1.5px); filter:brightness(1.15); box-shadow:0 6px 18px -6px rgba(0,0,0,.5); }
|
||||||
|
.btn-sm { padding:5.5px 11px; font-size:11px; border-radius:6px; }
|
||||||
|
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:5px; }
|
||||||
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); }
|
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); }
|
||||||
.btn-outline:hover { border-color:var(--accent); background:rgba(99,102,241,0.06); }
|
.btn-outline:hover { border-color:rgba(129,140,248,.5); background:rgba(129,140,248,.08); }
|
||||||
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:4px; }
|
|
||||||
.c-s { background:#10b981; } .c-e { background:#ef4444; } .c-w { background:#f59e0b; color:#1e293b; }
|
.c-s { background:#10b981; } .c-e { background:#ef4444; } .c-w { background:#f59e0b; color:#1e293b; }
|
||||||
.c-i { background:#3b82f6; } .c-p { background:#6366f1; } .c-pp { background:#8b5cf6; }
|
.c-i { background:#3b82f6; } .c-p { background:#6366f1; } .c-pp { background:#8b5cf6; }
|
||||||
.c-pk { background:#ec4899; } .c-t { background:#14b8a6; } .c-o { background:#f97316; }
|
.c-pk { background:#ec4899; } .c-t { background:#14b8a6; } .c-o { background:#f97316; }
|
||||||
.c-sl { background:#475569; } .c-r { background:#f43f5e; } .c-g { background:#22c55e; }
|
.c-sl { background:#475569; } .c-r { background:#f43f5e; } .c-g { background:#22c55e; }
|
||||||
.label { font-size:10px; text-transform:uppercase; letter-spacing:0.8px; color:var(--muted); margin-bottom:10px; font-weight:600; }
|
.c-grad { background:var(--grad); }
|
||||||
.demo-section { border-top:1px solid var(--border); padding-top:8px; margin-top:8px; }
|
.label {
|
||||||
|
font-size:10px; text-transform:uppercase; letter-spacing:1px; color:var(--muted);
|
||||||
|
margin-bottom:10px; font-weight:700;
|
||||||
|
}
|
||||||
|
.demo-section { border-top:1px solid var(--border); padding-top:10px; margin-top:10px; }
|
||||||
|
.status-line { font-size:11.5px; color:#34d399; margin-top:10px; font-family:monospace; min-height:18px; }
|
||||||
footer { text-align:center; padding:30px 20px; color:var(--muted); font-size:12px; border-top:1px solid var(--border); }
|
footer { text-align:center; padding:30px 20px; color:var(--muted); font-size:12px; border-top:1px solid var(--border); }
|
||||||
@media (max-width:768px) { .container { grid-template-columns:1fr; } }
|
|
||||||
|
@media (max-width:820px) { .container { grid-template-columns:1fr; } }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<div class="nav-inner">
|
<div class="nav-inner">
|
||||||
<div class="nav-logo">MetonaToast</div>
|
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="index.html">首页</a>
|
<a href="index.html">首页</a>
|
||||||
<a href="docs.html">文档</a>
|
<a href="docs.html">文档</a>
|
||||||
@@ -62,7 +89,7 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<h1><span>在线演示</span></h1>
|
<h1><span class="grad-text">在线演示</span></h1>
|
||||||
<p>覆盖项目全部功能的交互演示 — 点击即可体验</p>
|
<p>覆盖项目全部功能的交互演示 — 点击即可体验</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -81,10 +108,10 @@
|
|||||||
<div class="demo-section">
|
<div class="demo-section">
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.success({title:'保存成功',message:'文件已同步到云端'})">带标题</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.success({title:'保存成功',message:'文件已同步到云端'})">带标题</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续10秒',{duration:10000})">长时10s</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续 10 秒',{duration:10000})">长时 10s</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.warning('手动关闭',{duration:0,closeButton:true})">手动关闭</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.warning('手动关闭',{duration:0,closeButton:true})">手动关闭</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('',{icon:false})">无图标</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.info('',{icon:false})">无图标</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无按钮',{closeButton:false})">无关闭按钮</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无关闭按钮',{closeButton:false})">无按钮</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="demoCustomIcon()">自定义图标</button>
|
<button class="btn btn-outline btn-sm" onclick="demoCustomIcon()">自定义图标</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -92,16 +119,17 @@
|
|||||||
|
|
||||||
<!-- 2. Loading 链式 -->
|
<!-- 2. Loading 链式 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🔄 Loading 链式转换</div>
|
<div class="label">🔄 Loading 链式转换 <span style="color:#34d399">id 稳定</span></div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-s" onclick="demoLoading()">提交成功</button>
|
<button class="btn c-s" onclick="demoLoading('success')">提交成功</button>
|
||||||
<button class="btn c-e" onclick="demoLoadingFail()">提交失败</button>
|
<button class="btn c-e" onclick="demoLoading('error')">提交失败</button>
|
||||||
<button class="btn c-w" onclick="demoLoadingWarn()">转为警告</button>
|
<button class="btn c-w" onclick="demoLoading('warning')">转为警告</button>
|
||||||
<button class="btn c-i" onclick="demoLoadingUpdate()">原地更新</button>
|
<button class="btn c-i" onclick="demoLoading('update')">原地更新</button>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="status-line" id="loading-status">转换后返回的 toast 与 loading 是同一个实例(id 不变)</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 3. Promise + confirm + prompt -->
|
<!-- 3. 对话框 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">💬 对话框</div>
|
<div class="label">💬 对话框</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -116,8 +144,8 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">📊 进度 & 倒计时</div>
|
<div class="label">📊 进度 & 倒计时</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-o" onclick="demoProgress()">进度条(水平)</button>
|
<button class="btn c-o" onclick="demoProgress('horizontal')">进度条(水平)</button>
|
||||||
<button class="btn c-g" onclick="demoProgressV()">进度条(垂直)</button>
|
<button class="btn c-g" onclick="demoProgress('vertical')">进度条(垂直)</button>
|
||||||
<button class="btn c-pk" onclick="demoCountdown()">倒计时 5s</button>
|
<button class="btn c-pk" onclick="demoCountdown()">倒计时 5s</button>
|
||||||
<button class="btn c-r" onclick="demoCountdownPause()">倒计时(可暂停)</button>
|
<button class="btn c-r" onclick="demoCountdownPause()">倒计时(可暂停)</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -136,7 +164,7 @@
|
|||||||
|
|
||||||
<!-- 6. Action Toast -->
|
<!-- 6. Action Toast -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">⚡ Action Toast</div>
|
<div class="label">⚡ Action Toast <span style="color:#34d399">close:false 不关闭</span></div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
|
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
|
||||||
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
|
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
|
||||||
@@ -157,7 +185,7 @@
|
|||||||
|
|
||||||
<!-- 8. 动画效果 -->
|
<!-- 8. 动画效果 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🎭 11种动画效果</div>
|
<div class="label">🎭 11 种动画效果</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn btn-outline btn-tiny" onclick="anim('slide')">slide</button>
|
<button class="btn btn-outline btn-tiny" onclick="anim('slide')">slide</button>
|
||||||
<button class="btn btn-outline btn-tiny" onclick="anim('fade')">fade</button>
|
<button class="btn btn-outline btn-tiny" onclick="anim('fade')">fade</button>
|
||||||
@@ -171,9 +199,14 @@
|
|||||||
<button class="btn btn-outline btn-tiny" onclick="anim('slideLeft')">slideLeft</button>
|
<button class="btn btn-outline btn-tiny" onclick="anim('slideLeft')">slideLeft</button>
|
||||||
<button class="btn btn-outline btn-tiny" onclick="anim('slideRight')">slideRight</button>
|
<button class="btn btn-outline btn-tiny" onclick="anim('slideRight')">slideRight</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="demo-section">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn c-grad" onclick="demoCustomAnim()">✨ 注册自定义动画</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 9. 6种位置 -->
|
<!-- 9. 位置 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">📍 全部位置</div>
|
<div class="label">📍 全部位置</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -186,7 +219,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 10. 主题切换 + 自定义 -->
|
<!-- 10. 主题 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🎨 主题切换 & 自定义</div>
|
<div class="label">🎨 主题切换 & 自定义</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -194,24 +227,43 @@
|
|||||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('dark')">🌙 Dark</button>
|
<button class="btn btn-outline btn-sm" onclick="switchTheme('dark')">🌙 Dark</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('auto')">🔄 Auto</button>
|
<button class="btn btn-outline btn-sm" onclick="switchTheme('auto')">🔄 Auto</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('warm')">🔥 Warm</button>
|
<button class="btn btn-outline btn-sm" onclick="switchTheme('warm')">🔥 Warm</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义主题</button>
|
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 11. 插件系统 -->
|
<!-- 11. 插件 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🔌 插件系统</div>
|
<div class="label">🔌 插件系统 <span style="color:#34d399">4 款预设</span></div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard ESC关闭</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置将持久化到 localStorage')">persistence 持久化</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置持久化')">persistence</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器可用')">accessibility 无障碍</button>
|
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器')">accessibility</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="MeToast.use('dedupe');MeToast.info('已安装 — 同消息去重')">dedupe</button>
|
||||||
<button class="btn btn-outline btn-sm" onclick="demoCustomPlugin()">自定义插件</button>
|
<button class="btn btn-outline btn-sm" onclick="demoCustomPlugin()">自定义插件</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="demo-section">
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn c-t btn-sm" onclick="demoDedupe()">去重演示</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 12. 高级特性 -->
|
<!-- 12. 钩子系统 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🔥 高级特性 <span style="color:#34d399;font-size:10px;">resetTimer · onUpdate · render · notify</span></div>
|
<div class="label">🔗 钩子系统 <span style="color:#34d399">beforeShow 拦截演示</span></div>
|
||||||
|
<div class="btn-row">
|
||||||
|
<button class="btn c-p" onclick="demoHookBlock()">开启拦截 error</button>
|
||||||
|
<button class="btn c-e" onclick="MeToast.error('这条会被拦截')">触发被拦 error</button>
|
||||||
|
<button class="btn c-s" onclick="MeToast.success('这条不受影响')">正常 success</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="demoHookUnblock()">关闭拦截</button>
|
||||||
|
<button class="btn btn-outline btn-sm" onclick="demoHookCount()">钩子统计</button>
|
||||||
|
</div>
|
||||||
|
<p class="status-line" id="hook-status">钩子:未注册</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 13. 高级特性 -->
|
||||||
|
<div class="card">
|
||||||
|
<div class="label">🔥 高级特性</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
|
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
|
||||||
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
|
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
|
||||||
@@ -220,7 +272,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 13. Toast 管理 -->
|
<!-- 14. Toast 管理 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🔧 Toast 管理</div>
|
<div class="label">🔧 Toast 管理</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -230,7 +282,7 @@
|
|||||||
<button class="btn c-sl" onclick="MeToast.pauseAll();MeToast.info('已暂停全部')">暂停全部</button>
|
<button class="btn c-sl" onclick="MeToast.pauseAll();MeToast.info('已暂停全部')">暂停全部</button>
|
||||||
<button class="btn c-sl" onclick="MeToast.resumeAll();MeToast.info('已恢复全部')">恢复全部</button>
|
<button class="btn c-sl" onclick="MeToast.resumeAll();MeToast.info('已恢复全部')">恢复全部</button>
|
||||||
<button class="btn c-e" onclick="MeToast.dismiss()">关闭全部</button>
|
<button class="btn c-e" onclick="MeToast.dismiss()">关闭全部</button>
|
||||||
<button class="btn c-r" onclick="MeToast.destroy();MeToast.info('已销毁')">销毁</button>
|
<button class="btn c-r" onclick="MeToast.destroy();MeToast.init({});MeToast.info('已销毁并重建')">销毁重建</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="demo-section">
|
<div class="demo-section">
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -238,32 +290,34 @@
|
|||||||
<button class="btn btn-outline btn-tiny" onclick="demoGetAll()">getAll</button>
|
<button class="btn btn-outline btn-tiny" onclick="demoGetAll()">getAll</button>
|
||||||
<button class="btn btn-outline btn-tiny" onclick="demoClearByPos()">clear(位置)</button>
|
<button class="btn btn-outline btn-tiny" onclick="demoClearByPos()">clear(位置)</button>
|
||||||
<button class="btn btn-outline btn-tiny" onclick="demoGetConfig()">查看配置</button>
|
<button class="btn btn-outline btn-tiny" onclick="demoGetConfig()">查看配置</button>
|
||||||
|
<button class="btn btn-outline btn-tiny" onclick="demoRemove()">removeToast 立即移除</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 14. 错误回调 -->
|
<!-- 15. onError -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.1</span></div>
|
<div class="label">🛡️ onError 错误捕获</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
|
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
|
||||||
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
|
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 15. 交互特性 -->
|
<!-- 16. 交互特性 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">✋ 交互特性</div>
|
<div class="label">✋ 交互特性</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<button class="btn c-i" onclick="demoDraggable()">拖拽关闭</button>
|
<button class="btn c-i" onclick="demoDraggable()">拖拽关闭</button>
|
||||||
<button class="btn c-p" onclick="demoPauseHover()">悬停暂停</button>
|
<button class="btn c-p" onclick="demoDragThreshold()">拖拽阈值 300px</button>
|
||||||
<button class="btn c-w" onclick="demoCloseOnClick()">点击关闭</button>
|
<button class="btn c-w" onclick="demoPauseHover()">悬停暂停</button>
|
||||||
<button class="btn c-pp" onclick="demoWidth()">自定义宽度</button>
|
<button class="btn c-pp" onclick="demoCloseOnClick()">点击关闭</button>
|
||||||
<button class="btn c-t" onclick="demoClassName()">自定义className</button>
|
<button class="btn c-t" onclick="demoWidth()">自定义宽度</button>
|
||||||
|
<button class="btn c-sl" onclick="demoClassName()">自定义 className</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 16. 国际化切换 -->
|
<!-- 17. 国际化 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🌍 国际化 & 格式化</div>
|
<div class="label">🌍 国际化 & 格式化</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
@@ -273,16 +327,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 17. 107种预设图标 -->
|
<!-- 18. 图标网格 -->
|
||||||
<div class="card" style="grid-column:1/-1;">
|
<div class="card" style="grid-column:1/-1;">
|
||||||
<div class="label">🎨 107种预设图标 — 点击任意图标发送Toast</div>
|
<div class="label">🎨 107 种预设图标 — 点击任意图标发送 Toast</div>
|
||||||
<div class="btn-row" id="icon-grid" style="max-height:300px;overflow-y:auto;padding:4px 0;"></div>
|
<div class="btn-row" id="icon-grid" style="max-height:320px;overflow-y:auto;padding:4px 0;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
MetonaToast v0.2.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
MetonaToast v0.5.0 · MIT ·
|
||||||
|
<a href="index.html">首页</a> ·
|
||||||
|
<a href="docs.html">文档</a> ·
|
||||||
|
<a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="../dist/metona-toast.js"></script>
|
<script src="../dist/metona-toast.js"></script>
|
||||||
@@ -290,27 +347,41 @@
|
|||||||
MeToast.configure({ position:'top-right', duration:4000, theme:'dark', animation:'slide', pauseOnHover:true, showProgress:true });
|
MeToast.configure({ position:'top-right', duration:4000, theme:'dark', animation:'slide', pauseOnHover:true, showProgress:true });
|
||||||
|
|
||||||
// === Loading ===
|
// === Loading ===
|
||||||
function demoLoading() { const l=MeToast.loading('正在提交...'); setTimeout(()=>l.success('提交成功!'),2000); }
|
function demoLoading(mode) {
|
||||||
function demoLoadingFail() { const l=MeToast.loading('正在处理...'); setTimeout(()=>l.error('处理失败'),2000); }
|
const l = MeToast.loading('正在处理...');
|
||||||
function demoLoadingWarn() { const l=MeToast.loading('检查中...'); setTimeout(()=>l.warning('发现潜在问题'),2000); }
|
const showId = () => document.getElementById('loading-status').textContent = 'loading id: ' + l.id;
|
||||||
function demoLoadingUpdate() { const l=MeToast.loading('处理中 0%'); let i=1; const t=setInterval(()=>{if(i>3){clearInterval(t);l.success('完成!');return;} l.update({message:'处理中 '+i*33+'%'}); i++;},600); }
|
showId();
|
||||||
|
setTimeout(() => {
|
||||||
|
const map = { success:['提交成功!','success'], error:['处理失败','error'], warning:['发现潜在问题','warning'], update:null };
|
||||||
|
if (mode === 'update') {
|
||||||
|
l.update({ message: '进度 50%' });
|
||||||
|
setTimeout(() => l.success('完成!'), 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const [msg, type] = map[mode];
|
||||||
|
const t = l[type](msg);
|
||||||
|
if (t) document.getElementById('loading-status').textContent = '转换后 id: ' + t.id + '(' + (t.id === l.id ? '同一实例 ✓' : '不同实例 ✗') + ')';
|
||||||
|
}, 1600);
|
||||||
|
}
|
||||||
|
|
||||||
// === Promise / confirm / prompt ===
|
// === Promise / confirm / prompt ===
|
||||||
async function demoPromise() {
|
async function demoPromise() {
|
||||||
const p=new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
|
const p = new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
|
||||||
try{await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'});}catch(_){}
|
try{ await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'}); }catch(_){}
|
||||||
}
|
}
|
||||||
async function demoConfirm() { const ok=await MeToast.confirm('确定删除?',{confirmText:'删除',confirmColor:'#ef4444'}); if(ok)MeToast.success('已删除'); }
|
async function demoConfirm() { const ok=await MeToast.confirm('确定删除?',{confirmText:'删除',confirmColor:'#ef4444'}); if(ok)MeToast.success('已删除'); }
|
||||||
async function demoPrompt() { const v=await MeToast.prompt('请输入姓名',{placeholder:'请输入...'}); if(v) MeToast.info('你好,'+v+'!'); }
|
async function demoPrompt() { const v=await MeToast.prompt('请输入姓名',{placeholder:'请输入...'}); if(v) MeToast.info('你好,'+v+'!'); }
|
||||||
async function demoPromptPassword() { const v=await MeToast.prompt('请输入密码',{placeholder:'密码',inputType:'password',submitText:'登录'}); if(v) MeToast.info('密码已输入'); }
|
async function demoPromptPassword() { const v=await MeToast.prompt('请输入密码',{placeholder:'密码',inputType:'password',submitText:'登录'}); if(v) MeToast.info('密码已输入'); }
|
||||||
|
|
||||||
// === Progress & Countdown ===
|
// === Progress & Countdown ===
|
||||||
function demoProgress() { const p=MeToast.progress('上传中...',{progressColor:'#3b82f6'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400); }
|
function demoProgress(dir) {
|
||||||
function demoProgressV() { const p=MeToast.progress('上传中...',{progressColor:'#10b981',progressDirection:'vertical'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('完成!');}else p.setProgress(v);},400); }
|
const p=MeToast.progress('上传中...',{progressColor: dir==='vertical'?'#10b981':'#3b82f6', progressDirection: dir});
|
||||||
|
let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400);
|
||||||
|
}
|
||||||
function demoCountdown() { MeToast.countdown('{seconds} 秒后执行',5,{onComplete:()=>MeToast.success('已执行')}); }
|
function demoCountdown() { MeToast.countdown('{seconds} 秒后执行',5,{onComplete:()=>MeToast.success('已执行')}); }
|
||||||
function demoCountdownPause() {
|
function demoCountdownPause() {
|
||||||
const c=MeToast.countdown('{seconds}s 可暂停',10,{type:'info',onComplete:()=>MeToast.success('倒计时结束')});
|
const c=MeToast.countdown('{seconds}s 可暂停',10,{type:'info',onComplete:()=>MeToast.success('倒计时结束')});
|
||||||
setTimeout(()=>{c.pause();MeToast.warning('已暂停3秒后恢复',{duration:3000})},4000);
|
setTimeout(()=>{c.pause();MeToast.warning('已暂停 3 秒后恢复',{duration:3000})},4000);
|
||||||
setTimeout(()=>{c.resume();MeToast.info('已恢复倒计时')},7000);
|
setTimeout(()=>{c.resume();MeToast.info('已恢复倒计时')},7000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,14 +392,15 @@
|
|||||||
setTimeout(()=>{q.cancel();MeToast.warning('队列已取消',{duration:2000});},3500);
|
setTimeout(()=>{q.cancel();MeToast.warning('队列已取消',{duration:2000});},3500);
|
||||||
}
|
}
|
||||||
function demoStack() { MeToast.stack(['消息1','消息2','消息3','消息4','消息5'],{stagger:120,type:'info'}); }
|
function demoStack() { MeToast.stack(['消息1','消息2','消息3','消息4','消息5'],{stagger:120,type:'info'}); }
|
||||||
function demoStackMixed() {
|
function demoStackMixed() { MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150}); }
|
||||||
MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === Action ===
|
// === Action ===
|
||||||
function demoActionDelete() { MeToast.action('文件 data.json 已删除',[{text:'撤销',onClick:()=>MeToast.success('已撤销'),color:'#3b82f6'}]); }
|
function demoActionDelete() { MeToast.action('文件 data.json 已删除',[{text:'撤销',onClick:()=>MeToast.success('已撤销'),color:'#3b82f6'}]); }
|
||||||
function demoActionMulti() { MeToast.action('邮件已发送',[{text:'查看',onClick:()=>MeToast.info('打开邮件详情'),color:'#3b82f6'},{text:'撤回',onClick:()=>MeToast.warning('已撤回'),color:'#f59e0b'}]); }
|
function demoActionMulti() { MeToast.action('邮件已发送',[{text:'查看',onClick:()=>MeToast.info('打开邮件详情'),color:'#3b82f6'},{text:'撤回',onClick:()=>MeToast.warning('已撤回'),color:'#f59e0b'}]); }
|
||||||
function demoActionNoClose() { MeToast.action('完成后点击关闭',[{text:'我知道了',onClick:()=>{},color:'#10b981'}]); }
|
function demoActionNoClose() {
|
||||||
|
let n=0;
|
||||||
|
MeToast.action('点「+1」不会关闭,可连续点击',[{text:'+1',onClick:(t)=>{n++;t.update({message:'已点击 '+n+' 次(close:false 不关闭)'});},color:'#10b981',close:false}],{duration:8000});
|
||||||
|
}
|
||||||
|
|
||||||
// === Group ===
|
// === Group ===
|
||||||
const group = MeToast.group('demo-group');
|
const group = MeToast.group('demo-group');
|
||||||
@@ -342,32 +414,71 @@
|
|||||||
|
|
||||||
// === Animation ===
|
// === Animation ===
|
||||||
function anim(name) { MeToast.success('动画: '+name,{animation:name,duration:3000,showProgress:false}); }
|
function anim(name) { MeToast.success('动画: '+name,{animation:name,duration:3000,showProgress:false}); }
|
||||||
|
function demoCustomAnim() {
|
||||||
|
MeToast.animations.register('swing', {
|
||||||
|
enter: { transform: 'rotate(-40deg) scale(0.3)', opacity: 0 },
|
||||||
|
leave: { transform: 'rotate(20deg) scale(0.5)', opacity: 0 },
|
||||||
|
duration: 600,
|
||||||
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
|
});
|
||||||
|
MeToast.success('自定义 swing 动画!', { animation: 'swing', duration: 3000 });
|
||||||
|
}
|
||||||
|
|
||||||
// === Theme ===
|
// === Theme ===
|
||||||
function switchTheme(t) { MeToast.themes.switchTheme(t); MeToast.configure({theme:t}); MeToast.info('主题: '+t,{theme:t}); }
|
function switchTheme(t) { MeToast.themes.switchTheme(t); MeToast.configure({theme:t}); MeToast.info('主题: '+t,{theme:t}); }
|
||||||
function demoRegisterTheme() {
|
function demoRegisterTheme() {
|
||||||
try {
|
try {
|
||||||
MeToast.themes.registerTheme('sunset',{bg:'rgba(255,248,240,0.96)',text:'#92400e',border:'rgba(249,115,22,0.2)',shadow:'0 10px 36px -10px rgba(249,115,22,0.2)',hoverShadow:'0 14px 48px -10px rgba(249,115,22,0.3)',progressBg:'rgba(249,115,22,0.1)',closeHoverBg:'rgba(249,115,22,0.1)'});
|
MeToast.themes.registerTheme('sunset',{bg:'rgba(255,248,240,0.96)',text:'#92400e',border:'rgba(249,115,22,0.2)',shadow:'0 10px 36px -10px rgba(249,115,22,0.2)',hoverShadow:'0 14px 48px -10px rgba(249,115,22,0.3)',progressBg:'rgba(249,115,22,0.1)',closeHoverBg:'rgba(249,115,22,0.1)'});
|
||||||
MeToast.themes.switchTheme('sunset');
|
MeToast.themes.switchTheme('sunset'); MeToast.configure({theme:'sunset'});
|
||||||
MeToast.configure({theme:'sunset'});
|
|
||||||
MeToast.success('已切换到 sunset 主题',{theme:'sunset'});
|
MeToast.success('已切换到 sunset 主题',{theme:'sunset'});
|
||||||
} catch(_) { MeToast.warning('主题已注册,已切换'); }
|
} catch(_) { MeToast.warning('主题已注册,已切换'); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// === Plugins ===
|
// === Plugins ===
|
||||||
function demoCustomPlugin() {
|
function demoCustomPlugin() {
|
||||||
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\\(\\d+\\)/,'')+' (总通知#'+c+')'}),1);});}});
|
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\(\d+\)/,'')+' (总通知#'+c+')'}),1);});}});
|
||||||
MeToast.info('自定义插件已安装(消息后追加计数)');
|
MeToast.info('自定义插件已安装(消息后追加计数)');
|
||||||
}
|
}
|
||||||
|
function demoDedupe() {
|
||||||
|
if (!MeToast.plugins.has('dedupe')) MeToast.use('dedupe');
|
||||||
|
MeToast.info('同一条消息');
|
||||||
|
MeToast.info('同一条消息');
|
||||||
|
MeToast.info('同一条消息');
|
||||||
|
MeToast.info('当前仅 ' + MeToast.count() + ' 条 — 相同消息自动合并');
|
||||||
|
}
|
||||||
|
|
||||||
|
// === Hooks ===
|
||||||
|
let hookOff = null;
|
||||||
|
function demoHookBlock() {
|
||||||
|
if (hookOff) hookOff();
|
||||||
|
hookOff = Toast.on('beforeShow', (toast) => {
|
||||||
|
if (toast.type === 'error') { document.getElementById('hook-status').textContent = '钩子:error 已被拦截 ✋'; return false; }
|
||||||
|
});
|
||||||
|
document.getElementById('hook-status').textContent = '钩子:已注册(error 类型将被拦截)';
|
||||||
|
MeToast.info('拦截器已开启 — 试试触发 error');
|
||||||
|
}
|
||||||
|
function demoHookUnblock() {
|
||||||
|
if (hookOff) { hookOff(); hookOff = null; }
|
||||||
|
document.getElementById('hook-status').textContent = '钩子:已移除';
|
||||||
|
MeToast.info('拦截器已关闭');
|
||||||
|
}
|
||||||
|
function demoHookCount() {
|
||||||
|
const stats = [];
|
||||||
|
['afterShow','afterClose','configChange','themeChange'].forEach(name => {
|
||||||
|
const list = Toast._hooks.get(name);
|
||||||
|
stats.push(name + ':' + (list ? list.length : 0));
|
||||||
|
});
|
||||||
|
MeToast.info('钩子数量 — ' + stats.join(' '), { duration: 5000 });
|
||||||
|
}
|
||||||
|
|
||||||
// === 高级特性 ===
|
// === 高级特性 ===
|
||||||
function demoResetTimer() {
|
function demoResetTimer() {
|
||||||
const t=MeToast.info('这条消息不会自动消失(resetTimer)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
const t=MeToast.info('这条不会自动消失(每次更新重置 3 秒)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
||||||
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n+' — 每次更新重置3秒'});},1000);
|
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n});},1000);
|
||||||
}
|
}
|
||||||
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); setTimeout(()=>t.update({message:'再次更新!'}),2000); }
|
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); }
|
||||||
function demoCustomRender() {
|
function demoCustomRender() {
|
||||||
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render by thzxx</span></div>',message:'自定义渲染内容',duration:5000});
|
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render</span></div>',message:'自定义渲染内容',duration:5000});
|
||||||
}
|
}
|
||||||
function demoNotify() {
|
function demoNotify() {
|
||||||
if(typeof Notification==='undefined'){MeToast.error('浏览器不支持 Notification');return;}
|
if(typeof Notification==='undefined'){MeToast.error('浏览器不支持 Notification');return;}
|
||||||
@@ -382,48 +493,38 @@
|
|||||||
}
|
}
|
||||||
function demoGetAll() { const all=MeToast.getAll(); MeToast.info('当前共 '+all.size+' 条 toast'); }
|
function demoGetAll() { const all=MeToast.getAll(); MeToast.info('当前共 '+all.size+' 条 toast'); }
|
||||||
function demoClearByPos() { MeToast.error('右下角临时消息',{position:'bottom-right',duration:3000}); setTimeout(()=>{MeToast.clear('bottom-right');MeToast.info('右下角已清除');},500); }
|
function demoClearByPos() { MeToast.error('右下角临时消息',{position:'bottom-right',duration:3000}); setTimeout(()=>{MeToast.clear('bottom-right');MeToast.info('右下角已清除');},500); }
|
||||||
function demoGetConfig() { try{const c=MeToast.getConfig();MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000});}catch(e){MeToast.error('无 getConfig 方法');} }
|
function demoGetConfig() { const c=MeToast.getConfig(); MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000}); }
|
||||||
|
function demoRemove() { const t=MeToast.warning('这条将被立即移除(无动画)'); setTimeout(()=>{MeToast.removeToast(t.id);MeToast.info('已立即移除');},800); }
|
||||||
// === Interaction ===
|
|
||||||
function demoDraggable() { MeToast.info('← 试试拖动我来关闭',{draggable:true,duration:8000,closeButton:true}); }
|
|
||||||
function demoPauseHover() { MeToast.info('鼠标悬停在我上面,倒计时会暂停',{pauseOnHover:true,duration:8000}); }
|
|
||||||
function demoCloseOnClick() { MeToast.warning('点击我即可关闭',{closeOnClick:true,duration:10000}); }
|
|
||||||
function demoWidth() { MeToast.info('宽550px',{width:550}); setTimeout(()=>MeToast.info('宽280px(窄)',{width:280}),500); }
|
|
||||||
function demoClassName() { MeToast.success('带自定义 className',{className:'demo-custom-class'}); }
|
|
||||||
function demoCustomIcon() {
|
|
||||||
MeToast.info('通用通知', {
|
|
||||||
iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// === onError ===
|
// === onError ===
|
||||||
function demoOnError() {
|
function demoOnError() {
|
||||||
MeToast.configure({
|
MeToast.configure({ onError: function(info) { MeToast.error('⚠️ 捕获: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' }); } });
|
||||||
onError: function(info) {
|
MeToast.info('已启用 onError 监控', { duration: 3000 });
|
||||||
MeToast.error('⚠️ 捕获错误: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
MeToast.info('已启用 onError 监控(钩子/定时器异常将被捕获)', { duration: 3000 });
|
|
||||||
// 触发一个钩子错误来演示
|
|
||||||
var badFn = function() { throw new Error('demo-error'); };
|
var badFn = function() { throw new Error('demo-error'); };
|
||||||
Toast.on('afterShow', badFn);
|
Toast.on('afterShow', badFn);
|
||||||
MeToast.success('这条消息会触发错误', { duration: 2000 });
|
MeToast.success('这条会触发错误回调', { duration: 2000 });
|
||||||
setTimeout(function() { Toast.off('afterShow', badFn); }, 3000);
|
setTimeout(function() { Toast.off('afterShow', badFn); }, 3000);
|
||||||
}
|
}
|
||||||
function demoOnErrorReset() {
|
function demoOnErrorReset() { MeToast.configure({ onError: null }); MeToast.info('onError 已重置'); }
|
||||||
MeToast.configure({ onError: null });
|
|
||||||
MeToast.info('onError 已重置为 null');
|
// === Interaction ===
|
||||||
|
function demoDraggable() { MeToast.info('← 拖动我超过 120px 关闭',{draggable:true,duration:8000,closeButton:true}); }
|
||||||
|
function demoDragThreshold() { MeToast.info('← 拖动阈值 300px(要拖更远)',{draggable:true,dragThreshold:300,duration:8000,closeButton:true}); }
|
||||||
|
function demoPauseHover() { MeToast.info('悬停时倒计时会暂停',{pauseOnHover:true,duration:8000}); }
|
||||||
|
function demoCloseOnClick() { MeToast.warning('点击我即可关闭',{closeOnClick:true,duration:10000}); }
|
||||||
|
function demoWidth() { MeToast.info('宽 550px',{width:550}); setTimeout(()=>MeToast.info('宽 280px(窄)',{width:280}),500); }
|
||||||
|
function demoClassName() { MeToast.success('带自定义 className',{className:'demo-custom-class'}); }
|
||||||
|
function demoCustomIcon() {
|
||||||
|
MeToast.info('通用通知', { iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// === i18n ===
|
// === i18n ===
|
||||||
function switchLocale(loc) {
|
function switchLocale(loc) {
|
||||||
try{MeToast.i18n.switchLocale(loc);MeToast.configure({locale:loc});MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English',{theme:'dark'});}
|
MeToast.i18n.switchLocale(loc); MeToast.configure({locale:loc});
|
||||||
catch(e){MeToast.info('已切换语言');}
|
MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English');
|
||||||
}
|
}
|
||||||
function demoFormat() {
|
function demoFormat() {
|
||||||
try{
|
|
||||||
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
|
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
|
||||||
}catch(e){MeToast.info('格式化: 1234567 → 1,234,567');}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// === 图标网格 ===
|
// === 图标网格 ===
|
||||||
|
|||||||
+350
-344
@@ -7,448 +7,442 @@
|
|||||||
<title>API 文档 — MetonaToast</title>
|
<title>API 文档 — MetonaToast</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0b1121; --surface: #1a2332; --border: #1e2d4d;
|
--bg: #070b14; --surface: rgba(255,255,255,0.03); --border: rgba(255,255,255,0.08);
|
||||||
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
|
--text: #e8ecf4; --muted: #8b94a7; --accent: #a5b4fc; --accent2: #c7d2fe;
|
||||||
--radius: 10px;
|
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||||
|
--radius: 12px;
|
||||||
}
|
}
|
||||||
* { margin:0; padding:0; box-sizing:border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.7; display:flex; min-height:100vh; }
|
html { scroll-behavior:smooth; }
|
||||||
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.9em; white-space:nowrap; }
|
body {
|
||||||
pre { background:#0a0f1a; border:1px solid var(--border); border-radius:8px; padding:14px 18px; overflow-x:auto; font:13px/1.7 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; margin:10px 0; }
|
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||||
pre .kw { color:#c084fc; } pre .fn { color:#60a5fa; } pre .s { color:#34d399; } pre .cm { color:#475569; }
|
background:var(--bg); color:var(--text); line-height:1.75; display:flex; min-height:100vh;
|
||||||
a { color:var(--accent2); text-decoration:none; }
|
background-image:
|
||||||
|
radial-gradient(ellipse 70% 40% at 80% -10%, rgba(99,102,241,0.12), transparent),
|
||||||
|
radial-gradient(ellipse 50% 40% at 0% 20%, rgba(236,72,153,0.05), transparent);
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px;
|
||||||
|
border-radius:5px; font-size:.88em; white-space:nowrap;
|
||||||
|
}
|
||||||
|
a { color:var(--accent); text-decoration:none; }
|
||||||
|
a:hover { color:var(--accent2); }
|
||||||
|
|
||||||
.sidebar { width:260px; min-width:260px; background:var(--surface); border-right:1px solid var(--border); padding:24px 0; position:sticky; top:0; height:100vh; overflow-y:auto; }
|
/* ===== 侧边栏 ===== */
|
||||||
.sidebar h2 { font-size:15px; padding:0 24px; margin-bottom:16px; color:var(--accent2); }
|
.sidebar {
|
||||||
.sidebar a { display:block; padding:7px 24px; font-size:13px; color:var(--muted); transition:.15s; border-left:2px solid transparent; }
|
width:264px; min-width:264px; background:rgba(10,15,26,.8);
|
||||||
.sidebar a:hover, .sidebar a.active { color:var(--accent2); border-left-color:var(--accent); background:rgba(99,102,241,0.06); }
|
border-right:1px solid var(--border); padding:22px 0;
|
||||||
.sidebar .section { font-size:11px; text-transform:uppercase; letter-spacing:1px; padding:16px 24px 6px; color:var(--muted); font-weight:600; }
|
position:sticky; top:0; height:100vh; overflow-y:auto;
|
||||||
|
backdrop-filter:blur(14px);
|
||||||
|
}
|
||||||
|
.sidebar::-webkit-scrollbar { width:5px; }
|
||||||
|
.sidebar::-webkit-scrollbar-thumb { background:rgba(255,255,255,.12); border-radius:3px; }
|
||||||
|
.sidebar-logo { font-size:16px; font-weight:800; padding:0 24px; margin-bottom:4px; }
|
||||||
|
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
|
||||||
|
.sidebar .back { display:block; font-size:12.5px; padding:6px 24px; color:var(--muted); }
|
||||||
|
.sidebar .back:hover { color:var(--accent2); }
|
||||||
|
.sidebar a.snav {
|
||||||
|
display:block; padding:6.5px 24px; font-size:13px; color:var(--muted);
|
||||||
|
transition:.15s; border-left:2px solid transparent;
|
||||||
|
}
|
||||||
|
.sidebar a.snav:hover { color:var(--accent2); }
|
||||||
|
.sidebar a.snav.active { color:var(--accent2); border-left-color:#a855f7; background:rgba(139,92,246,.08); }
|
||||||
|
.sidebar .section {
|
||||||
|
font-size:11px; text-transform:uppercase; letter-spacing:1.2px;
|
||||||
|
padding:18px 24px 6px; color:var(--muted); font-weight:700; opacity:.75;
|
||||||
|
}
|
||||||
|
|
||||||
.main { flex:1; padding:40px 48px; max-width:950px; }
|
/* ===== 主内容 ===== */
|
||||||
.main h1 { font-size:30px; font-weight:700; margin-bottom:8px; }
|
.main { flex:1; padding:44px 52px; max-width:980px; min-width:0; }
|
||||||
.main h2 { font-size:22px; font-weight:700; margin:44px 0 14px; padding-top:22px; border-top:1px solid var(--border); }
|
.main h1 { font-size:32px; font-weight:800; margin-bottom:6px; letter-spacing:-.5px; }
|
||||||
.main h3 { font-size:17px; font-weight:600; margin:28px 0 10px; color:var(--accent2); }
|
.main .h1-desc { color:var(--muted); font-size:14px; margin-bottom:8px; }
|
||||||
|
.main h2 {
|
||||||
|
font-size:22px; font-weight:700; margin:52px 0 12px; padding-top:22px;
|
||||||
|
border-top:1px solid var(--border); scroll-margin-top:24px;
|
||||||
|
}
|
||||||
|
.main h2::before { content:'#'; margin-right:8px; background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
||||||
|
.main h3 { font-size:16.5px; font-weight:700; margin:26px 0 10px; color:var(--accent2); }
|
||||||
.main p { margin:8px 0; color:var(--muted); font-size:14px; }
|
.main p { margin:8px 0; color:var(--muted); font-size:14px; }
|
||||||
.main table { width:100%; border-collapse:collapse; margin:14px 0 20px; font-size:13px; }
|
.main p code { font-size:.9em; }
|
||||||
|
.main table { width:100%; border-collapse:collapse; margin:14px 0 22px; font-size:13px; }
|
||||||
.main th, .main td { text-align:left; padding:9px 14px; border-bottom:1px solid var(--border); vertical-align:top; }
|
.main th, .main td { text-align:left; padding:9px 14px; border-bottom:1px solid var(--border); vertical-align:top; }
|
||||||
.main th { color:var(--muted); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }
|
.main th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.8px; }
|
||||||
.main td:first-child { color:var(--accent2); font-family:monospace; font-size:12px; white-space:nowrap; }
|
.main td:first-child { color:var(--accent); font-family:monospace; font-size:12px; white-space:nowrap; }
|
||||||
.main td:nth-child(3) { font-family:monospace; font-size:11px; color:var(--muted); }
|
.main td:nth-child(3) { font-family:monospace; font-size:11.5px; color:var(--muted); }
|
||||||
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:10px; font-weight:700; margin-left:6px; vertical-align:middle; }
|
.badge {
|
||||||
.badge-new { background:rgba(16,185,129,0.2); color:#34d399; }
|
display:inline-block; padding:1px 8px; border-radius:4px; font-size:10px;
|
||||||
.hint { font-size:12px; color:var(--muted); margin-top:2px; }
|
font-weight:700; margin-left:6px; vertical-align:middle;
|
||||||
.return-box { background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:8px; padding:14px 18px; margin:12px 0; }
|
}
|
||||||
.return-box h4 { font-size:12px; color:#34d399; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.5px; }
|
.badge-new { background:rgba(16,185,129,.18); color:#34d399; border:1px solid rgba(16,185,129,.3); }
|
||||||
|
.badge-v5 { background:rgba(168,85,247,.2); color:#d8b4fe; border:1px solid rgba(168,85,247,.4); }
|
||||||
|
.hint { font-size:12.5px; color:var(--muted); margin-top:2px; }
|
||||||
|
.return-box {
|
||||||
|
background:rgba(16,185,129,.06); border:1px solid rgba(16,185,129,.2);
|
||||||
|
border-radius:10px; padding:14px 18px; margin:14px 0;
|
||||||
|
}
|
||||||
|
.return-box h4 { font-size:11.5px; color:#34d399; margin-bottom:8px; text-transform:uppercase; letter-spacing:.8px; }
|
||||||
|
|
||||||
@media (max-width:768px) {
|
.terminal {
|
||||||
|
background:#0a0f1c; border:1px solid var(--border); border-radius:10px;
|
||||||
|
overflow:hidden; margin:12px 0 20px;
|
||||||
|
}
|
||||||
|
.terminal-bar { display:flex; align-items:center; gap:7px; padding:9px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border); }
|
||||||
|
.terminal-bar .t-dot { width:10px; height:10px; border-radius:50%; }
|
||||||
|
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
|
||||||
|
.terminal-bar .t-title { margin-left:10px; font-size:11px; color:var(--muted); }
|
||||||
|
.terminal pre { padding:16px 18px; overflow-x:auto; font:12.5px/1.75 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; }
|
||||||
|
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; } .terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
|
||||||
|
|
||||||
|
@media (max-width:860px) {
|
||||||
body { flex-direction:column; }
|
body { flex-direction:column; }
|
||||||
.sidebar { width:100%; min-width:0; height:auto; position:static; border-right:none; border-bottom:1px solid var(--border); padding:12px 16px; }
|
.sidebar { width:100%; min-width:0; height:auto; position:static; border-right:none; border-bottom:1px solid var(--border); padding:12px 16px; }
|
||||||
.sidebar a { display:inline-block; padding:4px 10px; border-left:none; }
|
.sidebar .section, .sidebar .back { padding:4px 8px; }
|
||||||
.main { padding:24px 20px; }
|
.sidebar a.snav { display:inline-block; padding:4px 10px; border-left:none; font-size:12.5px; }
|
||||||
|
.sidebar a.snav.active { border-left:none; border-radius:6px; }
|
||||||
|
.main { padding:26px 20px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<h2>📘 MetonaToast 文档</h2>
|
<div class="sidebar-logo"><span class="grad-text">MetonaToast 文档</span></div>
|
||||||
<a href="index.html">← 返回首页</a>
|
<a class="back" href="index.html">← 返回首页</a>
|
||||||
<a href="demo.html">🎮 在线演示</a>
|
<a class="back" href="demo.html">🎮 在线演示</a>
|
||||||
<div class="section">基础通知</div>
|
<div class="section">基础通知</div>
|
||||||
<a href="#show">show()</a>
|
<a class="snav" href="#show">show()</a>
|
||||||
<a href="#success">success()</a>
|
<a class="snav" href="#success">success()</a>
|
||||||
<a href="#error">error()</a>
|
<a class="snav" href="#error">error()</a>
|
||||||
<a href="#warning">warning()</a>
|
<a class="snav" href="#warning">warning()</a>
|
||||||
<a href="#info">info()</a>
|
<a class="snav" href="#info">info()</a>
|
||||||
<a href="#loading">loading()</a>
|
<a class="snav" href="#loading">loading()</a>
|
||||||
<div class="section">高级功能</div>
|
<div class="section">高级功能</div>
|
||||||
<a href="#promise">promise()</a>
|
<a class="snav" href="#promise">promise()</a>
|
||||||
<a href="#confirm">confirm()</a>
|
<a class="snav" href="#confirm">confirm()</a>
|
||||||
<a href="#prompt">prompt()</a>
|
<a class="snav" href="#prompt">prompt()</a>
|
||||||
<a href="#progress">progress()</a>
|
<a class="snav" href="#progress">progress()</a>
|
||||||
<a href="#countdown">countdown()</a>
|
<a class="snav" href="#countdown">countdown()</a>
|
||||||
<a href="#queue">queue()</a>
|
<a class="snav" href="#queue">queue()</a>
|
||||||
<a href="#stack">stack()</a>
|
<a class="snav" href="#stack">stack()</a>
|
||||||
<a href="#action">action()</a>
|
<a class="snav" href="#action">action()</a>
|
||||||
<div class="section">管理方法</div>
|
<div class="section">管理方法</div>
|
||||||
<a href="#group">group()</a>
|
<a class="snav" href="#group">group()</a>
|
||||||
<a href="#dismiss">dismiss()</a>
|
<a class="snav" href="#dismiss">dismiss()</a>
|
||||||
<a href="#clear">clear()</a>
|
<a class="snav" href="#clear">clear()</a>
|
||||||
<a href="#configure">configure()</a>
|
<a class="snav" href="#updatePosition">updatePosition()</a>
|
||||||
<a href="#use">use()</a>
|
<a class="snav" href="#remove">remove() / removeToast()</a>
|
||||||
<a href="#destroy">destroy()</a>
|
<a class="snav" href="#configure">configure()</a>
|
||||||
<div class="section">配置参考</div>
|
<a class="snav" href="#use">use()</a>
|
||||||
<a href="#config">全部配置项</a>
|
<a class="snav" href="#destroy">destroy()</a>
|
||||||
<a href="#callbacks">回调函数</a>
|
<div class="section">扩展</div>
|
||||||
<a href="#animations">动画列表</a>
|
<a class="snav" href="#hooks">钩子系统</a>
|
||||||
<a href="#themes">主题参考</a>
|
<a class="snav" href="#react">React 适配器</a>
|
||||||
|
<a class="snav" href="#config">全部配置项</a>
|
||||||
|
<a class="snav" href="#callbacks">回调函数</a>
|
||||||
|
<a class="snav" href="#animations">动画列表</a>
|
||||||
|
<a class="snav" href="#themes">主题参考</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="main">
|
<main class="main">
|
||||||
|
|
||||||
<h1>API 文档</h1>
|
<h1>API 文档</h1>
|
||||||
<p>MetonaToast v0.2.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
<p class="h1-desc">MetonaToast v0.5.0 完整 API 参考。所有基础通知方法支持字符串与对象两种调用形式,均可传入任何<a href="#config">配置项</a>作为可选参数。</p>
|
||||||
|
|
||||||
<!-- ===== 基础通知 ===== -->
|
<!-- ===== 基础通知 ===== -->
|
||||||
<h2 id="show">show(message, opts?)</h2>
|
<h2 id="show">show(message, opts?)</h2>
|
||||||
<p>显示默认类型的 Toast 通知,无特定颜色和图标。</p>
|
<p>显示默认类型 Toast,无特定颜色与图标。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">show.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串,或包含 title/message 等属性的配置对象</td></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置对象,覆盖 <a href="#config">全部配置项</a>。仅当 message 为字符串时有效</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre><span class="cm">// 字符串形式</span>
|
<pre><span class="cm">// 字符串形式</span>
|
||||||
MeToast.<span class="fn">show</span>(<span class="s">'默认消息'</span>);
|
MeToast.<span class="fn">show</span>(<span class="s">'默认消息'</span>);
|
||||||
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: 2000, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
|
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: <span class="num">2000</span>, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
|
||||||
<span class="cm">// 对象形式(所有 opts 作为一级属性)</span>
|
<span class="cm">// 对象形式</span>
|
||||||
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: 3000 });</pre>
|
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: <span class="num">3000</span> });</pre></div>
|
||||||
|
|
||||||
<h2 id="success">success(message, opts?)</h2>
|
<h2 id="success">success(message, opts?)</h2>
|
||||||
<p>显示成功通知。绿色对勾图标 <code>#10b981</code>,type = success。</p>
|
<p>绿色对勾图标 <code>#10b981</code>,type = success。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">success.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 success</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">success</span>(<span class="s">'保存成功!'</span>);
|
<pre>MeToast.<span class="fn">success</span>(<span class="s">'保存成功!'</span>);
|
||||||
MeToast.<span class="fn">success</span>({ <span class="fn">title</span>: <span class="s">'已保存'</span>, <span class="fn">message</span>: <span class="s">'数据已同步'</span> });
|
MeToast.<span class="fn">success</span>({ <span class="fn">title</span>: <span class="s">'已保存'</span>, <span class="fn">message</span>: <span class="s">'数据已同步'</span> });
|
||||||
Met.<span class="fn">success</span>(<span class="s">'MeToast和Met等价'</span>);</pre>
|
Met.<span class="fn">success</span>(<span class="s">'Met 与 MeToast 等价'</span>);</pre></div>
|
||||||
|
|
||||||
<h2 id="error">error(message, opts?)</h2>
|
<h2 id="error">error(message, opts?)</h2>
|
||||||
<p>显示错误通知。红色叉号图标 <code>#ef4444</code>,type = error。aria-live 设为 assertive。</p>
|
<p>红色叉号图标 <code>#ef4444</code>,type = error。aria-live = assertive。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">error.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 error</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">error</span>(<span class="s">'网络错误,请重试'</span>);
|
<pre>MeToast.<span class="fn">error</span>(<span class="s">'网络错误,请重试'</span>);
|
||||||
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre>
|
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre></div>
|
||||||
|
|
||||||
<h2 id="warning">warning(message, opts?)</h2>
|
<h2 id="warning">warning(message, opts?)</h2>
|
||||||
<p>显示警告通知。黄色三角图标 <code>#f59e0b</code>,type = warning。</p>
|
<p>黄色三角图标 <code>#f59e0b</code>,type = warning。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">warning.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre></div>
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 warning</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre>
|
|
||||||
|
|
||||||
<h2 id="info">info(message, opts?)</h2>
|
<h2 id="info">info(message, opts?)</h2>
|
||||||
<p>显示信息通知。蓝色圆形图标 <code>#3b82f6</code>,type = info。</p>
|
<p>蓝色圆形图标 <code>#3b82f6</code>,type = info。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">info.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre></div>
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 info</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre>
|
|
||||||
|
|
||||||
<h2 id="loading">loading(message, opts?)</h2>
|
<h2 id="loading">loading(message, opts?)</h2>
|
||||||
<p>显示加载状态。type = loading,duration 强制为 0(不自动关闭),closeButton 和 showProgress 强制为 false。返回 LoadingControl 对象。</p>
|
<p>type = loading,duration 强制 0(不自动关闭)。返回 LoadingControl。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">loading.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>加载提示文本或配置对象</td></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre><span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
<pre><span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), 2000);</pre>
|
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre></div>
|
||||||
<div class="return-box">
|
<div class="return-box">
|
||||||
<h4>返回 LoadingControl</h4>
|
<h4>LoadingControl(链式转换 id 稳定)</h4>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
|
<tr><th>方法</th><th>说明</th></tr>
|
||||||
<tr><td>success</td><td>(msg, opts?) => ToastInstance</td><td>关闭加载 toast,原地替换为 success 类型</td></tr>
|
<tr><td>success/error/info/warning</td><td>原地更新为对应类型(同一实例,id 不变),duration 自动恢复默认值</td></tr>
|
||||||
<tr><td>error</td><td>(msg, opts?) => ToastInstance</td><td>替换为 error 类型</td></tr>
|
<tr><td>update(partial)</td><td>原地更新内容,不改变类型</td></tr>
|
||||||
<tr><td>info</td><td>(msg, opts?) => ToastInstance</td><td>替换为 info 类型</td></tr>
|
<tr><td>dismiss()</td><td>直接关闭,不转换</td></tr>
|
||||||
<tr><td>warning</td><td>(msg, opts?) => ToastInstance</td><td>替换为 warning 类型</td></tr>
|
|
||||||
<tr><td>update</td><td>(partial) => MeToast</td><td>不关闭加载 toast,原地更新其内容(支持 resetTimerOnUpdate)</td></tr>
|
|
||||||
<tr><td>dismiss</td><td>()</td><td>直接关闭加载 toast 不替换</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ===== 高级功能 ===== -->
|
<!-- ===== 高级功能 ===== -->
|
||||||
<h2 id="promise">promise(promise, opts)</h2>
|
<h2 id="promise">promise(promise, opts)</h2>
|
||||||
<p>监听 Promise 生命周期。自动显示 loading → 根据 resolve/reject 自动切换 success/error。返回原 Promise,支持 await 获取结果。</p>
|
<p>监听 Promise 生命周期,自动 loading → resolve/reject 切换 success/error,返回原 Promise。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
|
||||||
<tr><td>promise</td><td>Promise</td><td>—</td><td>要监听的 Promise 对象。非 Promise 会打印错误并返回 rejected Promise</td></tr>
|
<tr><td>promise</td><td>Promise</td><td>—</td><td>要监听的 Promise。非 Promise 打印错误并返回 rejected Promise</td></tr>
|
||||||
<tr><td>opts.loading</td><td>string</td><td>"加载中..."</td><td>加载中显示的文本</td></tr>
|
<tr><td>opts.loading / success / error</td><td>string</td><td>加载中... / 操作成功 / 操作失败</td><td>各阶段文本</td></tr>
|
||||||
<tr><td>opts.success</td><td>string</td><td>"操作成功"</td><td>resolve 后显示的文本</td></tr>
|
|
||||||
<tr><td>opts.error</td><td>string</td><td>"操作失败"</td><td>reject 后显示的文本</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">promise.js</span></div>
|
||||||
<pre><span class="kw">try</span> {
|
<pre><span class="kw">try</span> {
|
||||||
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
||||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
|
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成!'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>,
|
||||||
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
|
|
||||||
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
|
|
||||||
});
|
});
|
||||||
} <span class="kw">catch</span> (<span class="fn">e</span>) { <span class="cm">/* promise reject 会继续抛出 */</span> }</pre>
|
} <span class="kw">catch</span> (e) { <span class="cm">/* reject 会继续抛出 */</span> }</pre></div>
|
||||||
|
|
||||||
<h2 id="confirm">confirm(message, opts?)</h2>
|
<h2 id="confirm">confirm(message, opts?)</h2>
|
||||||
<p>确认对话框。返回 <code>Promise<boolean></code>。内置 10 秒安全超时,超时自动 resolve(false)。</p>
|
<p>确认对话框,返回 <code>Promise<boolean></code>,内置 10 秒安全超时自动 resolve(false)。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||||
<tr><td>message</td><td>string</td><td>—</td><td>对话框消息。非字符串会打印错误并 resolve(false)</td></tr>
|
<tr><td>confirmText / confirmColor</td><td>确认 / #10b981</td><td>确认按钮文字与颜色</td></tr>
|
||||||
<tr><td>opts.confirmText</td><td>string</td><td>"确认"</td><td>确认按钮文字</td></tr>
|
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮文字与颜色</td></tr>
|
||||||
<tr><td>opts.confirmColor</td><td>string</td><td>"#10b981"</td><td>确认按钮背景色</td></tr>
|
<tr><td>type</td><td>warning</td><td>Toast 类型</td></tr>
|
||||||
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
|
|
||||||
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
|
|
||||||
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型(影响图标和颜色)</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, {
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">confirm.js</span></div>
|
||||||
<span class="fn">confirmText</span>: <span class="s">'删除'</span>,
|
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span>, <span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span> });
|
||||||
<span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span>,
|
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre></div>
|
||||||
<span class="fn">cancelText</span>: <span class="s">'保留'</span>,
|
|
||||||
});
|
|
||||||
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre>
|
|
||||||
|
|
||||||
<h2 id="prompt">prompt(message, opts?)</h2>
|
<h2 id="prompt">prompt(message, opts?)</h2>
|
||||||
<p>输入对话框。返回 <code>Promise<string|null></code>。按 Enter 或点击提交按钮返回输入值,取消返回 null。内置 10 秒安全超时。</p>
|
<p>输入对话框,返回 <code>Promise<string|null></code>。Enter / 提交返回输入值,取消返回 null,10 秒超时。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||||
<tr><td>message</td><td>string</td><td>—</td><td>对话框消息。非字符串会打印错误并 resolve(null)</td></tr>
|
<tr><td>placeholder / defaultValue</td><td>""</td><td>输入框占位与默认值</td></tr>
|
||||||
<tr><td>opts.placeholder</td><td>string</td><td>""</td><td>输入框占位文字</td></tr>
|
<tr><td>inputType</td><td>text</td><td>input 的 type(password/email/number 等)</td></tr>
|
||||||
<tr><td>opts.defaultValue</td><td>string</td><td>""</td><td>输入框默认值</td></tr>
|
<tr><td>submitText / submitColor</td><td>确认 / #3b82f6</td><td>提交按钮</td></tr>
|
||||||
<tr><td>opts.inputType</td><td>string</td><td>"text"</td><td>input 标签 type 属性(如 password/email/number)</td></tr>
|
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮</td></tr>
|
||||||
<tr><td>opts.submitText</td><td>string</td><td>"确认"</td><td>提交按钮文字</td></tr>
|
|
||||||
<tr><td>opts.submitColor</td><td>string</td><td>"#3b82f6"</td><td>提交按钮背景色</td></tr>
|
|
||||||
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
|
|
||||||
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
|
|
||||||
<tr><td>opts.type</td><td>string</td><td>"info"</td><td>Toast 类型</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, {
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">prompt.js</span></div>
|
||||||
<span class="fn">placeholder</span>: <span class="s">'请输入...'</span>,
|
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, { <span class="fn">placeholder</span>: <span class="s">'请输入...'</span> });
|
||||||
<span class="fn">defaultValue</span>: <span class="s">'张三'</span>,
|
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre></div>
|
||||||
<span class="fn">submitText</span>: <span class="s">'确定'</span>,
|
|
||||||
});
|
|
||||||
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre>
|
|
||||||
|
|
||||||
<h2 id="progress">progress(message, opts?)</h2>
|
<h2 id="progress">progress(message, opts?)</h2>
|
||||||
<p>进度条通知。不自动关闭。返回 ProgressControl 对象以手动更新进度。</p>
|
<p>进度条通知,不自动关闭。返回 ProgressControl。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>方法</th><th>说明</th></tr>
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>进度提示文本或配置对象</td></tr>
|
<tr><td>setProgress(percent)</td><td>设置进度 0~100,自动 clamp</td></tr>
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。type 默认 info</td></tr>
|
<tr><td>complete(message?)</td><td>跳 100%,300ms 后转 success,1s 后自动关闭</td></tr>
|
||||||
<tr><td>opts.progressColor</td><td>string</td><td>"#3b82f6"</td><td>进度条填充颜色</td></tr>
|
<tr><td>error(message?)</td><td>转 error,2s 后自动关闭</td></tr>
|
||||||
|
<tr><td>dismiss()</td><td>直接关闭</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">progress.js</span></div>
|
||||||
<pre><span class="kw">const</span> p = MeToast.<span class="fn">progress</span>(<span class="s">'上传中...'</span>, { <span class="fn">progressColor</span>: <span class="s">'#10b981'</span> });
|
<pre><span class="kw">const</span> p = MeToast.<span class="fn">progress</span>(<span class="s">'上传中...'</span>, { <span class="fn">progressColor</span>: <span class="s">'#10b981'</span> });
|
||||||
p.<span class="fn">setProgress</span>(45); <span class="cm">// → 45%</span>
|
p.<span class="fn">setProgress</span>(<span class="num">45</span>);
|
||||||
p.<span class="fn">setProgress</span>(90); <span class="cm">// → 90%</span>
|
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>);</pre></div>
|
||||||
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>); <span class="cm">// → 100% → success</span>
|
|
||||||
<span class="cm">// 或</span>
|
|
||||||
p.<span class="fn">error</span>(<span class="s">'上传失败'</span>);
|
|
||||||
p.<span class="fn">dismiss</span>();</pre>
|
|
||||||
<div class="return-box">
|
|
||||||
<h4>返回 ProgressControl</h4>
|
|
||||||
<table>
|
|
||||||
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
|
|
||||||
<tr><td>setProgress</td><td>(percent: number)</td><td>设置进度 0~100,自动 clamp。更新进度条宽度和百分比文字</td></tr>
|
|
||||||
<tr><td>complete</td><td>(message?: string)</td><td>跳到 100%,300ms 后替换为 success toast,1s 后自动关闭</td></tr>
|
|
||||||
<tr><td>error</td><td>(message?: string)</td><td>替换为 error toast,2s 后自动关闭</td></tr>
|
|
||||||
<tr><td>dismiss</td><td>()</td><td>直接关闭</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 id="countdown">countdown(message, seconds, opts?)</h2>
|
<h2 id="countdown">countdown(message, seconds, opts?)</h2>
|
||||||
<p>倒计时 Toast。<code>{seconds}</code> 占位符每秒自动替换为剩余秒数。</p>
|
<p>倒计时 Toast,<code>{seconds}</code> 占位符每秒自动替换。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||||
<tr><td>message</td><td>string</td><td>—</td><td>消息文本。支持 <code>{seconds}</code> 占位符。非字符串打印错误并返回空控制对象</td></tr>
|
<tr><td>seconds</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
|
||||||
<tr><td>seconds</td><td>number</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
|
<tr><td>opts.onComplete</td><td>—</td><td>归零回调</td></tr>
|
||||||
<tr><td>opts.onComplete</td><td>function</td><td>—</td><td>倒计时归零时的回调</td></tr>
|
|
||||||
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, 5, {
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">countdown.js</span></div>
|
||||||
<span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>),
|
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, <span class="num">5</span>, { <span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>) });
|
||||||
});</pre>
|
<span class="cm">// 返回 { cancel(), pause(), resume() }</span></pre></div>
|
||||||
<div class="return-box">
|
|
||||||
<h4>返回 CountdownControl</h4>
|
|
||||||
<table>
|
|
||||||
<tr><th>方法</th><th>说明</th></tr>
|
|
||||||
<tr><td>cancel()</td><td>清除计时器并关闭 toast</td></tr>
|
|
||||||
<tr><td>pause()</td><td>暂停倒计时</td></tr>
|
|
||||||
<tr><td>resume()</td><td>恢复倒计时</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 id="queue">queue(messages, opts?)</h2>
|
<h2 id="queue">queue(messages, opts?)</h2>
|
||||||
<p>顺序逐个显示消息队列。前一条关闭后延时显示下一条。返回 QueueControl(thenable + cancel)。</p>
|
<p>顺序逐个显示消息队列,返回 thenable + cancel。支持 await。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||||
<tr><td>messages</td><td>Array<string|object></td><td>—</td><td>消息数组。可为字符串或带 message/type/duration/onClose 的对象。非数组打印错误并返回空控制对象</td></tr>
|
<tr><td>opts.delay</td><td>1000</td><td>每条关闭后到下一条的间隔(ms)</td></tr>
|
||||||
<tr><td>opts.delay</td><td>number</td><td>1000</td><td>每条消息关闭后到显示下一条的间隔(ms)</td></tr>
|
<tr><td>opts.duration</td><td>3000</td><td>每条显示时长,可被消息级 duration 覆盖</td></tr>
|
||||||
<tr><td>opts.duration</td><td>number</td><td>3000</td><td>每条消息显示时长(ms),可被消息级 duration 覆盖</td></tr>
|
|
||||||
<tr><td>opts.onClose</td><td>function</td><td>—</td><td>全部队列完成后的回调。消息级 onClose 和队列级 onClose 都会依次调用</td></tr>
|
|
||||||
<tr><td>opts.type</td><td>string</td><td>—</td><td>默认类型</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">queue.js</span></div>
|
||||||
<span class="s">'步骤一'</span>,
|
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([<span class="s">'步骤一'</span>, <span class="s">'步骤二'</span>, <span class="s">'步骤三'</span>], { <span class="fn">delay</span>: <span class="num">800</span>, <span class="fn">duration</span>: <span class="num">2000</span> });
|
||||||
{ <span class="fn">message</span>: <span class="s">'步骤二'</span>, <span class="fn">duration</span>: 5000, <span class="fn">type</span>: <span class="s">'warning'</span> },
|
|
||||||
<span class="s">'步骤三'</span>,
|
|
||||||
], { <span class="fn">delay</span>: 800, <span class="fn">duration</span>: 2000, <span class="fn">type</span>: <span class="s">'info'</span> });
|
|
||||||
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
||||||
<span class="kw">await</span> q; <span class="cm">// 等待完成(thenable 支持 await)</span></pre>
|
<span class="kw">await</span> q; <span class="cm">// 等待完成</span></pre></div>
|
||||||
<div class="return-box">
|
|
||||||
<h4>返回 QueueControl (thenable)</h4>
|
|
||||||
<table>
|
|
||||||
<tr><th>方法</th><th>说明</th></tr>
|
|
||||||
<tr><td>.then(fn, rj)</td><td>Promise.then 代理,支持 await</td></tr>
|
|
||||||
<tr><td>.catch(rj)</td><td>Promise.catch 代理</td></tr>
|
|
||||||
<tr><td>.cancel()</td><td>设置取消标志,不再显示下一条消息</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 id="stack">stack(messages, opts?)</h2>
|
<h2 id="stack">stack(messages, opts?)</h2>
|
||||||
<p>同时错峰显示多条消息。每条间隔 stagger 毫秒依次出现,全部叠加在屏幕上。</p>
|
<p>同时错峰显示多条消息。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">stack.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], { <span class="fn">stagger</span>: <span class="num">150</span> });</pre></div>
|
||||||
<tr><td>messages</td><td>Array<string|object></td><td>—</td><td>消息数组。可为字符串或带 message/type/duration 的对象。非数组打印错误</td></tr>
|
|
||||||
<tr><td>opts.stagger</td><td>number</td><td>100</td><td>每条消息之间的显示间隔(ms)</td></tr>
|
|
||||||
<tr><td>opts.type</td><td>string</td><td>—</td><td>默认类型</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], {
|
|
||||||
<span class="fn">stagger</span>: 150,
|
|
||||||
<span class="fn">type</span>: <span class="s">'info'</span>,
|
|
||||||
});</pre>
|
|
||||||
|
|
||||||
<h2 id="action">action(message, actions, opts?)</h2>
|
<h2 id="action">action(message, actions, opts?)</h2>
|
||||||
<p>Action Toast:内嵌操作按钮。默认 duration=0 不自动关闭,closeButton=true。</p>
|
<p>内嵌操作按钮。默认 duration=0 不自动关闭。点击按钮不会误触 closeOnClick(已隔离冒泡)。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>ActionButton</th><th>默认</th><th>说明</th></tr>
|
||||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
<tr><td>text</td><td>—(必填)</td><td>按钮文字</td></tr>
|
||||||
<tr><td>actions</td><td>ActionButton[]</td><td>[]</td><td>按钮数组。每个按钮可配 text/onClick/color/style/close</td></tr>
|
<tr><td>onClick</td><td>—(必填)</td><td>点击回调,参数为当前 toast</td></tr>
|
||||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。duration 默认 0,closeButton 默认 true</td></tr>
|
<tr><td>color / style</td><td>#6366f1</td><td>背景色 / 内联样式(style.background 优先)</td></tr>
|
||||||
</table>
|
<tr><td>close</td><td>true</td><td>点击后是否关闭。false 可多次点击</td></tr>
|
||||||
<table>
|
|
||||||
<tr><th>ActionButton 字段</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
|
||||||
<tr><td>text</td><td>string</td><td>—(必填)</td><td>按钮显示文字</td></tr>
|
|
||||||
<tr><td>onClick</td><td>(toast) => void</td><td>—(必填)</td><td>点击回调函数,参数为当前 toast 实例</td></tr>
|
|
||||||
<tr><td>color</td><td>string</td><td>"#6366f1"</td><td>按钮背景色</td></tr>
|
|
||||||
<tr><td>style</td><td>object</td><td>{}</td><td>按钮内联样式,可覆盖 color(style.background 优先)</td></tr>
|
|
||||||
<tr><td>close</td><td>boolean</td><td>true</td><td>点击后是否自动关闭 toast。设为 false 可多次点击</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">action.js</span></div>
|
||||||
<pre>MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
<pre>MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
||||||
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>(), <span class="fn">color</span>: <span class="s">'#3b82f6'</span> },
|
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>(), <span class="fn">color</span>: <span class="s">'#3b82f6'</span> },
|
||||||
{ <span class="fn">text</span>: <span class="s">'查看详情'</span>, <span class="fn">onClick</span>: (t) => <span class="fn">openFile</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: false },
|
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: <span class="kw">false</span> },
|
||||||
]);</pre>
|
]);</pre></div>
|
||||||
|
|
||||||
<!-- ===== 管理方法 ===== -->
|
<!-- ===== 管理方法 ===== -->
|
||||||
<h2 id="group">group(name)</h2>
|
<h2 id="group">group(name)</h2>
|
||||||
<p>创建 Toast 分组,返回 GroupAPI 对象。该对象所有方法自动传入 <code>group: name</code>,按组管理。</p>
|
<p>创建分组,返回 GroupAPI。所有方法自动注入 <code>group: name</code>。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">group.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
|
||||||
<tr><td>name</td><td>string</td><td>分组名称。GroupAPI 和 dismissGroup 通过此名称关联</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre><span class="kw">const</span> orders = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
<pre><span class="kw">const</span> orders = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
||||||
orders.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
orders.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
||||||
orders.<span class="fn">error</span>(<span class="s">'支付失败'</span>, { <span class="fn">duration</span>: 5000 });
|
orders.<span class="fn">count</span>(); <span class="cm">// 该组数量</span>
|
||||||
orders.<span class="fn">count</span>(); <span class="cm">// 该组当前 toast 数量</span>
|
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭整组</span>
|
||||||
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭该组全部 toast</span>
|
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre></div>
|
||||||
<span class="cm">// 也支持主对象关闭</span>
|
|
||||||
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre>
|
|
||||||
<div class="return-box">
|
|
||||||
<h4>返回 GroupAPI</h4>
|
|
||||||
<table>
|
|
||||||
<tr><th>方法</th><th>说明</th></tr>
|
|
||||||
<tr><td>show(msg, opts?)</td><td>等价 MeToast.show,自动注入 group</td></tr>
|
|
||||||
<tr><td>success(msg, opts?)</td><td>等价 MeToast.success</td></tr>
|
|
||||||
<tr><td>error(msg, opts?)</td><td>等价 MeToast.error</td></tr>
|
|
||||||
<tr><td>warning(msg, opts?)</td><td>等价 MeToast.warning</td></tr>
|
|
||||||
<tr><td>info(msg, opts?)</td><td>等价 MeToast.info</td></tr>
|
|
||||||
<tr><td>loading(msg, opts?)</td><td>等价 MeToast.loading</td></tr>
|
|
||||||
<tr><td>action(msg, actions, opts?)</td><td>等价 MeToast.action</td></tr>
|
|
||||||
<tr><td>dismiss()</td><td>关闭该组所有 toast</td></tr>
|
|
||||||
<tr><td>count()</td><td>返回该组当前 toast 数量</td></tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 id="dismiss">dismiss(id?)</h2>
|
<h2 id="dismiss">dismiss(id?)</h2>
|
||||||
<p>关闭 Toast。无参数则关闭全部。</p>
|
<p>关闭 Toast。无参数关闭全部,传入 id 关闭指定。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">dismiss.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 全部</span>
|
||||||
<tr><td>id</td><td>string</td><td>可选。Toast 的 id,不传则关闭所有</td></tr>
|
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 指定</span></pre></div>
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 关闭所有</span>
|
|
||||||
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 关闭指定</span></pre>
|
|
||||||
|
|
||||||
<h2 id="clear">clear(position?)</h2>
|
<h2 id="clear">clear(position?)</h2>
|
||||||
<p>按位置清除 Toast。不传参数清除所有位置。</p>
|
<p>按位置清除。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">clear.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
|
||||||
<tr><td>position</td><td>string</td><td>可选。位置如 'top-right',不传则清除全部</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
|
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
|
||||||
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre>
|
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre></div>
|
||||||
|
|
||||||
|
<h2 id="updatePosition">updatePosition(position) <span class="badge badge-v5">v0.3</span></h2>
|
||||||
|
<p>运行时将 Toast 移动到新位置容器,立即生效。</p>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">position.js</span></div>
|
||||||
|
<pre><span class="kw">const</span> t = MeToast.<span class="fn">info</span>(<span class="s">'可移动的 Toast'</span>);
|
||||||
|
t.<span class="fn">updatePosition</span>(<span class="s">'bottom-left'</span>);</pre></div>
|
||||||
|
|
||||||
|
<h2 id="remove">remove() / removeToast(id) <span class="badge badge-new">立即移除</span></h2>
|
||||||
|
<p>立即从 DOM 和内存移除,<b>不触发离场动画</b>。适用于无动画快速清除。</p>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">remove.js</span></div>
|
||||||
|
<pre>t.<span class="fn">remove</span>(); <span class="cm">// 实例方法</span>
|
||||||
|
MeToast.<span class="fn">removeToast</span>(t.id); <span class="cm">// 按 id(等价)</span></pre></div>
|
||||||
|
|
||||||
<h2 id="configure">configure(opts)</h2>
|
<h2 id="configure">configure(opts)</h2>
|
||||||
<p>全局配置,影响后续所有 Toast。theme 和 locale 变化会触发相应副作用(应用主题 CSS / 切换语言)。</p>
|
<p>全局配置,影响后续所有 Toast。theme/locale 变化触发副作用。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">configure.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
|
||||||
<tr><td>opts</td><td>object</td><td>包含任意 <a href="#config">配置项</a> 的对象</td></tr>
|
|
||||||
</table>
|
|
||||||
<pre>MeToast.<span class="fn">configure</span>({
|
<pre>MeToast.<span class="fn">configure</span>({
|
||||||
<span class="fn">position</span>: <span class="s">'top-right'</span>,
|
<span class="fn">position</span>: <span class="s">'top-right'</span>, <span class="fn">duration</span>: <span class="num">4000</span>, <span class="fn">theme</span>: <span class="s">'dark'</span>,
|
||||||
<span class="fn">duration</span>: 4000,
|
<span class="fn">animation</span>: <span class="s">'slide'</span>, <span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
|
||||||
<span class="fn">theme</span>: <span class="s">'dark'</span>,
|
});</pre></div>
|
||||||
<span class="fn">animation</span>: <span class="s">'slide'</span>,
|
|
||||||
<span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
|
|
||||||
});</pre>
|
|
||||||
|
|
||||||
<h2 id="use">use(plugin)</h2>
|
<h2 id="use">use(plugin)</h2>
|
||||||
<p>安装插件。支持字符串(内置预设名)或插件对象。已被拒绝注册的无效插件会打印错误。</p>
|
<p>安装插件:字符串预设名或插件对象。</p>
|
||||||
<table>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">use.js</span></div>
|
||||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有</span>
|
||||||
<tr><td>plugin</td><td>string | object</td><td>预设名 'keyboard'/'persistence'/'accessibility' 或自定义插件对象 { name, version?, install, uninstall? }</td></tr>
|
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置持久化 localStorage</span>
|
||||||
</table>
|
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器朗读</span>
|
||||||
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有 Toast</span>
|
MeToast.<span class="fn">use</span>(<span class="s">'dedupe'</span>); <span class="cm">// 相同 type+message 自动去重</span></pre></div>
|
||||||
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置自动保存到 localStorage</span>
|
<p class="hint">dedupe:检测已有相同 toast,更新它并阻止重复弹出(beforeShow 钩子实现,支持 uninstall 卸载钩子)。</p>
|
||||||
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器实时朗读 toast 内容</span></pre>
|
|
||||||
<p class="hint">内置插件详情:keyboard 监听 keydown Escape 键;persistence 将配置写入 localStorage 并在页面加载时恢复;accessibility 在 afterShow/afterUpdate 钩子中通过 aria-live 区域朗读 toast 内容。</p>
|
|
||||||
|
|
||||||
<h2 id="destroy">destroy()</h2>
|
<h2 id="destroy">destroy()</h2>
|
||||||
<p>完全销毁。关闭所有 Toast、移除所有 DOM 容器和注入的样式标签、清空内存缓存。</p>
|
<p>完全销毁:关闭所有 Toast、移除全部 DOM 容器与注入样式、清空内存缓存与钩子。支持重复调用,<code>init()</code> 可恢复。</p>
|
||||||
<pre>MeToast.<span class="fn">destroy</span>();</pre>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">destroy.js</span></div>
|
||||||
|
<pre>MeToast.<span class="fn">destroy</span>();
|
||||||
|
MeToast.<span class="fn">init</span>({ <span class="fn">config</span>: { <span class="fn">duration</span>: <span class="num">3000</span> } }); <span class="cm">// 恢复</span></pre></div>
|
||||||
|
|
||||||
|
<!-- ===== 钩子系统 ===== -->
|
||||||
|
<h2 id="hooks">钩子系统 <span class="badge badge-v5">v0.3</span></h2>
|
||||||
|
<p><code>Toast.on(name, handler)</code> 注册钩子并返回取消函数。<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 的 handler 返回 <code>false</code> 可拦截对应操作。</p>
|
||||||
|
<table>
|
||||||
|
<tr><th>钩子</th><th>触发时机</th><th>拦截</th></tr>
|
||||||
|
<tr><td>beforeInit / afterInit</td><td>init() 前后</td><td>—</td></tr>
|
||||||
|
<tr><td>beforeDestroy / afterDestroy</td><td>destroy() 前后</td><td>—</td></tr>
|
||||||
|
<tr><td>beforeShow / afterShow</td><td>显示前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||||
|
<tr><td>beforeClose / afterClose</td><td>关闭前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||||
|
<tr><td>beforeUpdate / afterUpdate</td><td>update() 前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||||
|
<tr><td>configChange</td><td>configure / updateConfig / resetConfig</td><td>—</td></tr>
|
||||||
|
<tr><td>themeChange / localeChange</td><td>主题 / 语言切换</td><td>—</td></tr>
|
||||||
|
<tr><td>click / hover</td><td>点击 / 悬停进出</td><td>—</td></tr>
|
||||||
|
<tr><td>dragStart / dragEnd</td><td>拖拽开始 / 结束</td><td>—</td></tr>
|
||||||
|
<tr><td>animationStart / animationEnd</td><td>入场动画开始 / 结束</td><td>—</td></tr>
|
||||||
|
<tr><td>progressStart / progressEnd</td><td>倒计时开始 / 归零</td><td>—</td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">hooks.js</span></div>
|
||||||
|
<pre><span class="kw">import</span> MeToast, { <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast'</span>;
|
||||||
|
|
||||||
|
<span class="cm">// 拦截:非允许时段禁止错误提示</span>
|
||||||
|
<span class="kw">const</span> off = <span class="fn">Toast</span>.<span class="fn">on</span>(<span class="s">'beforeShow'</span>, (toast) => {
|
||||||
|
<span class="kw">if</span> (toast.type === <span class="s">'error'</span> && !isAllowed) <span class="kw">return</span> <span class="kw">false</span>;
|
||||||
|
});
|
||||||
|
<span class="cm">// ...</span>
|
||||||
|
off(); <span class="cm">// 取消注册</span></pre></div>
|
||||||
|
|
||||||
|
<!-- ===== React ===== -->
|
||||||
|
<h2 id="react">React 适配器 <span class="badge badge-v5">v0.4</span></h2>
|
||||||
|
<p>主包保持零依赖。通过子路径 <code>@metona-team/metona-toast/react</code> 导入,<code>react</code> 为 optional peerDependency。</p>
|
||||||
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">react.tsx</span></div>
|
||||||
|
<pre><span class="kw">import</span> { <span class="fn">useToast</span>, <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast/react'</span>;
|
||||||
|
|
||||||
|
<span class="kw">function</span> <span class="fn">SubmitButton</span>() {
|
||||||
|
<span class="kw">const</span> toast = <span class="fn">useToast</span>(); <span class="cm">// 组件卸载自动清理本组件创建的 Toast</span>
|
||||||
|
<span class="kw">const</span> submit = <span class="kw">async</span> () => {
|
||||||
|
<span class="kw">const</span> loading = toast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||||
|
<span class="kw">try</span> { <span class="kw">await</span> api(); loading.<span class="fn">success</span>(<span class="s">'完成!'</span>); }
|
||||||
|
<span class="kw">catch</span> (e) { loading.<span class="fn">error</span>(<span class="s">'失败'</span>); }
|
||||||
|
};
|
||||||
|
<span class="kw">return</span> <button onClick={submit}>提交</button>;
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cm">// 声明式 Toast:props 变化更新,卸载自动移除(autoClose 默认 true)</span>
|
||||||
|
<span class="kw">function</span> <span class="fn">SaveIndicator</span>({ saving }) {
|
||||||
|
<span class="kw">return</span> saving ? <<span class="fn">Toast</span> type=<span class="s">"info"</span> message=<span class="s">"正在保存..."</span> /> : null;
|
||||||
|
}</pre></div>
|
||||||
|
|
||||||
<!-- ===== 配置参考 ===== -->
|
<!-- ===== 配置参考 ===== -->
|
||||||
<h2 id="config">全部配置项</h2>
|
<h2 id="config">全部配置项</h2>
|
||||||
<p>以下配置可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts 参数。</p>
|
<p>可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||||
<tr><td>position</td><td>string</td><td>'top-right'</td><td>位置:top-left / top-center / top-right / bottom-left / bottom-center / bottom-right</td></tr>
|
<tr><td>position</td><td>string</td><td>'top-right'</td><td>6 个位置之一(RTL 自动翻转)</td></tr>
|
||||||
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms),0=不自动关闭</td></tr>
|
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms),0 = 不自动关闭</td></tr>
|
||||||
<tr><td>max</td><td>number</td><td>6</td><td>同一位置最多同时显示条数,超出则关闭最早的</td></tr>
|
<tr><td>max</td><td>number</td><td>6</td><td>同位置最多条数,超出真正关闭最早的</td></tr>
|
||||||
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 之间的间距(px)</td></tr>
|
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 间距(px)</td></tr>
|
||||||
<tr><td>offset</td><td>number</td><td>24</td><td>容器到屏幕边缘的距离(px)</td></tr>
|
<tr><td>offset</td><td>number</td><td>24</td><td>容器距屏幕边缘(px)</td></tr>
|
||||||
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>鼠标悬停时暂停 duration 倒计时和进度条</td></tr>
|
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>悬停暂停倒计时</td></tr>
|
||||||
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击 Toast 任意位置关闭。关闭按钮始终触发关闭</td></tr>
|
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击关闭</td></tr>
|
||||||
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭。拖拽超过 120px 触发关闭</td></tr>
|
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭</td></tr>
|
||||||
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示 duration 倒计时进度条</td></tr>
|
<tr><td>dragThreshold</td><td>number</td><td>120</td><td>拖拽关闭阈值(px)</td></tr>
|
||||||
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>进度条方向:horizontal(底部水平) / vertical(右侧垂直)</td></tr>
|
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示倒计时进度条</td></tr>
|
||||||
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型对应图标(success→对勾等)</td></tr>
|
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>horizontal / vertical</td></tr>
|
||||||
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示右上角关闭 × 按钮</td></tr>
|
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型图标</td></tr>
|
||||||
<tr><td>theme</td><td>string</td><td>'auto'</td><td>主题:light / dark / auto(跟随系统)/ warm / 自定义注册名</td></tr>
|
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示关闭 × 按钮</td></tr>
|
||||||
<tr><td>animation</td><td>string</td><td>'slide'</td><td>入场动画名称,支持 CSS 动画列表见下</td></tr>
|
<tr><td>theme</td><td>string</td><td>'auto'</td><td>light / dark / auto / warm / 自定义名</td></tr>
|
||||||
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 CSS z-index</td></tr>
|
<tr><td>animation</td><td>string</td><td>'slide'</td><td>11 种内置或自定义动画名</td></tr>
|
||||||
<tr><td>width</td><td>number|string</td><td>360</td><td>Toast 宽度。数字表示 px</td></tr>
|
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 z-index</td></tr>
|
||||||
<tr><td>className</td><td>string</td><td>''</td><td>附加到 Toast 元素上的 CSS 类名</td></tr>
|
<tr><td>width</td><td>number|string</td><td>360</td><td>宽度,数字表示 px</td></tr>
|
||||||
<tr><td>style</td><td>object</td><td>{}</td><td>附加到 Toast 元素上的内联样式对象</td></tr>
|
<tr><td>className</td><td>string</td><td>''</td><td>附加 CSS 类名</td></tr>
|
||||||
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码(zh-CN / en-US 等)</td></tr>
|
<tr><td>style</td><td>object</td><td>{}</td><td>附加内联样式</td></tr>
|
||||||
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</td></tr>
|
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码</td></tr>
|
||||||
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时自动通过 Notification API 发送系统通知</td></tr>
|
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>update() 时重置倒计时</td></tr>
|
||||||
<tr><td>render</td><td>function</td><td>—</td><td>自定义渲染函数 <code>(toast) => htmlString</code>,完全接管 DOM 构建</td></tr>
|
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时发系统通知</td></tr>
|
||||||
<tr><td>onError</td><td>function</td><td>—</td><td>全局错误回调 <code>({ hook, source, error, toast }) => void</code>,钩子异常或定时器错误时触发</td></tr>
|
<tr><td>render</td><td>function</td><td>—</td><td>自定义渲染函数,完全接管 DOM</td></tr>
|
||||||
|
<tr><td>onBeforeShow</td><td>function</td><td>—</td><td>返回 false 阻止显示</td></tr>
|
||||||
|
<tr><td>onError</td><td>function</td><td>—</td><td>钩子/定时器异常全局回调</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 id="callbacks">回调函数</h2>
|
<h2 id="callbacks">回调函数</h2>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>回调</th><th>签名</th><th>触发时机</th></tr>
|
<tr><th>回调</th><th>签名</th><th>触发时机</th></tr>
|
||||||
<tr><td>onShow</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 创建并播放入场动画后</td></tr>
|
<tr><td>onBeforeShow</td><td>(toast) => boolean | void</td><td>DOM 创建前,返回 false 阻止显示</td></tr>
|
||||||
<tr><td>onClose</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 被移除后(离场动画完成时)</td></tr>
|
<tr><td>onShow</td><td>(toast) => void</td><td>创建并播放入场动画后</td></tr>
|
||||||
<tr><td>onClick</td><td>(toast: ToastInstance) => void</td><td>Toast 被点击时(closeOnClick 为 true 时还会自动关闭)</td></tr>
|
<tr><td>onClose</td><td>(toast) => void</td><td>DOM 移除后(离场完成)</td></tr>
|
||||||
<tr><td>onUpdate</td><td>(toast: ToastInstance) => void</td><td>Toast 内容通过 update() 更新后</td></tr>
|
<tr><td>onClick</td><td>(toast) => void</td><td>点击时(closeOnClick=true 时还会关闭)</td></tr>
|
||||||
<tr><td>onError</td><td>({ hook, source, error, toast }) => void</td><td>钩子回调或定时器发生异常时(全局配置,适用于接入错误监控)</td></tr>
|
<tr><td>onUpdate</td><td>(toast) => void</td><td>update() 后</td></tr>
|
||||||
|
<tr><td>onError</td><td>({ hook, source, error, toast }) => void</td><td>钩子/定时器异常时</td></tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 id="animations">动画列表</h2>
|
<h2 id="animations">动画列表</h2>
|
||||||
<p>配置 <code>animation</code> 的值,支持以下 CSS 动画。未在此列表中的值将 fallback 到 slide。</p>
|
<p>未注册的动画名自动 fallback 到 slide。</p>
|
||||||
<table>
|
<table>
|
||||||
<tr><th>名称</th><th>效果</th><th>时长</th></tr>
|
<tr><th>名称</th><th>效果</th><th>时长</th></tr>
|
||||||
<tr><td>slide</td><td>从右侧滑入 + 回弹</td><td>400ms</td></tr>
|
<tr><td>slide</td><td>从右侧滑入 + 回弹</td><td>400ms</td></tr>
|
||||||
@@ -458,20 +452,13 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
|
|||||||
<tr><td>flip</td><td>3D 翻转入场 + 回摆</td><td>500ms</td></tr>
|
<tr><td>flip</td><td>3D 翻转入场 + 回摆</td><td>500ms</td></tr>
|
||||||
<tr><td>rotate</td><td>旋转摇摆进入</td><td>500ms</td></tr>
|
<tr><td>rotate</td><td>旋转摇摆进入</td><td>500ms</td></tr>
|
||||||
<tr><td>zoom</td><td>从中心爆发式弹出</td><td>500ms</td></tr>
|
<tr><td>zoom</td><td>从中心爆发式弹出</td><td>500ms</td></tr>
|
||||||
<tr><td>slideUp</td><td>从下方弹入</td><td>400ms</td></tr>
|
<tr><td>slideUp / slideDown</td><td>从下方 / 上方弹入</td><td>400ms</td></tr>
|
||||||
<tr><td>slideDown</td><td>从上方弹入</td><td>400ms</td></tr>
|
<tr><td>slideLeft / slideRight</td><td>从左侧 / 右侧滑入</td><td>400ms</td></tr>
|
||||||
<tr><td>slideLeft</td><td>从左侧滑入</td><td>400ms</td></tr>
|
|
||||||
<tr><td>slideRight</td><td>从右侧滑入</td><td>400ms</td></tr>
|
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<h2 id="themes">主题参考</h2>
|
<h2 id="themes">主题参考</h2>
|
||||||
<pre><span class="cm">// 内置主题</span>
|
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">theme.js</span></div>
|
||||||
light <span class="cm">— 白色玻璃质感</span>
|
<pre><span class="cm">// 内置主题:light · dark · auto(跟随系统)· warm</span>
|
||||||
dark <span class="cm">— 深色玻璃质感</span>
|
|
||||||
auto <span class="cm">— 跟随系统主题设置</span>
|
|
||||||
warm <span class="cm">— 暖色调</span>
|
|
||||||
|
|
||||||
<span class="cm">// 注册自定义主题</span>
|
|
||||||
MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</span>, {
|
MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</span>, {
|
||||||
<span class="fn">bg</span>: <span class="s">'rgba(240,249,255,0.96)'</span>,
|
<span class="fn">bg</span>: <span class="s">'rgba(240,249,255,0.96)'</span>,
|
||||||
<span class="fn">text</span>: <span class="s">'#0c4a6e'</span>,
|
<span class="fn">text</span>: <span class="s">'#0c4a6e'</span>,
|
||||||
@@ -481,19 +468,38 @@ MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</sp
|
|||||||
<span class="fn">progressBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
<span class="fn">progressBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
||||||
<span class="fn">closeHoverBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
<span class="fn">closeHoverBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
||||||
});
|
});
|
||||||
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre>
|
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre></div>
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<script src="../dist/metona-toast.js"></script>
|
<script src="../dist/metona-toast.js"></script>
|
||||||
<script>
|
<script>
|
||||||
MeToast.configure({ position: 'top-right', duration: 4000, theme: 'dark', animation: 'slide' });
|
MeToast.configure({ position: 'top-right', duration: 4000, theme: 'dark', animation: 'slide' });
|
||||||
document.querySelectorAll('.sidebar a[href^="#"]').forEach(a => {
|
|
||||||
a.addEventListener('click', () => {
|
// 侧边栏滚动高亮
|
||||||
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
const links = document.querySelectorAll('.sidebar a.snav');
|
||||||
a.classList.add('active');
|
const sections = [];
|
||||||
|
links.forEach(l => {
|
||||||
|
const id = l.getAttribute('href').slice(1);
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) sections.push({ id, el, link: l });
|
||||||
});
|
});
|
||||||
|
const spy = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(e => {
|
||||||
|
if (e.isIntersecting) {
|
||||||
|
links.forEach(l => l.classList.remove('active'));
|
||||||
|
const hit = sections.find(s => s.el === e.target);
|
||||||
|
if (hit) hit.link.classList.add('active');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
}, { rootMargin: '-15% 0px -70% 0px' });
|
||||||
|
sections.forEach(s => spy.observe(s.el));
|
||||||
|
|
||||||
|
// 点击关闭高亮
|
||||||
|
links.forEach(l => l.addEventListener('click', () => {
|
||||||
|
links.forEach(x => x.classList.remove('active'));
|
||||||
|
l.classList.add('active');
|
||||||
|
}));
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
+367
-158
@@ -5,62 +5,231 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||||
<title>MetonaToast — 轻量精致的 Toast 通知库</title>
|
<title>MetonaToast — 轻量精致的 Toast 通知库</title>
|
||||||
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。80+ 图标类型、11 种动画、主题系统、国际化、插件架构。">
|
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。107 种图标、11 种动画、主题系统、国际化、插件架构、React 适配器。">
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--bg: #0b1121; --bg2: #111827; --surface: #1a2332;
|
--bg: #070b14;
|
||||||
--border: #1e2d4d; --text: #e2e8f0; --muted: #64748b;
|
--surface: rgba(255,255,255,0.03);
|
||||||
--accent: #6366f1; --accent2: #818cf8; --radius: 14px;
|
--surface-hover: rgba(255,255,255,0.06);
|
||||||
|
--border: rgba(255,255,255,0.08);
|
||||||
|
--text: #e8ecf4;
|
||||||
|
--muted: #8b94a7;
|
||||||
|
--accent1: #6366f1;
|
||||||
|
--accent2: #a855f7;
|
||||||
|
--accent3: #ec4899;
|
||||||
|
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||||
|
--radius: 16px;
|
||||||
}
|
}
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin:0; padding:0; box-sizing:border-box; }
|
||||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; }
|
html { scroll-behavior:smooth; }
|
||||||
a { color: var(--accent2); text-decoration: none; }
|
body {
|
||||||
code { background: rgba(99,102,241,0.12); color: #a5b4fc; padding: 2px 7px; border-radius: 5px; font-size: 0.9em; }
|
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||||
pre { background: #0a0f1a; border: 1px solid var(--border); border-radius: 10px; padding: 18px 22px; overflow-x: auto; font: 13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color: #cbd5e1; }
|
background:var(--bg); color:var(--text); line-height:1.7; overflow-x:hidden;
|
||||||
pre .kw { color: #c084fc; } pre .fn { color: #60a5fa; } pre .s { color: #34d399; } pre .cm { color: #475569; }
|
background-image:
|
||||||
|
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99,102,241,0.15), transparent),
|
||||||
|
radial-gradient(ellipse 60% 40% at 90% 10%, rgba(168,85,247,0.08), transparent),
|
||||||
|
radial-gradient(ellipse 50% 40% at 10% 30%, rgba(236,72,153,0.06), transparent);
|
||||||
|
}
|
||||||
|
/* 背景网格 */
|
||||||
|
body::before {
|
||||||
|
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
|
||||||
|
background-size:56px 56px;
|
||||||
|
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
|
||||||
|
-webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
|
||||||
|
}
|
||||||
|
a { color:#a5b4fc; text-decoration:none; transition:color .2s; }
|
||||||
|
a:hover { color:#c7d2fe; }
|
||||||
|
::selection { background:rgba(139,92,246,.4); }
|
||||||
|
|
||||||
.nav { position: sticky; top: 0; z-index: 100; background: rgba(11,17,33,0.88); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
|
.grad-text {
|
||||||
.nav-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 56px; }
|
background:var(--grad);
|
||||||
.nav-logo { font-size: 18px; font-weight: 700; background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
-webkit-background-clip:text; background-clip:text;
|
||||||
.nav-links { display: flex; gap: 24px; }
|
-webkit-text-fill-color:transparent; color:transparent;
|
||||||
.nav-links a { color: var(--muted); font-size: 14px; font-weight: 500; transition: .2s; }
|
}
|
||||||
.nav-links a:hover { color: var(--accent2); }
|
|
||||||
|
|
||||||
.hero { text-align: center; padding: 100px 20px 70px; background: radial-gradient(ellipse at 50% 0%, rgba(99,102,241,0.18) 0%, transparent 60%), linear-gradient(180deg, #0f172a 0%, var(--bg) 100%); border-bottom: 1px solid var(--border); }
|
/* ===== Nav ===== */
|
||||||
.hero h1 { font-size: 52px; font-weight: 800; }
|
.nav {
|
||||||
.hero h1 span { background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
position:sticky; top:0; z-index:100;
|
||||||
.hero p { color: var(--muted); font-size: 17px; margin-top: 12px; max-width: 600px; margin-left: auto; margin-right: auto; }
|
background:rgba(7,11,20,0.7);
|
||||||
.hero .badges { display: flex; gap: 12px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }
|
backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
|
||||||
.hero .badges span { background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 6px 18px; font-size: 13px; color: var(--accent2); }
|
border-bottom:1px solid var(--border);
|
||||||
.hero-actions { margin-top: 32px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
}
|
||||||
.btn { padding: 11px 28px; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: .2s; border: none; }
|
.nav-inner {
|
||||||
.btn-primary { background: var(--accent); color: #fff; }
|
max-width:1180px; margin:0 auto; display:flex; align-items:center;
|
||||||
.btn-primary:hover { background: var(--accent2); transform: translateY(-1px); }
|
justify-content:space-between; padding:0 28px; height:60px;
|
||||||
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
}
|
||||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent2); }
|
.nav-logo { font-size:19px; font-weight:800; letter-spacing:.2px; cursor:pointer; }
|
||||||
|
.nav-links { display:flex; gap:26px; }
|
||||||
|
.nav-links a { color:var(--muted); font-size:14px; font-weight:500; position:relative; }
|
||||||
|
.nav-links a::after {
|
||||||
|
content:''; position:absolute; left:0; right:100%; bottom:-6px; height:2px;
|
||||||
|
background:var(--grad); border-radius:2px; transition:right .25s;
|
||||||
|
}
|
||||||
|
.nav-links a:hover::after { right:0; }
|
||||||
|
.nav-links a:hover { color:var(--text); }
|
||||||
|
|
||||||
.container { max-width: 1100px; margin: 0 auto; padding: 60px 24px; }
|
/* ===== Hero ===== */
|
||||||
.section-title { font-size: 28px; font-weight: 700; text-align: center; margin-bottom: 8px; }
|
.hero { text-align:center; padding:110px 24px 60px; position:relative; z-index:1; }
|
||||||
.section-desc { color: var(--muted); text-align: center; margin-bottom: 40px; font-size: 15px; }
|
.hero-badge {
|
||||||
|
display:inline-flex; align-items:center; gap:8px;
|
||||||
|
background:rgba(99,102,241,0.1); border:1px solid rgba(99,102,241,0.3);
|
||||||
|
color:#a5b4fc; font-size:12px; font-weight:600; letter-spacing:1px;
|
||||||
|
padding:6px 16px; border-radius:999px; margin-bottom:26px;
|
||||||
|
}
|
||||||
|
.hero-badge .dot {
|
||||||
|
width:7px; height:7px; border-radius:50%; background:#34d399;
|
||||||
|
box-shadow:0 0 12px #34d399; animation:pulse 2s infinite;
|
||||||
|
}
|
||||||
|
@keyframes pulse { 50% { opacity:.4; } }
|
||||||
|
.hero h1 { font-size:clamp(42px, 7vw, 76px); font-weight:800; letter-spacing:-1px; line-height:1.1; }
|
||||||
|
.hero h1 .grad-text { animation:shimmer 6s linear infinite; }
|
||||||
|
@keyframes shimmer {
|
||||||
|
0%,100% { filter:hue-rotate(0deg); }
|
||||||
|
50% { filter:hue-rotate(30deg); }
|
||||||
|
}
|
||||||
|
.hero-sub {
|
||||||
|
color:var(--muted); font-size:clamp(15px, 2.2vw, 18px);
|
||||||
|
max-width:640px; margin:18px auto 0;
|
||||||
|
}
|
||||||
|
.hero-badges { display:flex; gap:10px; justify-content:center; margin-top:26px; flex-wrap:wrap; }
|
||||||
|
.hero-badges span {
|
||||||
|
background:var(--surface); border:1px solid var(--border); border-radius:999px;
|
||||||
|
padding:5px 14px; font-size:12.5px; color:#cbd5e1;
|
||||||
|
transition:border-color .2s, transform .2s;
|
||||||
|
}
|
||||||
|
.hero-badges span:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
|
||||||
|
.hero-actions { margin-top:36px; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
|
||||||
|
.btn {
|
||||||
|
padding:12px 30px; border-radius:12px; font-size:14.5px; font-weight:600;
|
||||||
|
cursor:pointer; transition:transform .2s, box-shadow .2s, border-color .2s;
|
||||||
|
border:1px solid transparent;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background:var(--grad); color:#fff;
|
||||||
|
box-shadow:0 8px 30px -8px rgba(139,92,246,.6);
|
||||||
|
}
|
||||||
|
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 12px 40px -8px rgba(139,92,246,.8); }
|
||||||
|
.btn-ghost { background:var(--surface); border-color:var(--border); color:var(--text); }
|
||||||
|
.btn-ghost:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); background:var(--surface-hover); }
|
||||||
|
|
||||||
.features { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
|
/* 浮动光球 */
|
||||||
.feature { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; transition: .2s; }
|
.orb { position:absolute; border-radius:50%; filter:blur(70px); pointer-events:none; z-index:0; }
|
||||||
.feature:hover { border-color: var(--accent); }
|
.orb-1 { width:340px; height:340px; background:rgba(99,102,241,.25); top:-80px; left:12%; animation:float 12s ease-in-out infinite; }
|
||||||
.feature-icon { font-size: 28px; margin-bottom: 12px; }
|
.orb-2 { width:260px; height:260px; background:rgba(236,72,153,.18); top:40px; right:10%; animation:float 15s ease-in-out infinite reverse; }
|
||||||
.feature h3 { font-size: 16px; margin-bottom: 6px; }
|
.orb-3 { width:200px; height:200px; background:rgba(168,85,247,.2); bottom:-40px; left:40%; animation:float 18s ease-in-out infinite 2s; }
|
||||||
.feature p { color: var(--muted); font-size: 13px; line-height: 1.6; }
|
@keyframes float { 0%,100%{transform:translateY(0) translateX(0);} 50%{transform:translateY(-30px) translateX(20px);} }
|
||||||
|
|
||||||
.code-block { max-width: 750px; margin: 0 auto; }
|
/* ===== 数据统计 ===== */
|
||||||
|
.stats { position:relative; z-index:1; max-width:1000px; margin:10px auto 0; padding:0 24px; }
|
||||||
|
.stats-grid {
|
||||||
|
display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:1px;
|
||||||
|
background:var(--border); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden;
|
||||||
|
}
|
||||||
|
.stat { background:rgba(10,15,26,.85); padding:22px 12px; text-align:center; backdrop-filter:blur(8px); }
|
||||||
|
.stat-num { font-size:26px; font-weight:800; }
|
||||||
|
.stat-label { font-size:12px; color:var(--muted); margin-top:2px; letter-spacing:.5px; }
|
||||||
|
|
||||||
.theme-demo { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin: 24px 0; }
|
/* ===== 通用 Section ===== */
|
||||||
.theme-btn { padding: 10px 20px; border-radius: 8px; border: 1px solid var(--border); cursor: pointer; background: var(--surface); color: var(--text); font-size: 13px; font-weight: 500; transition: .2s; }
|
.container { position:relative; z-index:1; max-width:1100px; margin:0 auto; padding:80px 24px; }
|
||||||
.theme-btn:hover { border-color: var(--accent); }
|
.section-title { font-size:30px; font-weight:800; text-align:center; letter-spacing:-.5px; }
|
||||||
|
.section-desc { color:var(--muted); text-align:center; margin-top:8px; font-size:15px; }
|
||||||
|
|
||||||
footer { text-align: center; padding: 40px 24px; color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
|
/* ===== 特性卡片 ===== */
|
||||||
|
.features { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:16px; margin-top:44px; }
|
||||||
|
.feature {
|
||||||
|
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||||
|
padding:26px 24px; transition:transform .25s, border-color .25s, background .25s;
|
||||||
|
position:relative; overflow:hidden;
|
||||||
|
}
|
||||||
|
.feature::before {
|
||||||
|
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||||
|
background:var(--grad); opacity:0; transition:opacity .25s;
|
||||||
|
}
|
||||||
|
.feature:hover { transform:translateY(-4px); border-color:rgba(129,140,248,.4); background:var(--surface-hover); }
|
||||||
|
.feature:hover::before { opacity:1; }
|
||||||
|
.feature-icon { font-size:26px; margin-bottom:12px; display:inline-block; }
|
||||||
|
.feature h3 { font-size:15.5px; margin-bottom:6px; font-weight:700; }
|
||||||
|
.feature p { color:var(--muted); font-size:13px; line-height:1.65; }
|
||||||
|
.feature code {
|
||||||
|
background:rgba(99,102,241,0.15); color:#a5b4fc;
|
||||||
|
padding:1px 6px; border-radius:5px; font-size:12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
/* ===== 代码块 ===== */
|
||||||
.hero h1 { font-size: 32px; }
|
.code-block { max-width:760px; margin:36px auto 0; }
|
||||||
.nav-links { gap: 14px; }
|
.terminal {
|
||||||
|
background:#0a0f1c; border:1px solid var(--border); border-radius:12px; overflow:hidden;
|
||||||
|
box-shadow:0 20px 60px -20px rgba(0,0,0,.6);
|
||||||
|
}
|
||||||
|
.terminal-bar {
|
||||||
|
display:flex; align-items:center; gap:7px; padding:11px 16px;
|
||||||
|
background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border);
|
||||||
|
}
|
||||||
|
.terminal-bar .t-dot { width:11px; height:11px; border-radius:50%; }
|
||||||
|
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
|
||||||
|
.terminal-bar .t-title { margin-left:10px; font-size:11.5px; color:var(--muted); letter-spacing:.5px; }
|
||||||
|
.terminal pre {
|
||||||
|
padding:20px 22px; overflow-x:auto;
|
||||||
|
font:13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
|
||||||
|
color:#cbd5e1;
|
||||||
|
}
|
||||||
|
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; }
|
||||||
|
.terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
|
||||||
|
.terminal pre .num { color:#f472b6; }
|
||||||
|
|
||||||
|
/* ===== 主题预览 ===== */
|
||||||
|
.theme-demo { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin-top:36px; }
|
||||||
|
.theme-btn {
|
||||||
|
padding:11px 22px; border-radius:10px; border:1px solid var(--border);
|
||||||
|
cursor:pointer; background:var(--surface); color:var(--text);
|
||||||
|
font-size:13.5px; font-weight:500; transition:all .2s;
|
||||||
|
}
|
||||||
|
.theme-btn:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
|
||||||
|
.theme-btn.active {
|
||||||
|
background:var(--grad); color:#fff; border-color:transparent;
|
||||||
|
box-shadow:0 6px 24px -6px rgba(139,92,246,.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== 环境支持 ===== */
|
||||||
|
.env-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:12px; margin-top:40px; }
|
||||||
|
.env-card {
|
||||||
|
background:var(--surface); border:1px solid var(--border); border-radius:12px;
|
||||||
|
padding:20px 14px; text-align:center; transition:border-color .2s, transform .2s;
|
||||||
|
}
|
||||||
|
.env-card:hover { border-color:rgba(129,140,248,.4); transform:translateY(-3px); }
|
||||||
|
.env-card .e-ico { font-size:26px; }
|
||||||
|
.env-card h3 { font-size:14px; margin:8px 0 2px; }
|
||||||
|
.env-card p { font-size:12.5px; color:var(--muted); }
|
||||||
|
|
||||||
|
/* ===== CTA ===== */
|
||||||
|
.cta {
|
||||||
|
max-width:760px; margin:0 auto; text-align:center;
|
||||||
|
background:var(--grad); border-radius:20px; padding:56px 32px;
|
||||||
|
position:relative; overflow:hidden;
|
||||||
|
}
|
||||||
|
.cta::before {
|
||||||
|
content:''; position:absolute; inset:0;
|
||||||
|
background:radial-gradient(ellipse 60% 60% at 50% 120%, rgba(255,255,255,.25), transparent);
|
||||||
|
}
|
||||||
|
.cta h2 { font-size:30px; font-weight:800; color:#fff; position:relative; }
|
||||||
|
.cta p { color:rgba(255,255,255,.85); margin-top:10px; position:relative; }
|
||||||
|
.cta .btn { margin-top:26px; background:#fff; color:#6d28d9; position:relative; }
|
||||||
|
.cta .btn:hover { transform:translateY(-2px); }
|
||||||
|
|
||||||
|
footer { text-align:center; padding:44px 24px; color:var(--muted); font-size:13px; border-top:1px solid var(--border); position:relative; z-index:1; }
|
||||||
|
footer a { color:#a5b4fc; }
|
||||||
|
|
||||||
|
/* 滚动淡入 */
|
||||||
|
.reveal { opacity:0; transform:translateY(24px); transition:opacity .6s ease, transform .6s ease; }
|
||||||
|
.reveal.visible { opacity:1; transform:none; }
|
||||||
|
|
||||||
|
@media (max-width:768px) {
|
||||||
|
.hero { padding:80px 20px 44px; }
|
||||||
|
.nav-links { gap:16px; }
|
||||||
|
.container { padding:56px 20px; }
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
@@ -68,7 +237,7 @@
|
|||||||
|
|
||||||
<nav class="nav">
|
<nav class="nav">
|
||||||
<div class="nav-inner">
|
<div class="nav-inner">
|
||||||
<div class="nav-logo">MetonaToast</div>
|
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
|
||||||
<div class="nav-links">
|
<div class="nav-links">
|
||||||
<a href="index.html">首页</a>
|
<a href="index.html">首页</a>
|
||||||
<a href="docs.html">文档</a>
|
<a href="docs.html">文档</a>
|
||||||
@@ -79,117 +248,117 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<h1><span>MetonaToast</span></h1>
|
<div class="orb orb-1"></div>
|
||||||
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式源码,gzip 不到 10KB。</p>
|
<div class="orb orb-2"></div>
|
||||||
<div class="badges">
|
<div class="orb orb-3"></div>
|
||||||
<span>📦 <10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
|
<div class="hero-badge"><span class="dot"></span> v0.5.0 · 385 测试 · 覆盖率 96%</div>
|
||||||
|
<h1><span class="grad-text">MetonaToast</span></h1>
|
||||||
|
<p class="hero-sub">轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式,gzip 不到 10KB,一切开箱即用。</p>
|
||||||
|
<div class="hero-badges">
|
||||||
|
<span>📦 <10KB gzip</span>
|
||||||
|
<span>🎨 107 图标</span>
|
||||||
|
<span>🎭 11 动画</span>
|
||||||
|
<span>🧩 4 插件</span>
|
||||||
|
<span>🔗 17 钩子</span>
|
||||||
|
<span>⚛️ React 适配器</span>
|
||||||
|
<span>🌍 国际化</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="hero-actions">
|
<div class="hero-actions">
|
||||||
<button class="btn btn-primary" onclick="demoQuickStart()">快速预览</button>
|
<button class="btn btn-primary" onclick="demoQuickStart()">✨ 快速预览</button>
|
||||||
<button class="btn btn-outline" onclick="location.href='docs.html'">查看文档</button>
|
<button class="btn btn-ghost" onclick="location.href='docs.html'">📖 查看文档</button>
|
||||||
|
<button class="btn btn-ghost" onclick="location.href='demo.html'">🎮 在线演示</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
<section class="stats reveal">
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat"><div class="stat-num grad-text"><10KB</div><div class="stat-label">gzip 体积</div></div>
|
||||||
|
<div class="stat"><div class="stat-num grad-text">107</div><div class="stat-label">内置图标</div></div>
|
||||||
|
<div class="stat"><div class="stat-num grad-text">11</div><div class="stat-label">动画效果</div></div>
|
||||||
|
<div class="stat"><div class="stat-num grad-text">17</div><div class="stat-label">生命周期钩子</div></div>
|
||||||
|
<div class="stat"><div class="stat-num grad-text">385</div><div class="stat-label">测试用例</div></div>
|
||||||
|
<div class="stat"><div class="stat-num grad-text">96%</div><div class="stat-label">行覆盖率</div></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<main class="container">
|
<main class="container">
|
||||||
<h2 class="section-title">核心特性</h2>
|
<h2 class="section-title reveal">核心特性</h2>
|
||||||
<p class="section-desc">一切你需要的,都在一个不到 10KB 的包中</p>
|
<p class="section-desc reveal">你需要的一切,都在一个不到 10KB 的包中</p>
|
||||||
|
|
||||||
<div class="features">
|
<div class="features">
|
||||||
<div class="feature">
|
<div class="feature reveal">
|
||||||
<div class="feature-icon">🎨</div>
|
|
||||||
<h3>107 种图标类型</h3>
|
|
||||||
<p>success / error / warning / info / loading 以及 100+ 扩展类型,覆盖所有常见场景。</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="feature-icon">🎭</div>
|
|
||||||
<h3>11 种动画效果</h3>
|
|
||||||
<p>slide / fade / scale / bounce / flip / rotate / zoom / slideUp / slideDown / slideLeft / slideRight,每种感官差异明显。</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="feature-icon">🖌️</div>
|
|
||||||
<h3>主题系统</h3>
|
|
||||||
<p>内置 light / dark / auto / warm 四种主题,支持 <code>registerTheme()</code> 自定义注册任意主题。</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="feature-icon">🌍</div>
|
|
||||||
<h3>国际化</h3>
|
|
||||||
<p>内置 zh-CN 和 en-US 完整翻译,400+ 词汇。支持 <code>addTranslations()</code> 扩展任意语言。</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="feature-icon">🔌</div>
|
|
||||||
<h3>插件系统</h3>
|
|
||||||
<p>3 款预设插件 + 自定义插件架构。keyboard / persistence / accessibility 开箱即用。</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="feature-icon">✋</div>
|
|
||||||
<h3>拖拽关闭</h3>
|
|
||||||
<p>任意方向拖动 Toast 即可关闭。支持 Pointer Events,移动端和桌面端体验一致。</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature">
|
|
||||||
<div class="feature-icon">⚡</div>
|
<div class="feature-icon">⚡</div>
|
||||||
<h3>零依赖</h3>
|
<h3>零依赖</h3>
|
||||||
<p>TypeScript 严格模式,纯原生实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
<p>TypeScript 严格模式,纯原生实现,无任何运行时依赖。兼容所有现代浏览器。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">🎨</div>
|
||||||
|
<h3>107 种图标类型</h3>
|
||||||
|
<p>success / error / warning / info / loading 及 100+ 扩展类型,覆盖所有常见场景。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">🎭</div>
|
||||||
|
<h3>11 种动画 + 自定义</h3>
|
||||||
|
<p>每种感官差异明显,<code>animations.register()</code> 注册自定义动画,动态注入 keyframes 真实生效。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">🖌️</div>
|
||||||
|
<h3>主题系统</h3>
|
||||||
|
<p>light / dark / auto / warm 内置主题,<code>registerTheme()</code> 自定义,跟随系统自动切换。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">🌍</div>
|
||||||
|
<h3>国际化</h3>
|
||||||
|
<p>zh-CN / en-US 完整翻译,<code>addTranslations()</code> 扩展任意语言,RTL 完整适配。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">🧩</div>
|
||||||
|
<h3>插件系统</h3>
|
||||||
|
<p>4 款预设:keyboard / persistence / accessibility / dedupe,支持自定义插件架构。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">🔗</div>
|
||||||
|
<h3>17 个钩子</h3>
|
||||||
|
<p>完整生命周期钩子,<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 支持拦截。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">✋</div>
|
||||||
|
<h3>拖拽关闭</h3>
|
||||||
|
<p>任意方向拖动即关,阈值 <code>dragThreshold</code> 可配置。Pointer Events 全端一致。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
|
<div class="feature-icon">⚛️</div>
|
||||||
|
<h3>React 适配器</h3>
|
||||||
|
<p><code>@metona-team/metona-toast/react</code> — useToast hook + 声明式 Toast 组件,主包保持零依赖。</p>
|
||||||
|
</div>
|
||||||
|
<div class="feature reveal">
|
||||||
<div class="feature-icon">🛡️</div>
|
<div class="feature-icon">🛡️</div>
|
||||||
<h3>错误捕获</h3>
|
<h3>错误捕获</h3>
|
||||||
<p>全局 <code>onError</code> 回调可捕获钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统。</p>
|
<p>全局 <code>onError</code> 回调捕获钩子与定时器异常,方便接入 Sentry、DataDog。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature reveal">
|
||||||
<div class="feature-icon">⏱️</div>
|
<div class="feature-icon">⏱️</div>
|
||||||
<h3>暂停恢复</h3>
|
<h3>暂停恢复</h3>
|
||||||
<p>悬停自动暂停、拖拽临时暂停、手动 pause/resume,剩余时间精确保持,不丢失进度。</p>
|
<p>悬停暂停、拖拽暂停、手动 pause/resume,剩余时间精确保持不丢失。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature reveal">
|
||||||
<div class="feature-icon">📘</div>
|
<div class="feature-icon">📘</div>
|
||||||
<h3>TypeScript</h3>
|
<h3>类型安全</h3>
|
||||||
<p>严格模式源码,类型从源码自动生成。strictNullChecks、noImplicitAny 全开,零类型漂移。</p>
|
<p>严格模式源码,类型自动生成零漂移。加载态转换 id 稳定,原地更新。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<section class="container">
|
<section class="container" style="padding-top:0">
|
||||||
<h2 class="section-title">环境支持</h2>
|
<h2 class="section-title reveal">快速开始</h2>
|
||||||
<p class="section-desc">所有现代浏览器原生支持,无需 polyfill</p>
|
<p class="section-desc reveal">浏览器上仅注册两个全局变量:<code>MeToast</code> 和 <code>Met</code>,完全等价</p>
|
||||||
<div class="env-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(165px,1fr)); gap:12px;">
|
<div class="code-block reveal">
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
<div class="terminal">
|
||||||
<div style="font-size:28px;margin-bottom:6px;">🌐</div>
|
<div class="terminal-bar">
|
||||||
<h3>Chrome</h3>
|
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
|
||||||
<p>64+</p>
|
<span class="t-title">quickstart.js</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
<pre><span class="cm"># 安装</span>
|
||||||
<div style="font-size:28px;margin-bottom:6px;">🦊</div>
|
|
||||||
<h3>Firefox</h3>
|
|
||||||
<p>63+</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
|
||||||
<div style="font-size:28px;margin-bottom:6px;">🧭</div>
|
|
||||||
<h3>Safari</h3>
|
|
||||||
<p>11+</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
|
||||||
<div style="font-size:28px;margin-bottom:6px;">🪟</div>
|
|
||||||
<h3>Edge</h3>
|
|
||||||
<p>79+</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
|
||||||
<div style="font-size:28px;margin-bottom:6px;">📦</div>
|
|
||||||
<h3>Node.js</h3>
|
|
||||||
<p>16+</p>
|
|
||||||
</div>
|
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
|
||||||
<div style="font-size:28px;margin-bottom:6px;">📘</div>
|
|
||||||
<h3>TypeScript</h3>
|
|
||||||
<p>4.5+</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">核心依赖:Web Animations API · Pointer Events · CSS Grid · CSS Custom Properties</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="container">
|
|
||||||
<h2 class="section-title">快速开始</h2>
|
|
||||||
<p class="section-desc">浏览器 window 上仅注册两个全局变量:<code>MeToast</code> 和 <code>Met</code>,完全等价</p>
|
|
||||||
<div class="code-block">
|
|
||||||
<pre><span class="cm"># npm 安装</span>
|
|
||||||
npm install @metona-team/metona-toast
|
npm install @metona-team/metona-toast
|
||||||
|
|
||||||
<span class="cm">// 导入</span>
|
<span class="cm">// 导入</span>
|
||||||
@@ -203,42 +372,44 @@ MeToast.<span class="fn">loading</span>(<span class="s">'加载中...'</span>);
|
|||||||
<span class="cm">// CDN 直接引用</span>
|
<span class="cm">// CDN 直接引用</span>
|
||||||
<span class="kw"><script</span> <span class="fn">src</span>=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaToast/raw/branch/master/dist/metona-toast.min.js"</span><span class="kw">></script></span></pre>
|
<span class="kw"><script</span> <span class="fn">src</span>=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaToast/raw/branch/master/dist/metona-toast.min.js"</span><span class="kw">></script></span></pre>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="container">
|
<section class="container" style="padding-top:0">
|
||||||
<h2 class="section-title">高级功能速览</h2>
|
<h2 class="section-title reveal">高级功能速览</h2>
|
||||||
<p class="section-desc">不仅仅是一个 Toast——确认框、输入框、进度条、倒计时、队列,应有尽有</p>
|
<p class="section-desc reveal">不仅仅是 Toast — 确认框、输入框、进度条、倒计时、队列、Action,应有尽有</p>
|
||||||
<div class="code-block">
|
<div class="code-block reveal">
|
||||||
|
<div class="terminal">
|
||||||
|
<div class="terminal-bar">
|
||||||
|
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
|
||||||
|
<span class="t-title">advanced.js</span>
|
||||||
|
</div>
|
||||||
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
|
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
|
||||||
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
||||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
|
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>
|
||||||
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
|
|
||||||
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
<span class="cm">// 确认对话框 → Promise<boolean></span>
|
<span class="cm">// 确认 / 输入对话框</span>
|
||||||
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除吗?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
|
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
|
||||||
|
|
||||||
<span class="cm">// 输入对话框 → Promise<string|null></span>
|
|
||||||
<span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>);
|
<span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>);
|
||||||
|
|
||||||
<span class="cm">// Action Toast — 内嵌操作按钮</span>
|
<span class="cm">// Action Toast — 内嵌操作按钮</span>
|
||||||
MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
||||||
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>() },
|
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>() },
|
||||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>() },
|
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">close</span>: <span class="kw">false</span> },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
<span class="cm">// Toast 分组管理</span>
|
<span class="cm">// 加载 → 链式转换(id 稳定)</span>
|
||||||
<span class="kw">const</span> orderGroup = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
<span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||||
orderGroup.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre>
|
||||||
orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭整组</span></pre>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="container">
|
<section class="container" style="padding-top:0">
|
||||||
<h2 class="section-title">主题切换</h2>
|
<h2 class="section-title reveal">主题切换</h2>
|
||||||
<p class="section-desc">点击按钮实时预览四种内置主题</p>
|
<p class="section-desc reveal">点击按钮实时预览内置主题,切换结果自动持久化</p>
|
||||||
<div class="theme-demo">
|
<div class="theme-demo reveal">
|
||||||
<button class="theme-btn" onclick="switchTheme('light')">☀️ 浅色 Light</button>
|
<button class="theme-btn" onclick="switchTheme('light')">☀️ 浅色 Light</button>
|
||||||
<button class="theme-btn" onclick="switchTheme('dark')">🌙 深色 Dark</button>
|
<button class="theme-btn" onclick="switchTheme('dark')">🌙 深色 Dark</button>
|
||||||
<button class="theme-btn" onclick="switchTheme('auto')">🔄 自动 Auto</button>
|
<button class="theme-btn" onclick="switchTheme('auto')">🔄 自动 Auto</button>
|
||||||
@@ -246,8 +417,34 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="container" style="padding-top:0">
|
||||||
|
<h2 class="section-title reveal">环境支持</h2>
|
||||||
|
<p class="section-desc reveal">所有现代浏览器原生支持,无需 polyfill</p>
|
||||||
|
<div class="env-grid reveal">
|
||||||
|
<div class="env-card"><div class="e-ico">🌐</div><h3>Chrome</h3><p>64+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">🦊</div><h3>Firefox</h3><p>63+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">🧭</div><h3>Safari</h3><p>11+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">🪟</div><h3>Edge</h3><p>79+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">📱</div><h3>iOS Safari</h3><p>11+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">🤖</div><h3>Android</h3><p>64+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">📦</div><h3>Node.js</h3><p>16+</p></div>
|
||||||
|
<div class="env-card"><div class="e-ico">📘</div><h3>TypeScript</h3><p>4.5+</p></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="container" style="padding-top:0">
|
||||||
|
<div class="cta reveal">
|
||||||
|
<h2>开始使用 MetonaToast</h2>
|
||||||
|
<p>一条命令安装,立即体验精致优雅的 Toast 通知</p>
|
||||||
|
<button class="btn" onclick="location.href='demo.html'">🎮 打开在线演示</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>MetonaToast v0.2.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
MetonaToast v0.5.0 · MIT License ·
|
||||||
|
<a href="https://git.metona.cn/MetonaTeam/MetonaToast" target="_blank">Gitea</a> ·
|
||||||
|
<a href="docs.html">文档</a> ·
|
||||||
|
<a href="demo.html">演示</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="../dist/metona-toast.js"></script>
|
<script src="../dist/metona-toast.js"></script>
|
||||||
@@ -256,16 +453,28 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
|||||||
|
|
||||||
function demoQuickStart() {
|
function demoQuickStart() {
|
||||||
MeToast.success('✨ 欢迎使用 MetonaToast!');
|
MeToast.success('✨ 欢迎使用 MetonaToast!');
|
||||||
setTimeout(() => MeToast.info('💡 试试点击右上角的 Toast 来关闭它'), 800);
|
setTimeout(() => MeToast.info('💡 点击 Toast 或点击 × 即可关闭'), 800);
|
||||||
setTimeout(() => MeToast.warning('⚠️ 你也可以拖动 Toast 来关闭'), 1600);
|
setTimeout(() => MeToast.warning('⚠️ 拖动 Toast 也能关闭', { draggable: true }), 1600);
|
||||||
|
setTimeout(() => MeToast.success('🎭 试试 bounce 动画', { animation: 'bounce' }), 2400);
|
||||||
}
|
}
|
||||||
|
|
||||||
function switchTheme(t) {
|
function switchTheme(t) {
|
||||||
MeToast.themes.switchTheme(t);
|
MeToast.themes.switchTheme(t);
|
||||||
MeToast.configure({ theme: t });
|
MeToast.configure({ theme: t });
|
||||||
|
document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
|
||||||
|
event.target.classList.add('active');
|
||||||
const names = { light: '浅色', dark: '深色', auto: '自动', warm: '暖色' };
|
const names = { light: '浅色', dark: '深色', auto: '自动', warm: '暖色' };
|
||||||
MeToast.info('已切换到「' + names[t] + '」主题', { theme: t });
|
MeToast.info('已切换到「' + names[t] + '」主题', { theme: t });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动淡入
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } });
|
||||||
|
}, { threshold: 0.12 });
|
||||||
|
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
||||||
|
|
||||||
|
// 页面加载自动演示
|
||||||
|
setTimeout(demoQuickStart, 600);
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+62
-1
@@ -4,12 +4,71 @@
|
|||||||
* @version 0.2.1
|
* @version 0.2.1
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ANIMATIONS } from './constants.js';
|
import { ANIMATIONS, ANIMATION_TYPES } from './constants.js';
|
||||||
import type { AnimationConfig, AnimationUtils } from './types.js';
|
import type { AnimationConfig, AnimationUtils } from './types.js';
|
||||||
|
|
||||||
// 动画缓存
|
// 动画缓存
|
||||||
const animationMap: Map<string, AnimationConfig> = new Map();
|
const animationMap: Map<string, AnimationConfig> = new Map();
|
||||||
|
|
||||||
|
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
||||||
|
const BUILTIN_ANIMATIONS: Set<string> = new Set(ANIMATION_TYPES);
|
||||||
|
|
||||||
|
// 动态注入的自定义动画样式(惰性创建)
|
||||||
|
let animStyleElement: HTMLStyleElement | null = null;
|
||||||
|
const injectedAnimations: Set<string> = new Set();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 CSS 属性对象转换为内联 CSS 字符串
|
||||||
|
*/
|
||||||
|
const toCss = (props: Record<string, string | number>): string =>
|
||||||
|
Object.entries(props)
|
||||||
|
.map(([key, value]) => `${key}:${value}`)
|
||||||
|
.join(';');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 为自定义动画生成入场 keyframes + 类规则
|
||||||
|
*/
|
||||||
|
const buildAnimationCSS = (name: string, config: AnimationConfig): string => {
|
||||||
|
const enter = toCss(config.enter);
|
||||||
|
return `
|
||||||
|
@keyframes met-${name}-in {
|
||||||
|
from { ${enter}; }
|
||||||
|
}
|
||||||
|
.met-anim-${name}.met-toast { opacity: 0; }
|
||||||
|
.met-anim-${name}.met-toast.met-show {
|
||||||
|
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
||||||
|
*/
|
||||||
|
export const ensureAnimationCSS = (name: string): void => {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
if (BUILTIN_ANIMATIONS.has(name)) return;
|
||||||
|
|
||||||
|
const config = animationMap.get(name);
|
||||||
|
if (!config || injectedAnimations.has(name)) return;
|
||||||
|
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.getElementById('metona-toast-anim-styles') as HTMLStyleElement | null;
|
||||||
|
if (!animStyleElement) {
|
||||||
|
animStyleElement = document.createElement('style');
|
||||||
|
animStyleElement.id = 'metona-toast-anim-styles';
|
||||||
|
document.head.appendChild(animStyleElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animStyleElement.textContent += buildAnimationCSS(name, config);
|
||||||
|
injectedAnimations.add(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查动画是否存在(内置或自定义)
|
||||||
|
*/
|
||||||
|
export const hasAnimation = (name: string): boolean => animationMap.has(name);
|
||||||
|
|
||||||
// 注册默认动画
|
// 注册默认动画
|
||||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||||
animationMap.set(name, {
|
animationMap.set(name, {
|
||||||
@@ -70,6 +129,8 @@ export const animationUtils: AnimationUtils = {
|
|||||||
|
|
||||||
destroy(): void {
|
destroy(): void {
|
||||||
animationMap.clear();
|
animationMap.clear();
|
||||||
|
injectedAnimations.clear();
|
||||||
|
animStyleElement = null;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+56
-23
@@ -1,12 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast API — meToast 核心 API 对象
|
* MetonaToast API — meToast 核心 API 对象
|
||||||
* @module api
|
* @module api
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Toast, _containerCache } from './toast.js';
|
import { Toast, _containerCache } from './toast.js';
|
||||||
import { DEFAULTS } from './constants.js';
|
import { DEFAULTS, VERSION } from './constants.js';
|
||||||
import { escapeHTML } from './utils.js';
|
|
||||||
import { t, setCurrentLocale } from './i18n.js';
|
import { t, setCurrentLocale } from './i18n.js';
|
||||||
import { applyTheme } from './themes.js';
|
import { applyTheme } from './themes.js';
|
||||||
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
||||||
@@ -48,16 +47,13 @@ const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions =
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 版本号常量 */
|
|
||||||
const VERSION = '0.2.1';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* meToast API 对象
|
* meToast API 对象
|
||||||
*/
|
*/
|
||||||
const meToast: MeToast = {
|
const meToast: MeToast = {
|
||||||
_toasts: new Map<string, ToastInstance>(),
|
_toasts: new Map<string, ToastInstance>(),
|
||||||
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
||||||
version: '0.2.1',
|
version: VERSION,
|
||||||
|
|
||||||
configure(opts: Partial<ToastConfig>): MeToast {
|
configure(opts: Partial<ToastConfig>): MeToast {
|
||||||
if (!opts || typeof opts !== 'object') return this;
|
if (!opts || typeof opts !== 'object') return this;
|
||||||
@@ -71,6 +67,7 @@ const meToast: MeToast = {
|
|||||||
setCurrentLocale(opts.locale);
|
setCurrentLocale(opts.locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -80,7 +77,10 @@ const meToast: MeToast = {
|
|||||||
|
|
||||||
const t = new Toast(merged);
|
const t = new Toast(merged);
|
||||||
t.create();
|
t.create();
|
||||||
|
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||||
|
if (t.el !== null) {
|
||||||
this._toasts.set(t.id, t);
|
this._toasts.set(t.id, t);
|
||||||
|
}
|
||||||
|
|
||||||
return t;
|
return t;
|
||||||
},
|
},
|
||||||
@@ -161,15 +161,22 @@ const meToast: MeToast = {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||||
|
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||||
|
*/
|
||||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
||||||
const id = loadingToast.id;
|
const old = this._toasts.get(loadingToast.id);
|
||||||
const old = this._toasts.get(id);
|
|
||||||
if (!old) return null;
|
if (!old) return null;
|
||||||
|
|
||||||
const position = old.config.position;
|
old.update({
|
||||||
old.close();
|
...opts,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||||
|
} as ToastOptions);
|
||||||
|
|
||||||
return this._emit({ ...opts, type, message, position } as ToastOptions);
|
return old;
|
||||||
},
|
},
|
||||||
|
|
||||||
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
||||||
@@ -387,8 +394,10 @@ const meToast: MeToast = {
|
|||||||
actions.forEach((a, i) => {
|
actions.forEach((a, i) => {
|
||||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
|
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
|
||||||
if (btn && typeof a.onClick === 'function') {
|
if (btn && typeof a.onClick === 'function') {
|
||||||
btn.addEventListener('click', () => {
|
btn.addEventListener('click', (e: Event) => {
|
||||||
try { a.onClick(toast); } catch (e) { console.error('Action onClick error:', e); }
|
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||||
|
e.stopPropagation();
|
||||||
|
try { a.onClick(toast); } catch (err) { console.error('Action onClick error:', err); }
|
||||||
if (a.close !== false) toast.close();
|
if (a.close !== false) toast.close();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -588,6 +597,9 @@ const meToast: MeToast = {
|
|||||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||||
|
|
||||||
init(options: InitOptions = {}): MeToast {
|
init(options: InitOptions = {}): MeToast {
|
||||||
|
Toast.trigger('beforeInit');
|
||||||
|
this._destroyed = false;
|
||||||
|
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
this.configure(options.config);
|
this.configure(options.config);
|
||||||
}
|
}
|
||||||
@@ -599,9 +611,11 @@ const meToast: MeToast = {
|
|||||||
}
|
}
|
||||||
if (options.plugins && Array.isArray(options.plugins)) {
|
if (options.plugins && Array.isArray(options.plugins)) {
|
||||||
options.plugins.forEach((p) => {
|
options.plugins.forEach((p) => {
|
||||||
this.use(p as string);
|
this.use(p);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toast.trigger('afterInit');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -623,6 +637,7 @@ const meToast: MeToast = {
|
|||||||
updateConfig(config: Partial<ToastConfig>): MeToast {
|
updateConfig(config: Partial<ToastConfig>): MeToast {
|
||||||
if (config && typeof config === 'object') {
|
if (config && typeof config === 'object') {
|
||||||
Object.assign(this._config, config);
|
Object.assign(this._config, config);
|
||||||
|
Toast.trigger('configChange');
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -631,6 +646,7 @@ const meToast: MeToast = {
|
|||||||
const keys = Object.keys(this._config);
|
const keys = Object.keys(this._config);
|
||||||
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
||||||
Object.assign(this._config, DEFAULTS);
|
Object.assign(this._config, DEFAULTS);
|
||||||
|
Toast.trigger('configChange');
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -643,23 +659,31 @@ const meToast: MeToast = {
|
|||||||
}
|
}
|
||||||
this.plugins.register(plugin, { ...preset, ...options });
|
this.plugins.register(plugin, { ...preset, ...options });
|
||||||
|
|
||||||
// 连接插件钩子
|
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||||
|
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||||
|
const stored = this.plugins.get(plugin) as (Record<string, unknown> & Plugin) | null;
|
||||||
if (plugin === 'accessibility') {
|
if (plugin === 'accessibility') {
|
||||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
if (stored && typeof stored._off === 'function') { (stored._off as () => void)(); }
|
||||||
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
||||||
|
if (stored) {
|
||||||
|
stored._off = Toast.on('afterShow', (toast: ToastInstance) => {
|
||||||
if (typeof acc.announce === 'function') acc.announce(toast);
|
if (typeof acc.announce === 'function') acc.announce(toast);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (plugin === 'persistence') {
|
if (plugin === 'persistence') {
|
||||||
|
if (stored && typeof stored._saveOff === 'function') { (stored._saveOff as () => void)(); }
|
||||||
const saved = typeof preset.install === 'function'
|
const saved = typeof preset.install === 'function'
|
||||||
? preset.install(this.plugins as unknown as import('./types.js').PluginManager)
|
? preset.install(this.plugins as unknown as import('./types.js').PluginManager)
|
||||||
: null;
|
: null;
|
||||||
if (saved) this.configure(saved as Partial<ToastConfig>);
|
if (saved) this.configure(saved as Partial<ToastConfig>);
|
||||||
Toast.on('afterClose', () => {
|
|
||||||
const p = preset as Record<string, (c: unknown) => void>;
|
const p = preset as Record<string, (c: unknown) => void>;
|
||||||
|
if (stored) {
|
||||||
|
stored._saveOff = Toast.on('afterClose', () => {
|
||||||
if (typeof p.save === 'function') p.save(this.getConfig());
|
if (typeof p.save === 'function') p.save(this.getConfig());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else if (plugin && typeof plugin === 'object') {
|
} else if (plugin && typeof plugin === 'object') {
|
||||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
|
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
|
||||||
}
|
}
|
||||||
@@ -670,6 +694,8 @@ const meToast: MeToast = {
|
|||||||
destroy(): void {
|
destroy(): void {
|
||||||
if (this._destroyed) return;
|
if (this._destroyed) return;
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
|
|
||||||
|
Toast.trigger('beforeDestroy');
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
|
|
||||||
// 清理子模块
|
// 清理子模块
|
||||||
@@ -687,21 +713,28 @@ const meToast: MeToast = {
|
|||||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||||
this.i18n.clearLocaleListeners();
|
this.i18n.clearLocaleListeners();
|
||||||
}
|
}
|
||||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
if (this.animations && typeof this.animations.destroy === 'function') {
|
||||||
this.animations.cancelAll();
|
this.animations.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理 DOM
|
// 清理 DOM 与静态资源
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const containers = document.querySelectorAll('.met-container');
|
const containers = document.querySelectorAll('.met-container');
|
||||||
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||||
|
|
||||||
const style = document.getElementById('metona-toast-styles');
|
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
||||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
const el = document.getElementById(id);
|
||||||
|
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this._toasts.clear();
|
this._toasts.clear();
|
||||||
_containerCache.clear();
|
_containerCache.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
|
||||||
|
// afterDestroy 必须在清理 hooks 之前触发
|
||||||
|
Toast.trigger('afterDestroy');
|
||||||
|
Toast._hooks.clear();
|
||||||
},
|
},
|
||||||
|
|
||||||
// 子模块引用(由 index.ts 注入实际实现)
|
// 子模块引用(由 index.ts 注入实际实现)
|
||||||
|
|||||||
+7
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Constants — 常量定义
|
* MetonaToast Constants — 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ICONS } from './icons.js';
|
import { ICONS } from './icons.js';
|
||||||
@@ -9,6 +9,11 @@ import { LOCALES } from './locales.js';
|
|||||||
|
|
||||||
export { ICONS, LOCALES };
|
export { ICONS, LOCALES };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本号 — 唯一来源,发布时只需修改此处
|
||||||
|
*/
|
||||||
|
export const VERSION = '0.5.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认配置
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
@@ -21,6 +26,7 @@ export const DEFAULTS = Object.freeze({
|
|||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
dragThreshold: 120,
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
progressDirection: 'horizontal' as const,
|
progressDirection: 'horizontal' as const,
|
||||||
icon: true,
|
icon: true,
|
||||||
|
|||||||
+7
-7
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast i18n — 国际化管理
|
* MetonaToast i18n — 国际化管理
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LOCALES } from './constants.js';
|
import { LOCALES } from './constants.js';
|
||||||
@@ -405,7 +405,7 @@ export const clearLocaleListeners = (): void => {
|
|||||||
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
|
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return number.toString();
|
return number.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -420,7 +420,7 @@ export const formatCurrency = (amount: number, currency = 'USD', options: Intl.N
|
|||||||
currency,
|
currency,
|
||||||
...options,
|
...options,
|
||||||
}).format(amount);
|
}).format(amount);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return amount.toString();
|
return amount.toString();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -436,7 +436,7 @@ export const formatPercent = (value: number, options: Intl.NumberFormatOptions =
|
|||||||
maximumFractionDigits: 2,
|
maximumFractionDigits: 2,
|
||||||
...options,
|
...options,
|
||||||
}).format(value / 100);
|
}).format(value / 100);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return `${value}%`;
|
return `${value}%`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -448,7 +448,7 @@ export const formatDate = (date: Date | number | string, options: Intl.DateTimeF
|
|||||||
try {
|
try {
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -497,7 +497,7 @@ export const formatRelativeTime = (date: Date | number | string, options: Intl.R
|
|||||||
}
|
}
|
||||||
|
|
||||||
return String(date);
|
return String(date);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return String(date);
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -524,7 +524,7 @@ export const formatList = (list: string[], _options: Record<string, unknown> = {
|
|||||||
export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => {
|
export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => {
|
||||||
try {
|
try {
|
||||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||||
} catch (e) {
|
} catch {
|
||||||
return count === 1 ? 'one' : 'other';
|
return count === 1 ? 'one' : 'other';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Icons — 图标SVG定义
|
* MetonaToast Icons — 图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 107 个内置 SVG 图标
|
* @description 107 个内置 SVG 图标
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+11
-5
@@ -1,22 +1,20 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast — 轻量级Toast通知库
|
* MetonaToast — 轻量级Toast通知库
|
||||||
* @module metona-toast
|
* @module metona-toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @author thzxx
|
* @author thzxx
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { meToast } from './api.js';
|
import { meToast } from './api.js';
|
||||||
import { Toast, _containerCache } from './toast.js';
|
import { Toast } from './toast.js';
|
||||||
import { animationUtils } from './animations.js';
|
import { animationUtils } from './animations.js';
|
||||||
import { themeUtils } from './themes.js';
|
import { themeUtils } from './themes.js';
|
||||||
import { i18nUtils } from './i18n.js';
|
import { i18nUtils } from './i18n.js';
|
||||||
import { pluginUtils, presetPlugins } from './plugins.js';
|
import { pluginUtils, presetPlugins } from './plugins.js';
|
||||||
|
import { VERSION } from './constants.js';
|
||||||
import type { ToastInstance } from './types.js';
|
import type { ToastInstance } from './types.js';
|
||||||
|
|
||||||
// 版本信息
|
|
||||||
const VERSION = '0.2.1';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||||
*/
|
*/
|
||||||
@@ -40,6 +38,14 @@ if (typeof i18nUtils.initI18n === 'function') {
|
|||||||
i18nUtils.initI18n();
|
i18nUtils.initI18n();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
||||||
|
if (typeof themeUtils.addThemeListener === 'function') {
|
||||||
|
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
||||||
|
}
|
||||||
|
if (typeof i18nUtils.addLocaleListener === 'function') {
|
||||||
|
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
||||||
|
}
|
||||||
|
|
||||||
// 浏览器环境全局注册
|
// 浏览器环境全局注册
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
window.MeToast = enhancedMeToast;
|
window.MeToast = enhancedMeToast;
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Locales — 国际化翻译数据
|
* MetonaToast Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description 内置 zh-CN / en-US 完整翻译
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+48
-2
@@ -1,11 +1,12 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Plugins — 插件系统
|
* MetonaToast Plugins — 插件系统
|
||||||
* @module plugins
|
* @module plugins
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
import type { Plugin, PluginManager as IPluginManager, PluginUtils } from './types.js';
|
import { Toast } from './toast.js';
|
||||||
|
import type { Plugin, PluginManager as IPluginManager, PluginUtils, ToastInstance } from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 插件管理器
|
* 插件管理器
|
||||||
@@ -153,6 +154,12 @@ const presetPlugins: Record<string, Plugin> = {
|
|||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
} catch (_e) { /* noop */ }
|
} catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
|
// 卸载 use() 注册的配置保存钩子
|
||||||
|
const meta = this as Record<string, unknown>;
|
||||||
|
if (typeof meta._saveOff === 'function') {
|
||||||
|
(meta._saveOff as () => void)();
|
||||||
|
meta._saveOff = null;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -175,6 +182,45 @@ const presetPlugins: Record<string, Plugin> = {
|
|||||||
document.body.appendChild(el);
|
document.body.appendChild(el);
|
||||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
uninstall(this: Plugin) {
|
||||||
|
// 卸载 use() 注册的朗读钩子
|
||||||
|
const meta = this as Record<string, unknown>;
|
||||||
|
if (typeof meta._off === 'function') {
|
||||||
|
(meta._off as () => void)();
|
||||||
|
meta._off = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||||
|
*/
|
||||||
|
dedupe: {
|
||||||
|
name: 'dedupe',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||||
|
_off: null as (() => void) | null,
|
||||||
|
|
||||||
|
install(this: Plugin) {
|
||||||
|
const off = Toast.on('beforeShow', (toast: ToastInstance) => {
|
||||||
|
const existing = Array.from(Toast._registry.values()).find(
|
||||||
|
t => !t.closing && t.type === toast.type && t.message === toast.message
|
||||||
|
);
|
||||||
|
if (existing) {
|
||||||
|
existing.update({ title: toast.title || existing.title });
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
(this as Record<string, unknown>)._off = off;
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall(this: Plugin) {
|
||||||
|
const off = (this as Record<string, unknown>)._off as (() => void) | null;
|
||||||
|
if (off) off();
|
||||||
|
(this as Record<string, unknown>)._off = null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+122
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast React — React 适配器
|
||||||
|
* @module react
|
||||||
|
* @version 0.5.0
|
||||||
|
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
import MeToast from './index.js';
|
||||||
|
import type { ToastInstance, ToastOptions } from './types.js';
|
||||||
|
|
||||||
|
/** 支持声明式渲染的 Toast 类型 */
|
||||||
|
export type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||||
|
|
||||||
|
export interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||||
|
message: string;
|
||||||
|
type?: ReactToastType;
|
||||||
|
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||||
|
autoClose?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'] as const;
|
||||||
|
type ShowMethod = (typeof SHOW_METHODS)[number];
|
||||||
|
|
||||||
|
/** 有 id 的可追踪对象(ToastInstance / LoadingControl) */
|
||||||
|
interface Tracked {
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showToast = (method: ShowMethod, message: string, opts: Omit<ToastProps, 'message' | 'type' | 'autoClose'>): ToastInstance => {
|
||||||
|
switch (method) {
|
||||||
|
case 'success': return MeToast.success(message, opts);
|
||||||
|
case 'error': return MeToast.error(message, opts);
|
||||||
|
case 'warning': return MeToast.warning(message, opts);
|
||||||
|
case 'info': return MeToast.info(message, opts);
|
||||||
|
default: return MeToast.show(message, opts);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||||
|
* 纯函数,便于独立测试与扩展。
|
||||||
|
*/
|
||||||
|
export const createBoundApi = (cleanup: Set<string>): Record<string, unknown> => {
|
||||||
|
const track = <T extends Tracked | null>(fn: () => T): T => {
|
||||||
|
const result = fn();
|
||||||
|
if (result && result.id) cleanup.add(result.id);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bound: Record<string, unknown> = {
|
||||||
|
...MeToast,
|
||||||
|
|
||||||
|
success: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||||
|
track(() => MeToast.success(messageOrOpts, opts)),
|
||||||
|
error: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||||
|
track(() => MeToast.error(messageOrOpts, opts)),
|
||||||
|
warning: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||||
|
track(() => MeToast.warning(messageOrOpts, opts)),
|
||||||
|
info: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||||
|
track(() => MeToast.info(messageOrOpts, opts)),
|
||||||
|
show: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||||
|
track(() => MeToast.show(messageOrOpts, opts)),
|
||||||
|
loading: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||||
|
track(() => MeToast.loading(messageOrOpts, opts)),
|
||||||
|
|
||||||
|
// 通过 id 关闭时同步清理集合,避免卸载时重复移除
|
||||||
|
dismiss: (id?: string) => {
|
||||||
|
if (id) cleanup.delete(id);
|
||||||
|
return MeToast.dismiss(id);
|
||||||
|
},
|
||||||
|
removeToast: (id: string) => {
|
||||||
|
cleanup.delete(id);
|
||||||
|
return MeToast.removeToast(id);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return bound;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||||
|
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||||
|
*/
|
||||||
|
export const useToast = (): Record<string, unknown> => {
|
||||||
|
const cleanupRef = useRef<Set<string>>(new Set());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const set = cleanupRef.current;
|
||||||
|
return () => {
|
||||||
|
set.forEach(id => MeToast.removeToast(id));
|
||||||
|
set.clear();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return useMemo(() => createBoundApi(cleanupRef.current), []);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||||
|
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||||
|
*/
|
||||||
|
export const Toast = (props: ToastProps): null => {
|
||||||
|
const { message, type, autoClose = true, ...rest } = props;
|
||||||
|
const toastRef = useRef<ToastInstance | null>(null);
|
||||||
|
const restKey = JSON.stringify(rest);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const method: ShowMethod = SHOW_METHODS.includes(type as ShowMethod) ? (type as ShowMethod) : 'show';
|
||||||
|
toastRef.current = showToast(method, message, rest);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (autoClose) toastRef.current?.remove();
|
||||||
|
toastRef.current = null;
|
||||||
|
};
|
||||||
|
// autoClose 不参与重建依赖:它只影响卸载时的清理行为
|
||||||
|
}, [message, type, restKey]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { MeToast };
|
||||||
+41
-16
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Styles — 样式管理
|
* MetonaToast Styles — 样式管理
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { ThemeConfig } from './types.js';
|
import type { ThemeConfig } from './types.js';
|
||||||
@@ -363,6 +363,41 @@ const generateCSS = (): string => {
|
|||||||
transition: transform 0.1s linear;
|
transition: transform 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||||
|
[dir="rtl"] .met-bar {
|
||||||
|
transform-origin: right;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-side {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-progress-v {
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-success {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #10b981;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-error {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #ef4444;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-warning {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #f59e0b;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-info {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #3b82f6;
|
||||||
|
}
|
||||||
|
[dir="rtl"] .met-toast.met-loading {
|
||||||
|
border-left-width: 1px;
|
||||||
|
border-right: 4px solid #6366f1;
|
||||||
|
}
|
||||||
|
|
||||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
.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 { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||||
@@ -506,6 +541,11 @@ export const generateThemeVariables = (theme: ThemeConfig): string => {
|
|||||||
export const applyThemeVariables = (theme: ThemeConfig): void => {
|
export const applyThemeVariables = (theme: ThemeConfig): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
|
const oldStyle = document.getElementById('metona-toast-custom-styles');
|
||||||
|
if (oldStyle && oldStyle.parentNode) {
|
||||||
|
oldStyle.parentNode.removeChild(oldStyle);
|
||||||
|
}
|
||||||
|
|
||||||
const css = generateThemeVariables(theme);
|
const css = generateThemeVariables(theme);
|
||||||
const customStyle = document.createElement('style');
|
const customStyle = document.createElement('style');
|
||||||
customStyle.id = 'metona-toast-custom-styles';
|
customStyle.id = 'metona-toast-custom-styles';
|
||||||
@@ -555,18 +595,3 @@ export const watchSystemTheme = (callback: (theme: string) => void): (() => void
|
|||||||
mediaQuery.removeEventListener('change', handler);
|
mediaQuery.removeEventListener('change', handler);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 自动应用系统主题
|
|
||||||
*/
|
|
||||||
export const autoApplySystemTheme = (): (() => void) => {
|
|
||||||
const applyTheme = (theme: string): void => {
|
|
||||||
updateStyles();
|
|
||||||
// _THEMES reference is from constants, but here we just need to use theme string
|
|
||||||
// applyThemeVariables is called externally
|
|
||||||
};
|
|
||||||
|
|
||||||
applyTheme(getSystemTheme());
|
|
||||||
|
|
||||||
return watchSystemTheme(applyTheme);
|
|
||||||
};
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Templates — HTML 模板辅助函数
|
* MetonaToast Templates — HTML 模板辅助函数
|
||||||
* @module templates
|
* @module templates
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Themes — 主题管理
|
* MetonaToast Themes — 主题管理
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { THEMES } from './constants.js';
|
import { THEMES } from './constants.js';
|
||||||
|
|||||||
+78
-19
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Toast — Toast 类
|
* MetonaToast Toast — Toast 类
|
||||||
* @module toast
|
* @module toast
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { generateId, escapeHTML } from './utils.js';
|
import { generateId, escapeHTML } from './utils.js';
|
||||||
@@ -9,6 +9,7 @@ import { DEFAULTS, ICONS, TYPE_COLORS, THEMES } from './constants.js';
|
|||||||
import { injectStyles } from './styles.js';
|
import { injectStyles } from './styles.js';
|
||||||
import { getTheme } from './themes.js';
|
import { getTheme } from './themes.js';
|
||||||
import { t, getLocaleDirection, getCurrentLocale } from './i18n.js';
|
import { t, getLocaleDirection, getCurrentLocale } from './i18n.js';
|
||||||
|
import { hasAnimation, ensureAnimationCSS } from './animations.js';
|
||||||
import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js';
|
import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,6 +19,9 @@ export class Toast implements ToastInstance {
|
|||||||
// 静态钩子系统
|
// 静态钩子系统
|
||||||
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>> = new Map();
|
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>> = new Map();
|
||||||
|
|
||||||
|
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
||||||
|
static _registry: Map<string, Toast> = new Map();
|
||||||
|
|
||||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||||
private static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
private static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
||||||
private static _removeToast: ((id: string) => void) | null = null;
|
private static _removeToast: ((id: string) => void) | null = null;
|
||||||
@@ -36,31 +40,38 @@ export class Toast implements ToastInstance {
|
|||||||
static on(name: string, fn: (toast: Toast) => boolean | void): () => void {
|
static on(name: string, fn: (toast: Toast) => boolean | void): () => void {
|
||||||
if (!this._hooks.has(name)) this._hooks.set(name, []);
|
if (!this._hooks.has(name)) this._hooks.set(name, []);
|
||||||
this._hooks.get(name)!.push(fn);
|
this._hooks.get(name)!.push(fn);
|
||||||
return () => this.off(name, fn);
|
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||||
|
return () => Toast.off(name, fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
static off(name: string, fn: (toast: Toast) => boolean | void): void {
|
static off(name: string, fn: (toast: Toast) => boolean | void): void {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (list) this._hooks.set(name, list.filter(f => f !== fn));
|
if (list) {
|
||||||
|
const next = list.filter(f => f !== fn);
|
||||||
|
// 空数组直接删除 key,避免残留
|
||||||
|
if (next.length === 0) this._hooks.delete(name);
|
||||||
|
else this._hooks.set(name, next);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||||
|
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||||
*/
|
*/
|
||||||
static trigger(name: string, toast: Toast): boolean {
|
static trigger(name: string, toast: Toast | null = null): boolean {
|
||||||
const list = this._hooks.get(name);
|
const list = this._hooks.get(name);
|
||||||
if (!list || list.length === 0) return true;
|
if (!list || list.length === 0) return true;
|
||||||
|
|
||||||
let allow = true;
|
let allow = true;
|
||||||
list.forEach(fn => {
|
list.forEach(fn => {
|
||||||
try {
|
try {
|
||||||
const result = fn(toast);
|
const result = fn(toast as Toast);
|
||||||
if (result === false) allow = false;
|
if (result === false) allow = false;
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('Hook error:', name, e);
|
console.error('Hook error:', name, e);
|
||||||
if (Toast._onError) {
|
if (Toast._onError) {
|
||||||
try { Toast._onError({ hook: name, error: e as Error, toast }); } catch (_e) { /* noop */ }
|
try { Toast._onError({ hook: name, error: e as Error, toast: toast as ToastInstance }); } catch (_e) { /* noop */ }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -124,6 +135,7 @@ export class Toast implements ToastInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
injectStyles();
|
injectStyles();
|
||||||
|
ensureAnimationCSS(this.config.animation || 'slide');
|
||||||
|
|
||||||
const container = this._getContainer();
|
const container = this._getContainer();
|
||||||
const { theme, c, t } = this._palette();
|
const { theme, c, t } = this._palette();
|
||||||
@@ -148,6 +160,8 @@ export class Toast implements ToastInstance {
|
|||||||
container.appendChild(el);
|
container.appendChild(el);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toast._registry.set(this.id, this);
|
||||||
|
|
||||||
this._bindEvents(el);
|
this._bindEvents(el);
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
|
|
||||||
@@ -196,6 +210,8 @@ export class Toast implements ToastInstance {
|
|||||||
el.className = `met-container ${position}`;
|
el.className = `met-container ${position}`;
|
||||||
el.setAttribute('aria-label', 'Notifications');
|
el.setAttribute('aria-label', 'Notifications');
|
||||||
el.setAttribute('role', 'region');
|
el.setAttribute('role', 'region');
|
||||||
|
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||||
|
if (isRTL) el.setAttribute('dir', 'rtl');
|
||||||
|
|
||||||
const posStyles: Record<string, string> = {
|
const posStyles: Record<string, string> = {
|
||||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||||
@@ -227,19 +243,21 @@ export class Toast implements ToastInstance {
|
|||||||
_limitToasts(container: HTMLElement): void {
|
_limitToasts(container: HTMLElement): void {
|
||||||
const max = this.config.max || 6;
|
const max = this.config.max || 6;
|
||||||
const list = Array.from(container.querySelectorAll('.met-toast'));
|
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||||
if (list.length >= max) {
|
while (list.length >= max) {
|
||||||
const first = list[0] as HTMLElement;
|
const el = list.shift() as HTMLElement;
|
||||||
const id = first?.dataset.id;
|
const id = el?.dataset.id;
|
||||||
if (id && Toast._removeToast) {
|
if (!id) continue;
|
||||||
|
const existing = Toast._registry.get(id);
|
||||||
|
if (existing) {
|
||||||
|
existing.close(true);
|
||||||
|
} else if (Toast._removeToast) {
|
||||||
Toast._removeToast(id);
|
Toast._removeToast(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_buildClassName(theme: string): string {
|
_buildClassName(theme: string): string {
|
||||||
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
|
const anim = hasAnimation(this.config.animation || '') ? (this.config.animation as string) : 'slide';
|
||||||
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
|
||||||
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
|
|
||||||
return [
|
return [
|
||||||
'met-toast',
|
'met-toast',
|
||||||
`met-${this.type}`,
|
`met-${this.type}`,
|
||||||
@@ -327,6 +345,8 @@ export class Toast implements ToastInstance {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Toast.trigger('click', this);
|
||||||
|
|
||||||
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||||
if (typeof this.config.onClick === 'function') {
|
if (typeof this.config.onClick === 'function') {
|
||||||
try {
|
try {
|
||||||
@@ -344,8 +364,14 @@ export class Toast implements ToastInstance {
|
|||||||
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||||
|
|
||||||
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||||
const mouseEnter = () => this._pause();
|
const mouseEnter = () => {
|
||||||
const mouseLeave = () => this._resume();
|
Toast.trigger('hover', this);
|
||||||
|
this._pause();
|
||||||
|
};
|
||||||
|
const mouseLeave = () => {
|
||||||
|
Toast.trigger('hover', this);
|
||||||
|
this._resume();
|
||||||
|
};
|
||||||
|
|
||||||
el.addEventListener('mouseenter', mouseEnter);
|
el.addEventListener('mouseenter', mouseEnter);
|
||||||
el.addEventListener('mouseleave', mouseLeave);
|
el.addEventListener('mouseleave', mouseLeave);
|
||||||
@@ -356,6 +382,16 @@ export class Toast implements ToastInstance {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
||||||
|
const animStart = () => Toast.trigger('animationStart', this);
|
||||||
|
const animEnd = () => Toast.trigger('animationEnd', this);
|
||||||
|
el.addEventListener('animationstart', animStart);
|
||||||
|
el.addEventListener('animationend', animEnd);
|
||||||
|
this._cleanups.push(() => {
|
||||||
|
el.removeEventListener('animationstart', animStart);
|
||||||
|
el.removeEventListener('animationend', animEnd);
|
||||||
|
});
|
||||||
|
|
||||||
if (this.config.draggable) {
|
if (this.config.draggable) {
|
||||||
this._bindDrag(el);
|
this._bindDrag(el);
|
||||||
}
|
}
|
||||||
@@ -372,6 +408,7 @@ export class Toast implements ToastInstance {
|
|||||||
el.setPointerCapture(e.pointerId);
|
el.setPointerCapture(e.pointerId);
|
||||||
el.style.transition = 'none';
|
el.style.transition = 'none';
|
||||||
this._pause();
|
this._pause();
|
||||||
|
Toast.trigger('dragStart', this);
|
||||||
};
|
};
|
||||||
|
|
||||||
const move = (e: PointerEvent) => {
|
const move = (e: PointerEvent) => {
|
||||||
@@ -388,7 +425,8 @@ export class Toast implements ToastInstance {
|
|||||||
el.releasePointerCapture(e.pointerId);
|
el.releasePointerCapture(e.pointerId);
|
||||||
el.style.transition = '';
|
el.style.transition = '';
|
||||||
|
|
||||||
if (Math.abs(dx) > 120) {
|
const threshold = this.config.dragThreshold ?? 120;
|
||||||
|
if (Math.abs(dx) > threshold) {
|
||||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||||
el.style.opacity = '0';
|
el.style.opacity = '0';
|
||||||
setTimeout(() => this.close(true), 250);
|
setTimeout(() => this.close(true), 250);
|
||||||
@@ -398,6 +436,7 @@ export class Toast implements ToastInstance {
|
|||||||
this._resume();
|
this._resume();
|
||||||
}
|
}
|
||||||
dx = dy = 0;
|
dx = dy = 0;
|
||||||
|
Toast.trigger('dragEnd', this);
|
||||||
};
|
};
|
||||||
|
|
||||||
el.addEventListener('pointerdown', down);
|
el.addEventListener('pointerdown', down);
|
||||||
@@ -419,6 +458,7 @@ export class Toast implements ToastInstance {
|
|||||||
if (!resuming) {
|
if (!resuming) {
|
||||||
this.startedAt = Date.now();
|
this.startedAt = Date.now();
|
||||||
this.remaining = this.config.duration || 0;
|
this.remaining = this.config.duration || 0;
|
||||||
|
Toast.trigger('progressStart', this);
|
||||||
}
|
}
|
||||||
|
|
||||||
const tick = (): void => {
|
const tick = (): void => {
|
||||||
@@ -436,6 +476,7 @@ export class Toast implements ToastInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.remaining <= 0) {
|
if (this.remaining <= 0) {
|
||||||
|
Toast.trigger('progressEnd', this);
|
||||||
this.close();
|
this.close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -467,7 +508,9 @@ export class Toast implements ToastInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update(partial: Partial<ToastOptions>): this {
|
update(partial: Partial<ToastOptions>): this {
|
||||||
Toast.trigger('beforeUpdate', this);
|
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
||||||
|
if (!Toast.trigger('beforeUpdate', this)) return this;
|
||||||
|
|
||||||
const typeChanged = partial.type && partial.type !== this.type;
|
const typeChanged = partial.type && partial.type !== this.type;
|
||||||
if (partial.type) this.type = partial.type;
|
if (partial.type) this.type = partial.type;
|
||||||
if (partial.title !== undefined) this.title = partial.title;
|
if (partial.title !== undefined) this.title = partial.title;
|
||||||
@@ -501,6 +544,18 @@ export class Toast implements ToastInstance {
|
|||||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||||
|
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||||
|
this.config.duration = partial.duration;
|
||||||
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||||
|
this.rafId = null;
|
||||||
|
this.remaining = Math.max(0, partial.duration);
|
||||||
|
if (partial.duration > 0 && !this.paused) {
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||||
@@ -559,14 +614,17 @@ export class Toast implements ToastInstance {
|
|||||||
}
|
}
|
||||||
this.el = null;
|
this.el = null;
|
||||||
|
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
close(immediate = false): void {
|
close(immediate = false): void {
|
||||||
if (this.closing) return;
|
if (this.closing) return;
|
||||||
this.closing = true;
|
|
||||||
|
|
||||||
Toast.trigger('beforeClose', this);
|
// beforeClose 钩子 — 返回 false 可阻止关闭
|
||||||
|
if (!Toast.trigger('beforeClose', this)) return;
|
||||||
|
|
||||||
|
this.closing = true;
|
||||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||||
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
||||||
this._cleanups = [];
|
this._cleanups = [];
|
||||||
@@ -622,6 +680,7 @@ export class Toast implements ToastInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Toast.trigger('afterClose', this);
|
Toast.trigger('afterClose', this);
|
||||||
|
Toast._registry.delete(this.id);
|
||||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-5
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast — 核心类型定义
|
* MetonaToast — 核心类型定义
|
||||||
* @module types
|
* @module types
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// ========== 基础类型 ==========
|
// ========== 基础类型 ==========
|
||||||
@@ -72,6 +72,7 @@ export interface ToastConfig {
|
|||||||
pauseOnHover?: boolean;
|
pauseOnHover?: boolean;
|
||||||
closeOnClick?: boolean;
|
closeOnClick?: boolean;
|
||||||
draggable?: boolean;
|
draggable?: boolean;
|
||||||
|
dragThreshold?: number;
|
||||||
showProgress?: boolean;
|
showProgress?: boolean;
|
||||||
progressDirection?: ToastProgressDirection;
|
progressDirection?: ToastProgressDirection;
|
||||||
icon?: boolean;
|
icon?: boolean;
|
||||||
@@ -271,8 +272,8 @@ export interface StatusInfo {
|
|||||||
// ========== 动画 ==========
|
// ========== 动画 ==========
|
||||||
|
|
||||||
export interface AnimationConfig {
|
export interface AnimationConfig {
|
||||||
enter: Record<string, string>;
|
enter: Record<string, string | number>;
|
||||||
leave: Record<string, string>;
|
leave: Record<string, string | number>;
|
||||||
duration: number;
|
duration: number;
|
||||||
easing: string;
|
easing: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -538,10 +539,8 @@ export const HOOK_NAMES = {
|
|||||||
ANIMATION_START: 'animationStart',
|
ANIMATION_START: 'animationStart',
|
||||||
ANIMATION_END: 'animationEnd',
|
ANIMATION_END: 'animationEnd',
|
||||||
PROGRESS_START: 'progressStart',
|
PROGRESS_START: 'progressStart',
|
||||||
PROGRESS_UPDATE: 'progressUpdate',
|
|
||||||
PROGRESS_END: 'progressEnd',
|
PROGRESS_END: 'progressEnd',
|
||||||
ERROR: 'error',
|
ERROR: 'error',
|
||||||
CUSTOM: 'custom',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES];
|
export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES];
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Utils — 工具函数
|
* MetonaToast Utils — 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+505
-63
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast 覆盖率补充测试 — 目标 95%+
|
* MetonaToast 覆盖率补充测试 — 目标 95%+
|
||||||
* @module tests
|
* @module tests
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MeToast, { Toast, VERSION } from '../src/index';
|
import MeToast, { Toast, VERSION } from '../src/index';
|
||||||
@@ -139,6 +139,70 @@ describe('animations.ts 覆盖率', () => {
|
|||||||
test('getActiveCount 返回动画数量', () => {
|
test('getActiveCount 返回动画数量', () => {
|
||||||
expect(MeToast.animations.getActiveCount()).toBeGreaterThan(0);
|
expect(MeToast.animations.getActiveCount()).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('ensureAnimationCSS 内置动画不注入', () => {
|
||||||
|
const { ensureAnimationCSS } = require('../src/animations.js');
|
||||||
|
// jsdom 真实 DOM:内置动画由 styles.ts 统一注入,不应产生独立 style 标签
|
||||||
|
ensureAnimationCSS('slide');
|
||||||
|
expect(document.getElementById('metona-toast-anim-styles')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensureAnimationCSS 自定义动画注入样式(幂等)', () => {
|
||||||
|
const { ensureAnimationCSS } = require('../src/animations.js');
|
||||||
|
MeToast.animations.destroy(); // 清理模块注入状态,保证测试独立
|
||||||
|
MeToast.animations.register('my-anim', {
|
||||||
|
enter: { opacity: 0, transform: 'scale(0.5)' },
|
||||||
|
leave: { opacity: 1 },
|
||||||
|
duration: 400,
|
||||||
|
easing: 'ease',
|
||||||
|
});
|
||||||
|
ensureAnimationCSS('my-anim');
|
||||||
|
const styleEl = document.getElementById('metona-toast-anim-styles');
|
||||||
|
expect(styleEl).not.toBeNull();
|
||||||
|
expect(styleEl!.textContent).toContain('@keyframes met-my-anim-in');
|
||||||
|
expect(styleEl!.textContent).toContain('.met-anim-my-anim.met-toast.met-show');
|
||||||
|
// 幂等:二次调用不重复追加
|
||||||
|
const before = styleEl!.textContent!.length;
|
||||||
|
ensureAnimationCSS('my-anim');
|
||||||
|
expect(styleEl!.textContent!.length).toBe(before);
|
||||||
|
MeToast.animations.unregister('my-anim');
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ensureAnimationCSS 未注册动画不注入', () => {
|
||||||
|
const { ensureAnimationCSS } = require('../src/animations.js');
|
||||||
|
// 清理 jsdom 中可能残留的动态 style 元素
|
||||||
|
document.getElementById('metona-toast-anim-styles')?.remove();
|
||||||
|
ensureAnimationCSS('not-registered');
|
||||||
|
expect(document.getElementById('metona-toast-anim-styles')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hasAnimation 检查内置与自定义动画', () => {
|
||||||
|
const { hasAnimation } = require('../src/animations.js');
|
||||||
|
expect(hasAnimation('slide')).toBe(true);
|
||||||
|
expect(hasAnimation('nonexistent')).toBe(false);
|
||||||
|
MeToast.animations.register('has-anim-test', { enter: {}, leave: {}, duration: 300 });
|
||||||
|
expect(hasAnimation('has-anim-test')).toBe(true);
|
||||||
|
MeToast.animations.unregister('has-anim-test');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('自定义动画注册后 _buildClassName 使用该动画', () => {
|
||||||
|
MeToast.animations.register('custom-anim', {
|
||||||
|
enter: { opacity: 0 },
|
||||||
|
leave: { opacity: 1 },
|
||||||
|
duration: 400,
|
||||||
|
easing: 'ease',
|
||||||
|
});
|
||||||
|
const t = MeToast.info({ message: 'custom', animation: 'custom-anim' });
|
||||||
|
expect(t.el?.className).toContain('met-anim-custom-anim');
|
||||||
|
MeToast.animations.unregister('custom-anim');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未注册动画名 fallback 到 slide', () => {
|
||||||
|
const t = new Toast({ message: 'test', animation: 'unknown-anim' });
|
||||||
|
t.create();
|
||||||
|
expect(t.el?.className).toContain('met-anim-slide');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
@@ -745,7 +809,7 @@ describe('api.ts 覆盖率', () => {
|
|||||||
|
|
||||||
test('getStatus 返回完整状态', () => {
|
test('getStatus 返回完整状态', () => {
|
||||||
const status = MeToast.getStatus();
|
const status = MeToast.getStatus();
|
||||||
expect(status.version).toBe('0.2.1');
|
expect(status.version).toBe('0.5.0');
|
||||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||||
expect(status.theme).toBeDefined();
|
expect(status.theme).toBeDefined();
|
||||||
expect(status.locale).toBeDefined();
|
expect(status.locale).toBeDefined();
|
||||||
@@ -1016,41 +1080,11 @@ describe('styles.ts 覆盖率', () => {
|
|||||||
expect(typeof unsub).toBe('function');
|
expect(typeof unsub).toBe('function');
|
||||||
unsub();
|
unsub();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('autoApplySystemTheme 返回取消函数', () => {
|
|
||||||
const unsub = styles.autoApplySystemTheme();
|
|
||||||
expect(typeof unsub).toBe('function');
|
|
||||||
unsub();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// 9. utils.ts 覆盖率补充
|
// 9. utils.ts 覆盖率补充(SSR 分支见 tests/ssr.test.ts — 独立环境隔离)
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
describe('utils.ts 覆盖率', () => {
|
|
||||||
test('escapeHTML SSR 路径(无 document)', () => {
|
|
||||||
// 需要绕过 mock document
|
|
||||||
const originalDoc = (global as Record<string, unknown>).document;
|
|
||||||
(global as Record<string, unknown>).document = undefined;
|
|
||||||
jest.resetModules();
|
|
||||||
const { escapeHTML } = require('../src/utils.js');
|
|
||||||
const result = escapeHTML('<script>alert("xss")</script>');
|
|
||||||
expect(result).not.toContain('<script>');
|
|
||||||
expect(result).toContain('<script>');
|
|
||||||
(global as Record<string, unknown>).document = originalDoc;
|
|
||||||
jest.resetModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('prefersDark 无 window 时返回 false', () => {
|
|
||||||
const originalWindow = (global as Record<string, unknown>).window;
|
|
||||||
(global as Record<string, unknown>).window = undefined;
|
|
||||||
jest.resetModules();
|
|
||||||
const { prefersDark } = require('../src/utils.js');
|
|
||||||
expect(prefersDark()).toBe(false);
|
|
||||||
(global as Record<string, unknown>).window = originalWindow;
|
|
||||||
jest.resetModules();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ==================================================================
|
// ==================================================================
|
||||||
// 10. 最终补漏 — 针对性覆盖剩余未覆盖行
|
// 10. 最终补漏 — 针对性覆盖剩余未覆盖行
|
||||||
@@ -1213,17 +1247,7 @@ describe('最终补漏', () => {
|
|||||||
unsub();
|
unsub();
|
||||||
});
|
});
|
||||||
|
|
||||||
// === themes.ts localStorage 异常路径 ===
|
// === themes.ts localStorage 异常路径(详见 tests/ssr.test.ts) ===
|
||||||
test('saveTheme localStorage 异常被捕获', () => {
|
|
||||||
const orig = (global as Record<string, unknown>).localStorage;
|
|
||||||
(global as Record<string, unknown>).localStorage = undefined;
|
|
||||||
jest.resetModules();
|
|
||||||
const { saveTheme } = require('../src/themes.js');
|
|
||||||
expect(() => saveTheme('dark')).not.toThrow();
|
|
||||||
(global as Record<string, unknown>).localStorage = orig;
|
|
||||||
jest.resetModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('loadTheme/loadLocale 异常路径不抛异常', () => {
|
test('loadTheme/loadLocale 异常路径不抛异常', () => {
|
||||||
const { loadTheme } = require('../src/themes.js');
|
const { loadTheme } = require('../src/themes.js');
|
||||||
const { loadLocale } = require('../src/i18n.js');
|
const { loadLocale } = require('../src/i18n.js');
|
||||||
@@ -1231,25 +1255,6 @@ describe('最终补漏', () => {
|
|||||||
expect(typeof loadLocale()).toBe('string');
|
expect(typeof loadLocale()).toBe('string');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('loadLocale localStorage 异常返回 default', () => {
|
|
||||||
const i18n = require('../src/i18n.js');
|
|
||||||
const orig = (global as Record<string, unknown>).localStorage;
|
|
||||||
(global as Record<string, unknown>).localStorage = {
|
|
||||||
getItem: () => { throw new Error('quota'); },
|
|
||||||
};
|
|
||||||
jest.resetModules();
|
|
||||||
const { loadLocale } = require('../src/i18n.js');
|
|
||||||
expect(typeof loadLocale()).toBe('string');
|
|
||||||
(global as Record<string, unknown>).localStorage = orig;
|
|
||||||
jest.resetModules();
|
|
||||||
});
|
|
||||||
|
|
||||||
// === styles.ts 剩余 ===
|
|
||||||
test('autoApplySystemTheme 内部逻辑不抛异常', () => {
|
|
||||||
const styles = require('../src/styles.js');
|
|
||||||
expect(() => styles.autoApplySystemTheme()).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
// === api.ts prompt 流程 ===
|
// === api.ts prompt 流程 ===
|
||||||
test('prompt 创建后按钮事件绑定', async () => {
|
test('prompt 创建后按钮事件绑定', async () => {
|
||||||
const promise = MeToast.prompt('name?', {
|
const promise = MeToast.prompt('name?', {
|
||||||
@@ -1299,3 +1304,440 @@ describe('最终补漏', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// 11. v0.3.0 修复验证
|
||||||
|
// ==================================================================
|
||||||
|
describe('v0.3.0 修复验证', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.resetConfig();
|
||||||
|
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||||
|
Toast._hooks.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
const { _containerCache } = require('../src/toast.js');
|
||||||
|
_containerCache.clear();
|
||||||
|
// 清理动画模块状态(injected 集合 + 动态 style 引用),再恢复默认动画
|
||||||
|
MeToast.animations.destroy();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_limitToasts 超出 max 时真正关闭最早的 toast', () => {
|
||||||
|
const t1 = MeToast.info('m1');
|
||||||
|
const t2 = MeToast.info('m2');
|
||||||
|
const el = createMockElement() as unknown as HTMLElement;
|
||||||
|
(el as unknown as { querySelectorAll: unknown }).querySelectorAll = jest.fn(() => [
|
||||||
|
{ dataset: { id: t1.id } },
|
||||||
|
{ dataset: { id: t2.id } },
|
||||||
|
]);
|
||||||
|
const newToast = new Toast({ message: 'm3', max: 2 });
|
||||||
|
newToast._limitToasts(el as unknown as HTMLElement);
|
||||||
|
expect(t1.closing).toBe(true);
|
||||||
|
expect(t2.closing).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('beforeClose 钩子返回 false 阻止关闭', () => {
|
||||||
|
const t = MeToast.info('test');
|
||||||
|
const blocker = () => false;
|
||||||
|
Toast.on('beforeClose', blocker);
|
||||||
|
t.close();
|
||||||
|
expect(t.closing).toBe(false);
|
||||||
|
Toast.off('beforeClose', blocker);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('beforeUpdate 钩子返回 false 阻止更新', () => {
|
||||||
|
const t = MeToast.info('原始');
|
||||||
|
const blocker = () => false;
|
||||||
|
Toast.on('beforeUpdate', blocker);
|
||||||
|
t.update({ message: '新消息' });
|
||||||
|
expect(t.message).toBe('原始');
|
||||||
|
Toast.off('beforeUpdate', blocker);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('configure/updateConfig/resetConfig 触发 configChange 钩子', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
Toast.on('configChange', fn);
|
||||||
|
MeToast.configure({ duration: 2500 });
|
||||||
|
MeToast.updateConfig({ gap: 8 });
|
||||||
|
MeToast.resetConfig();
|
||||||
|
expect(fn).toHaveBeenCalledTimes(3);
|
||||||
|
Toast.off('configChange', fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('init 触发 beforeInit/afterInit 钩子', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const before = jest.fn(() => calls.push('beforeInit'));
|
||||||
|
const after = jest.fn(() => calls.push('afterInit'));
|
||||||
|
Toast.on('beforeInit', before);
|
||||||
|
Toast.on('afterInit', after);
|
||||||
|
MeToast.init();
|
||||||
|
expect(calls).toEqual(['beforeInit', 'afterInit']);
|
||||||
|
Toast.off('beforeInit', before);
|
||||||
|
Toast.off('afterInit', after);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy 触发 beforeDestroy/afterDestroy 钩子', () => {
|
||||||
|
const calls: string[] = [];
|
||||||
|
const before = jest.fn(() => calls.push('beforeDestroy'));
|
||||||
|
const after = jest.fn(() => calls.push('afterDestroy'));
|
||||||
|
Toast.on('beforeDestroy', before);
|
||||||
|
Toast.on('afterDestroy', after);
|
||||||
|
MeToast.destroy();
|
||||||
|
expect(calls).toEqual(['beforeDestroy', 'afterDestroy']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy 清理全部钩子', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
Toast.on('afterShow', fn);
|
||||||
|
MeToast.destroy();
|
||||||
|
MeToast.success('after-destroy');
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy 清理实例注册表', () => {
|
||||||
|
MeToast.info('m1');
|
||||||
|
expect(Toast._registry.size).toBe(1);
|
||||||
|
MeToast.destroy();
|
||||||
|
expect(Toast._registry.size).toBe(0);
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('destroy 后 init 恢复可用', () => {
|
||||||
|
MeToast.destroy();
|
||||||
|
MeToast.init({});
|
||||||
|
const t = MeToast.success('revived');
|
||||||
|
expect(t).toBeDefined();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('init plugins 支持插件对象', () => {
|
||||||
|
const install = jest.fn();
|
||||||
|
MeToast.init({ plugins: [{ name: 'obj-plugin', install }] });
|
||||||
|
expect(MeToast.plugins.has('obj-plugin')).toBe(true);
|
||||||
|
expect(install).toHaveBeenCalled();
|
||||||
|
MeToast.plugins.unregister('obj-plugin');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('animate 生命周期钩子 progressStart/progressEnd 触发', (done) => {
|
||||||
|
const start = jest.fn();
|
||||||
|
const end = jest.fn();
|
||||||
|
Toast.on('progressStart', start);
|
||||||
|
Toast.on('progressEnd', end);
|
||||||
|
const t = MeToast.info({ message: 'timed', duration: 30 });
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(start).toHaveBeenCalled();
|
||||||
|
Toast.off('progressStart', start);
|
||||||
|
Toast.off('progressEnd', end);
|
||||||
|
done();
|
||||||
|
}, 120);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// 12. v0.4.0 能力增强验证
|
||||||
|
// ==================================================================
|
||||||
|
describe('v0.4.0 能力增强', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.resetConfig();
|
||||||
|
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||||
|
Toast._hooks.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
const { _containerCache } = require('../src/toast.js');
|
||||||
|
_containerCache.clear();
|
||||||
|
MeToast.animations.destroy();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loading 链式转换 id 稳定(原地 update)', () => {
|
||||||
|
const l = MeToast.loading('正在加载');
|
||||||
|
const idBefore = l.id;
|
||||||
|
const toast = l.success('加载完成');
|
||||||
|
expect(toast).not.toBeNull();
|
||||||
|
expect(toast!.id).toBe(idBefore);
|
||||||
|
expect(toast!.type).toBe('success');
|
||||||
|
expect(toast!.message).toBe('加载完成');
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loading 转换后 duration 恢复默认自动关闭', () => {
|
||||||
|
MeToast.configure({ duration: 4000 });
|
||||||
|
const l = MeToast.loading('loading');
|
||||||
|
const toast = l.error('失败');
|
||||||
|
expect(toast!.config.duration).toBe(4000);
|
||||||
|
MeToast.resetConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loading 转换支持 opts 覆盖 duration', () => {
|
||||||
|
const l = MeToast.loading('loading');
|
||||||
|
const toast = l.warning('警告', { duration: 8000 });
|
||||||
|
expect(toast!.config.duration).toBe(8000);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('update duration 变更重启计时器', () => {
|
||||||
|
const t = MeToast.info('test', { duration: 0 });
|
||||||
|
expect(t.rafId).toBeNull();
|
||||||
|
t.update({ duration: 5000 });
|
||||||
|
expect(t.config.duration).toBe(5000);
|
||||||
|
expect(t.rafId).not.toBeNull();
|
||||||
|
t.update({ duration: 0 });
|
||||||
|
expect(t.config.duration).toBe(0);
|
||||||
|
expect(t.rafId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dragThreshold 配置生效且默认 120', () => {
|
||||||
|
expect(MeToast.getConfig().dragThreshold).toBe(120);
|
||||||
|
const t = MeToast.info('x', { dragThreshold: 200 });
|
||||||
|
expect(t.config.dragThreshold).toBe(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('RTL 容器设置 dir 属性', () => {
|
||||||
|
const { getCurrentLocale, setCurrentLocale, addTranslations } = require('../src/i18n.js');
|
||||||
|
const originalLocale = getCurrentLocale();
|
||||||
|
// 'ar' 需先注册翻译,否则 setCurrentLocale fallback 回默认语言
|
||||||
|
addTranslations('ar', { close: 'إغلاق' });
|
||||||
|
setCurrentLocale('ar');
|
||||||
|
try {
|
||||||
|
const toast = new Toast({ message: 'test', position: 'top-left' });
|
||||||
|
const container = toast._getContainer();
|
||||||
|
expect(container.getAttribute('dir')).toBe('rtl');
|
||||||
|
toast.close(true);
|
||||||
|
} finally {
|
||||||
|
setCurrentLocale(originalLocale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('LTR 容器不设置 dir 属性', () => {
|
||||||
|
const { getCurrentLocale, setCurrentLocale } = require('../src/i18n.js');
|
||||||
|
const originalLocale = getCurrentLocale();
|
||||||
|
setCurrentLocale('zh-CN');
|
||||||
|
try {
|
||||||
|
const toast = new Toast({ message: 'test' });
|
||||||
|
const container = toast._getContainer();
|
||||||
|
expect(container.getAttribute('dir')).toBeNull();
|
||||||
|
toast.close(true);
|
||||||
|
} finally {
|
||||||
|
setCurrentLocale(originalLocale);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dedupe 插件同消息去重', () => {
|
||||||
|
MeToast.use('dedupe');
|
||||||
|
MeToast.info('same message');
|
||||||
|
MeToast.info('same message');
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
MeToast.info('different message');
|
||||||
|
expect(MeToast.count()).toBe(2);
|
||||||
|
MeToast.plugins.unregister('dedupe');
|
||||||
|
// 卸载后不再去重
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.info('same message');
|
||||||
|
MeToast.info('same message');
|
||||||
|
expect(MeToast.count()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dedupe 插件不同 type 不去重', () => {
|
||||||
|
MeToast.use('dedupe');
|
||||||
|
MeToast.info('msg');
|
||||||
|
MeToast.success('msg');
|
||||||
|
expect(MeToast.count()).toBe(2);
|
||||||
|
MeToast.plugins.unregister('dedupe');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dedupe 插件 uninstall 移除钩子', () => {
|
||||||
|
MeToast.use('dedupe');
|
||||||
|
MeToast.plugins.unregister('dedupe');
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.info('m');
|
||||||
|
MeToast.info('m');
|
||||||
|
expect(MeToast.count()).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ==================================================================
|
||||||
|
// 13. v0.5.0 异常路径与边界覆盖
|
||||||
|
// ==================================================================
|
||||||
|
describe('v0.5.0 异常路径与边界', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.resetConfig();
|
||||||
|
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||||
|
Toast._hooks.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
const { _containerCache } = require('../src/toast.js');
|
||||||
|
_containerCache.clear();
|
||||||
|
MeToast.animations.destroy();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('i18n.ts 异常与 fallback', () => {
|
||||||
|
const i18n = require('../src/i18n.js');
|
||||||
|
|
||||||
|
test('t fallback 到 fallbackLocale', () => {
|
||||||
|
i18n.setFallbackLocale('en-US');
|
||||||
|
i18n.removeTranslation('zh-CN', 'retry');
|
||||||
|
expect(i18n.t('retry')).toBe('Retry');
|
||||||
|
i18n.addTranslations('zh-CN', { retry: '重试' });
|
||||||
|
i18n.setFallbackLocale('zh-CN');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeTranslation 中间层级缺失提前返回', () => {
|
||||||
|
i18n.addTranslations('ja', { a: { b: 'x' } });
|
||||||
|
expect(() => i18n.removeTranslation('ja', 'a.nonexistent.c')).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveLocale localStorage 异常被捕获', () => {
|
||||||
|
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||||
|
expect(() => i18n.saveLocale('en-US')).not.toThrow();
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadLocale localStorage 异常返回默认', () => {
|
||||||
|
const spy = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||||
|
expect(typeof i18n.loadLocale()).toBe('string');
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getDefaultLocale 不支持浏览器语言时回退', () => {
|
||||||
|
const orig = Object.getOwnPropertyDescriptor(navigator, 'language');
|
||||||
|
Object.defineProperty(navigator, 'language', { value: 'zz-ZZ', configurable: true });
|
||||||
|
expect(typeof i18n.getDefaultLocale()).toBe('string');
|
||||||
|
if (orig) Object.defineProperty(navigator, 'language', orig);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('locale listener 异常不中断其他监听器', () => {
|
||||||
|
const bad = () => { throw new Error('listener-err'); };
|
||||||
|
const good = jest.fn();
|
||||||
|
i18n.addLocaleListener(bad);
|
||||||
|
i18n.addLocaleListener(good);
|
||||||
|
expect(() => i18n.switchLocale('en-US')).not.toThrow();
|
||||||
|
expect(good).toHaveBeenCalled();
|
||||||
|
i18n.removeLocaleListener(bad);
|
||||||
|
i18n.removeLocaleListener(good);
|
||||||
|
i18n.switchLocale('zh-CN');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Intl 异常时 format 系列降级返回原始值', () => {
|
||||||
|
const origIntl = (global as Record<string, unknown>).Intl;
|
||||||
|
(global as Record<string, unknown>).Intl = new Proxy(origIntl as object, {
|
||||||
|
get: (t: Record<string, unknown>, p: string) => {
|
||||||
|
if (['NumberFormat', 'DateTimeFormat', 'RelativeTimeFormat', 'PluralRules'].includes(p)) {
|
||||||
|
return class { constructor() { throw new RangeError('invalid'); } };
|
||||||
|
}
|
||||||
|
return (t as Record<string, unknown>)[p];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(i18n.formatNumber(1)).toBe('1');
|
||||||
|
expect(i18n.formatCurrency(1)).toBe('1');
|
||||||
|
expect(i18n.formatPercent(50)).toBe('50%');
|
||||||
|
expect(i18n.formatDate('2026-01-01')).toBe('2026-01-01');
|
||||||
|
expect(typeof i18n.formatRelativeTime(Date.now())).toBe('string');
|
||||||
|
expect(i18n.formatPlural(5)).toBe('other');
|
||||||
|
(global as Record<string, unknown>).Intl = origIntl;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatList Intl.ListFormat 缺失时逗号拼接', () => {
|
||||||
|
const origLF = (Intl as unknown as Record<string, unknown>).ListFormat;
|
||||||
|
(Intl as unknown as Record<string, unknown>).ListFormat = undefined;
|
||||||
|
const result = i18n.formatList(['a', 'b']);
|
||||||
|
expect(result).toBe('a, b');
|
||||||
|
(Intl as unknown as Record<string, unknown>).ListFormat = origLF;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('plural 缺失时返回原 key', () => {
|
||||||
|
expect(i18n.plural('missing_plural_key_xyz', 5)).toBe('missing_plural_key_xyz');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('themes.ts 异常与监听', () => {
|
||||||
|
const themes = require('../src/themes.js');
|
||||||
|
|
||||||
|
test('saveTheme/loadTheme localStorage 异常被捕获', () => {
|
||||||
|
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||||
|
expect(() => themes.saveTheme('dark')).not.toThrow();
|
||||||
|
spy.mockRestore();
|
||||||
|
const spy2 = jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => { throw new Error('quota'); });
|
||||||
|
expect(themes.loadTheme()).toBe('auto');
|
||||||
|
spy2.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watchSystemTheme 重复调用先移除旧监听', () => {
|
||||||
|
expect(() => { themes.watchSystemTheme(); themes.watchSystemTheme(); }).not.toThrow();
|
||||||
|
themes.unwatchSystemTheme();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('unwatchSystemTheme 清理监听', () => {
|
||||||
|
themes.watchSystemTheme();
|
||||||
|
expect(() => themes.unwatchSystemTheme()).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('theme listener 异常不中断其他监听器', () => {
|
||||||
|
const bad = () => { throw new Error('theme-listener-err'); };
|
||||||
|
const good = jest.fn();
|
||||||
|
themes.addThemeListener(bad);
|
||||||
|
themes.addThemeListener(good);
|
||||||
|
expect(() => themes.switchTheme('dark')).not.toThrow();
|
||||||
|
expect(good).toHaveBeenCalled();
|
||||||
|
themes.removeThemeListener(bad);
|
||||||
|
themes.removeThemeListener(good);
|
||||||
|
themes.switchTheme('auto');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeThemeCSS 移除已存在样式', () => {
|
||||||
|
themes.applyThemeCSS('dark');
|
||||||
|
expect(() => themes.removeThemeCSS()).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('styles.ts 边界', () => {
|
||||||
|
const styles = require('../src/styles.js');
|
||||||
|
|
||||||
|
test('applyThemeVariables 先移除旧样式再追加', () => {
|
||||||
|
const config = { bg: '#fff', text: '#000', border: '#ccc', shadow: 'none', hoverShadow: 'none', progressBg: '#eee', closeHoverBg: '#ddd' };
|
||||||
|
styles.applyThemeVariables(config);
|
||||||
|
styles.applyThemeVariables(config);
|
||||||
|
const els = document.querySelectorAll('#metona-toast-custom-styles');
|
||||||
|
expect(els.length).toBeLessThanOrEqual(1);
|
||||||
|
styles.clearThemeVariables();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('watchSystemTheme 无 matchMedia 时返回空函数', () => {
|
||||||
|
const orig = (window as unknown as Record<string, unknown>).matchMedia;
|
||||||
|
(window as unknown as Record<string, unknown>).matchMedia = undefined;
|
||||||
|
const unsub = styles.watchSystemTheme(jest.fn());
|
||||||
|
expect(unsub).toBeInstanceOf(Function);
|
||||||
|
(window as unknown as Record<string, unknown>).matchMedia = orig;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('api.ts use 预设插件', () => {
|
||||||
|
test('use persistence 从 localStorage 恢复配置', () => {
|
||||||
|
localStorage.setItem('metona-toast-config', JSON.stringify({ duration: 3000 }));
|
||||||
|
MeToast.use('persistence');
|
||||||
|
expect(MeToast.getConfig().duration).toBe(3000);
|
||||||
|
MeToast.plugins.unregister('persistence');
|
||||||
|
localStorage.removeItem('metona-toast-config');
|
||||||
|
MeToast.resetConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('use accessibility 注册朗读钩子', () => {
|
||||||
|
const announceSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
||||||
|
MeToast.use('accessibility');
|
||||||
|
MeToast.success('announce-test');
|
||||||
|
MeToast.plugins.unregister('accessibility');
|
||||||
|
announceSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('重复 use persistence 不重复注册保存钩子', () => {
|
||||||
|
localStorage.removeItem('metona-toast-config');
|
||||||
|
MeToast.use('persistence');
|
||||||
|
MeToast.use('persistence');
|
||||||
|
MeToast.plugins.unregister('persistence');
|
||||||
|
expect(Toast._hooks.has('afterClose')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -0,0 +1,369 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast DOM 交互测试 — 基于 jsdom 真实 DOM 事件驱动
|
||||||
|
* @module tests
|
||||||
|
* @version 0.5.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import MeToast, { Toast } from '../src/index';
|
||||||
|
import type { ToastInstance } from '../src/types';
|
||||||
|
|
||||||
|
// jsdom polyfill: PointerEvent 相关方法
|
||||||
|
if (typeof Element !== 'undefined') {
|
||||||
|
if (!(Element.prototype as unknown as { setPointerCapture?: unknown }).setPointerCapture) {
|
||||||
|
(Element.prototype as unknown as Record<string, unknown>).setPointerCapture = jest.fn();
|
||||||
|
}
|
||||||
|
if (!(Element.prototype as unknown as { releasePointerCapture?: unknown }).releasePointerCapture) {
|
||||||
|
(Element.prototype as unknown as Record<string, unknown>).releasePointerCapture = jest.fn();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const pointer = (type: string, x: number, y: number): PointerEvent =>
|
||||||
|
new MouseEvent(type, { clientX: x, clientY: y, bubbles: true }) as unknown as PointerEvent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.resetConfig();
|
||||||
|
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||||
|
Toast._hooks.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
const { _containerCache } = require('../src/toast.js');
|
||||||
|
_containerCache.clear();
|
||||||
|
MeToast.animations.destroy();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('toast.ts — DOM 交互', () => {
|
||||||
|
test('onShow 回调异常被捕获', () => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
MeToast.success('x', { onShow: () => { throw new Error('show-err'); } });
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onUpdate 回调异常被捕获', () => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const t = MeToast.success('x', { onUpdate: () => { throw new Error('upd-err'); } });
|
||||||
|
t.update({ message: 'y' });
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_applyStyles 字符串 width 生效', () => {
|
||||||
|
const t = MeToast.info({ message: 'w', width: '80%' });
|
||||||
|
expect(t.el!.style.width).toBe('80%');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('点击 close 按钮关闭 toast 且不触发 click 钩子', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
Toast.on('click', fn);
|
||||||
|
const t = MeToast.info('test');
|
||||||
|
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||||
|
closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
expect(t.closing).toBe(true);
|
||||||
|
expect(fn).not.toHaveBeenCalled();
|
||||||
|
Toast.off('click', fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('点击主体触发 onClick 并关闭', () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const t = MeToast.info('test', { onClick });
|
||||||
|
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
expect(onClick).toHaveBeenCalledWith(t);
|
||||||
|
expect(t.closing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('onClick 回调异常被捕获', () => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const t = MeToast.info('test', { onClick: () => { throw new Error('click-err'); } });
|
||||||
|
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('closeOnClick false 时点击不关闭但触发 click 钩子', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
Toast.on('click', fn);
|
||||||
|
const t = MeToast.info('test', { closeOnClick: false });
|
||||||
|
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
expect(t.closing).toBe(false);
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
Toast.off('click', fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('hover 触发暂停/恢复与 hover 钩子', () => {
|
||||||
|
const fn = jest.fn();
|
||||||
|
Toast.on('hover', fn);
|
||||||
|
const t = MeToast.info({ message: 'h', duration: 5000 });
|
||||||
|
t.el!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||||
|
expect(t.paused).toBe(true);
|
||||||
|
t.el!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
||||||
|
expect(t.paused).toBe(false);
|
||||||
|
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||||
|
Toast.off('hover', fn);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('animationstart/animationend 触发钩子', () => {
|
||||||
|
const start = jest.fn();
|
||||||
|
const end = jest.fn();
|
||||||
|
Toast.on('animationStart', start);
|
||||||
|
Toast.on('animationEnd', end);
|
||||||
|
const t = MeToast.info('a');
|
||||||
|
t.el!.dispatchEvent(new Event('animationstart'));
|
||||||
|
t.el!.dispatchEvent(new Event('animationend'));
|
||||||
|
expect(start).toHaveBeenCalled();
|
||||||
|
expect(end).toHaveBeenCalled();
|
||||||
|
Toast.off('animationStart', start);
|
||||||
|
Toast.off('animationEnd', end);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('拖拽 down→move→up 超过阈值触发关闭', (done) => {
|
||||||
|
const t = MeToast.info('drag', { duration: 5000 });
|
||||||
|
const el = t.el!;
|
||||||
|
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||||
|
el.dispatchEvent(pointer('pointermove', 250, 60));
|
||||||
|
expect(el.style.transform).toContain('translate(150px');
|
||||||
|
expect(el.style.opacity).toBe('0.25');
|
||||||
|
el.dispatchEvent(pointer('pointerup', 250, 60));
|
||||||
|
expect(el.style.opacity).toBe('0');
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(t.closing).toBe(true);
|
||||||
|
done();
|
||||||
|
}, 350);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('拖拽未超阈值松手恢复', () => {
|
||||||
|
const t = MeToast.info('drag2', { duration: 5000 });
|
||||||
|
const el = t.el!;
|
||||||
|
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||||
|
el.dispatchEvent(pointer('pointermove', 150, 60));
|
||||||
|
el.dispatchEvent(pointer('pointerup', 150, 60));
|
||||||
|
expect(t.closing).toBe(false);
|
||||||
|
expect(t.paused).toBe(false);
|
||||||
|
expect(el.style.transform).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('拖拽触发 dragStart/dragEnd 钩子', () => {
|
||||||
|
const start = jest.fn();
|
||||||
|
const end = jest.fn();
|
||||||
|
Toast.on('dragStart', start);
|
||||||
|
Toast.on('dragEnd', end);
|
||||||
|
const t = MeToast.info('d', { duration: 5000 });
|
||||||
|
const el = t.el!;
|
||||||
|
el.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||||
|
el.dispatchEvent(pointer('pointerup', 0, 0));
|
||||||
|
expect(start).toHaveBeenCalled();
|
||||||
|
expect(end).toHaveBeenCalled();
|
||||||
|
Toast.off('dragStart', start);
|
||||||
|
Toast.off('dragEnd', end);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pointercancel 等价于 pointerup', () => {
|
||||||
|
const t = MeToast.info('pc', { duration: 5000 });
|
||||||
|
const el = t.el!;
|
||||||
|
el.dispatchEvent(pointer('pointerdown', 10, 10));
|
||||||
|
el.dispatchEvent(pointer('pointercancel', 10, 10));
|
||||||
|
expect(t.closing).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('拖拽从 close 按钮按下时早退', () => {
|
||||||
|
const t = MeToast.info('dc', { duration: 5000 });
|
||||||
|
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||||
|
closeBtn.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||||
|
expect(t.paused).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('close 非 immediate 走离场动画并清理', (done) => {
|
||||||
|
const t = MeToast.info('anim-close');
|
||||||
|
t.close();
|
||||||
|
expect(t.el!.classList.contains('met-leaving')).toBe(true);
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(t.el).toBeNull();
|
||||||
|
expect(MeToast.count()).toBe(0);
|
||||||
|
done();
|
||||||
|
}, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updatePosition 移动到新容器', () => {
|
||||||
|
const t = MeToast.info('move', { position: 'top-left' });
|
||||||
|
t.updatePosition('bottom-right');
|
||||||
|
expect(t.config.position).toBe('bottom-right');
|
||||||
|
expect(t.el!.parentElement!.className).toContain('bottom-right');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('updatePosition 同位置直接返回', () => {
|
||||||
|
const t = MeToast.info('same');
|
||||||
|
const container = t.el!.parentElement;
|
||||||
|
t.updatePosition('top-right');
|
||||||
|
expect(t.el!.parentElement).toBe(container);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_limitToasts registry 无实例时走 _removeToast 兜底', () => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
const ghost = document.createElement('div');
|
||||||
|
ghost.dataset.id = 'ghost-id';
|
||||||
|
(el.querySelectorAll as unknown) = () => [ghost];
|
||||||
|
const t = new Toast({ message: 'x', max: 0 });
|
||||||
|
expect(() => t._limitToasts(el)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('remove() 清理注册表与内存', () => {
|
||||||
|
const t = MeToast.info('rm');
|
||||||
|
expect(Toast._registry.has(t.id)).toBe(true);
|
||||||
|
t.remove();
|
||||||
|
expect(Toast._registry.has(t.id)).toBe(false);
|
||||||
|
expect(MeToast.count()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('api.ts — 对话框与按钮交互', () => {
|
||||||
|
test('confirm 点击确认按钮 resolve true', async () => {
|
||||||
|
const promise = MeToast.confirm('确定删除?');
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
const toast = MeToast.getToasts()[0];
|
||||||
|
(toast.el!.querySelector('.met-confirm-btn') as HTMLElement).click();
|
||||||
|
const result = await promise;
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('confirm 点击取消按钮 resolve false', async () => {
|
||||||
|
const promise = MeToast.confirm('确定删除?');
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
const toast = MeToast.getToasts()[0];
|
||||||
|
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||||
|
const result = await promise;
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prompt Enter 提交输入值', async () => {
|
||||||
|
const promise = MeToast.prompt('请输入姓名');
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
const toast = MeToast.getToasts()[0];
|
||||||
|
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||||
|
input.value = '张三';
|
||||||
|
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||||
|
const result = await promise;
|
||||||
|
expect(result).toBe('张三');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prompt 点击提交按钮', async () => {
|
||||||
|
const promise = MeToast.prompt('请输入姓名');
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
const toast = MeToast.getToasts()[0];
|
||||||
|
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||||
|
input.value = '李四';
|
||||||
|
(toast.el!.querySelector('.met-submit-btn') as HTMLElement).click();
|
||||||
|
const result = await promise;
|
||||||
|
expect(result).toBe('李四');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prompt 点击取消按钮 resolve null', async () => {
|
||||||
|
const promise = MeToast.prompt('请输入姓名');
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
const toast = MeToast.getToasts()[0];
|
||||||
|
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||||
|
const result = await promise;
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('action 按钮点击触发回调并关闭', async () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const a = MeToast.action('msg', [{ text: 'OK', onClick }]);
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||||
|
expect(onClick).toHaveBeenCalledWith(a.toast);
|
||||||
|
expect(a.toast.closing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('action 按钮 close:false 点击不关闭', async () => {
|
||||||
|
const onClick = jest.fn();
|
||||||
|
const a = MeToast.action('msg', [{ text: 'Keep', onClick, close: false }]);
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||||
|
expect(a.toast.closing).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('action 按钮 onClick 异常被捕获', async () => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const a = MeToast.action('msg', [{ text: 'Boom', onClick: () => { throw new Error('act-err'); } }]);
|
||||||
|
await new Promise(r => setTimeout(r, 10));
|
||||||
|
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('countdown onComplete 异常被捕获', (done) => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
MeToast.countdown('{seconds} 秒', 1, { onComplete: () => { throw new Error('cd-err'); } });
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
done();
|
||||||
|
}, 1600);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loading control 支持 update 与 dismiss', () => {
|
||||||
|
const l = MeToast.loading('l1');
|
||||||
|
l.update({ message: 'l2' });
|
||||||
|
const toast = MeToast.find(l.id);
|
||||||
|
expect(toast!.message).toBe('l2');
|
||||||
|
l.dismiss();
|
||||||
|
expect(toast!.closing).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('group 单对象参数分支', () => {
|
||||||
|
const g = MeToast.group('obj-group');
|
||||||
|
const t = g.show({ message: 'obj-arg' });
|
||||||
|
expect(t.group).toBe('obj-group');
|
||||||
|
expect(t.message).toBe('obj-arg');
|
||||||
|
g.dismiss();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('queue 消息级 onClose 异常被静默捕获', async () => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
const q = MeToast.queue([
|
||||||
|
{ message: 'a', onClose: () => { throw new Error('q-err'); } },
|
||||||
|
], { delay: 5, duration: 10 });
|
||||||
|
await q;
|
||||||
|
expect(consoleError).not.toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('_remove 从内存移除指定 id', () => {
|
||||||
|
const t = MeToast.info('x');
|
||||||
|
MeToast._remove(t.id);
|
||||||
|
expect(MeToast.count()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('plugins.ts — 异常与钩子管理', () => {
|
||||||
|
test('插件 install 抛错被捕获', () => {
|
||||||
|
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
MeToast.plugins.register('bad-install', { name: 'bad-install', install: () => { throw new Error('install-err'); } });
|
||||||
|
expect(consoleError).toHaveBeenCalled();
|
||||||
|
consoleError.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keyboard 插件 install/uninstall 移除事件监听', () => {
|
||||||
|
MeToast.use('keyboard');
|
||||||
|
MeToast.plugins.unregister('keyboard');
|
||||||
|
expect(MeToast.plugins.has('keyboard')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persistence 插件 save 异常被捕获', () => {
|
||||||
|
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||||
|
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||||
|
const preset = pluginUtils.getPreset('persistence');
|
||||||
|
const saveFn = (preset as Record<string, (c: unknown) => void>).save;
|
||||||
|
expect(() => saveFn({ duration: 3000 })).not.toThrow();
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('persistence 插件 uninstall removeItem 异常被捕获', () => {
|
||||||
|
const spy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { throw new Error('quota'); });
|
||||||
|
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||||
|
const preset = pluginUtils.getPreset('persistence');
|
||||||
|
expect(() => preset?.uninstall?.()).not.toThrow();
|
||||||
|
spy.mockRestore();
|
||||||
|
});
|
||||||
|
});
|
||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast 单元测试
|
* MetonaToast 单元测试
|
||||||
* @module tests
|
* @module tests
|
||||||
* @version 0.2.1
|
* @version 0.5.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import MeToast, { Toast, VERSION } from '../src/index';
|
import MeToast, { Toast, VERSION } from '../src/index';
|
||||||
@@ -105,8 +105,8 @@ const mockWindow = {
|
|||||||
|
|
||||||
describe('版本信息', () => {
|
describe('版本信息', () => {
|
||||||
test('应该有正确的版本号', () => {
|
test('应该有正确的版本号', () => {
|
||||||
expect(VERSION).toBe('0.2.1');
|
expect(VERSION).toBe('0.5.0');
|
||||||
expect(MeToast.version).toBe('0.2.1');
|
expect(MeToast.version).toBe('0.5.0');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -526,7 +526,7 @@ const mockWindow = {
|
|||||||
test('应该能够获取状态信息', () => {
|
test('应该能够获取状态信息', () => {
|
||||||
MeToast.success('消息');
|
MeToast.success('消息');
|
||||||
const status = MeToast.getStatus();
|
const status = MeToast.getStatus();
|
||||||
expect(status.version).toBe('0.2.1');
|
expect(status.version).toBe('0.5.0');
|
||||||
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
expect(status.toasts).toBeGreaterThanOrEqual(0);
|
||||||
expect(status.theme).toBeDefined();
|
expect(status.theme).toBeDefined();
|
||||||
expect(status.locale).toBeDefined();
|
expect(status.locale).toBeDefined();
|
||||||
@@ -1213,9 +1213,9 @@ describe('新增功能', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ========== v0.2.1 新增功能测试 ==========
|
// ========== v0.4.0 新增功能测试 ==========
|
||||||
|
|
||||||
describe('v0.2.1 新增功能', () => {
|
describe('v0.4.0 新增功能', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
MeToast._toasts.clear();
|
MeToast._toasts.clear();
|
||||||
const { _containerCache } = require('../src/toast.js');
|
const { _containerCache } = require('../src/toast.js');
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast React 适配器测试
|
||||||
|
* @module tests
|
||||||
|
* @version 0.5.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
import { renderToString } from 'react-dom/server';
|
||||||
|
import { createRoot } from 'react-dom/client';
|
||||||
|
import { act } from 'react';
|
||||||
|
import MeToast, { Toast } from '../src/index';
|
||||||
|
import { createBoundApi, Toast as ToastComponent, useToast } from '../src/react';
|
||||||
|
import type { ToastInstance } from '../src/types';
|
||||||
|
|
||||||
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
MeToast._toasts.clear();
|
||||||
|
MeToast.resetConfig();
|
||||||
|
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||||
|
Toast._hooks.clear();
|
||||||
|
Toast._registry.clear();
|
||||||
|
const { _containerCache } = require('../src/toast.js');
|
||||||
|
_containerCache.clear();
|
||||||
|
MeToast.animations.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('React 适配器 — createBoundApi', () => {
|
||||||
|
test('跟踪创建的 toast id', () => {
|
||||||
|
const cleanup = new Set<string>();
|
||||||
|
const api = createBoundApi(cleanup);
|
||||||
|
(api.info as (m: string) => ToastInstance)('hello');
|
||||||
|
expect(cleanup.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('success/error/warning/show 均被跟踪', () => {
|
||||||
|
const cleanup = new Set<string>();
|
||||||
|
const api = createBoundApi(cleanup);
|
||||||
|
(api.success as (m: string) => ToastInstance)('a');
|
||||||
|
(api.error as (m: string) => ToastInstance)('b');
|
||||||
|
(api.warning as (m: string) => ToastInstance)('c');
|
||||||
|
(api.show as (m: string) => ToastInstance)('d');
|
||||||
|
expect(cleanup.size).toBe(4);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loading 控制对象也被跟踪', () => {
|
||||||
|
const cleanup = new Set<string>();
|
||||||
|
const api = createBoundApi(cleanup);
|
||||||
|
(api.loading as (m: string) => { id: string })('loading...');
|
||||||
|
expect(cleanup.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('dismiss 同步清理集合', () => {
|
||||||
|
const cleanup = new Set<string>();
|
||||||
|
const api = createBoundApi(cleanup);
|
||||||
|
const t = (api.success as (m: string) => ToastInstance)('hi');
|
||||||
|
expect(cleanup.has(t.id)).toBe(true);
|
||||||
|
(api.dismiss as (id?: string) => void)(t.id);
|
||||||
|
expect(cleanup.has(t.id)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('removeToast 同步清理集合', () => {
|
||||||
|
const cleanup = new Set<string>();
|
||||||
|
const api = createBoundApi(cleanup);
|
||||||
|
const t = (api.info as (m: string) => ToastInstance)('hi');
|
||||||
|
(api.removeToast as (id: string) => void)(t.id);
|
||||||
|
expect(cleanup.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未跟踪的方法直接透传', () => {
|
||||||
|
const cleanup = new Set<string>();
|
||||||
|
const api = createBoundApi(cleanup);
|
||||||
|
expect(typeof (api as Record<string, unknown>).count).toBe('function');
|
||||||
|
expect((api as { count(): number }).count()).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('React 适配器 — Toast 组件', () => {
|
||||||
|
test('SSR 渲染为空(effect 不执行)', () => {
|
||||||
|
const html = renderToString(React.createElement(ToastComponent, { message: 'ssr message' }));
|
||||||
|
expect(html).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SSR 带 type 不抛异常', () => {
|
||||||
|
const html = renderToString(React.createElement(ToastComponent, { message: 'x', type: 'success' }));
|
||||||
|
expect(html).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SSR 未知 type fallback 到 show 不抛异常', () => {
|
||||||
|
const html = renderToString(React.createElement(ToastComponent, { message: 'y', type: 'custom-type' }));
|
||||||
|
expect(html).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('组件挂载创建 toast,卸载自动移除', () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'mounted', type: 'success' })); });
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
act(() => { root.unmount(); });
|
||||||
|
expect(MeToast.count()).toBe(0);
|
||||||
|
host.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('组件 autoClose:false 卸载不移除', () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'keep', type: 'error', autoClose: false })); });
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
act(() => { root.unmount(); });
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
MeToast.dismiss();
|
||||||
|
host.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('组件 message 变化重新创建实例', () => {
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'v1', type: 'info' })); });
|
||||||
|
act(() => { root.render(React.createElement(ToastComponent, { message: 'v2', type: 'warning' })); });
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
const t = MeToast.getToasts()[0];
|
||||||
|
expect(t.message).toBe('v2');
|
||||||
|
expect(t.type).toBe('warning');
|
||||||
|
act(() => { root.unmount(); });
|
||||||
|
expect(MeToast.count()).toBe(0);
|
||||||
|
host.remove();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('useToast 组件内创建的 toast 在卸载时清理', () => {
|
||||||
|
const Harness = (): React.ReactElement => {
|
||||||
|
const toast = useToast() as Record<string, (m: string) => ToastInstance>;
|
||||||
|
return React.createElement('button', { onClick: () => toast.info('tracked') });
|
||||||
|
};
|
||||||
|
const host = document.createElement('div');
|
||||||
|
document.body.appendChild(host);
|
||||||
|
const root = createRoot(host);
|
||||||
|
act(() => { root.render(React.createElement(Harness)); });
|
||||||
|
const btn = host.querySelector('button')!;
|
||||||
|
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
||||||
|
expect(MeToast.count()).toBe(1);
|
||||||
|
act(() => { root.unmount(); });
|
||||||
|
expect(MeToast.count()).toBe(0);
|
||||||
|
host.remove();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
/**
|
||||||
|
* Jest 环境补丁 — jsdom 未暴露 Node 全局对象
|
||||||
|
*/
|
||||||
|
import { TextEncoder, TextDecoder } from 'util';
|
||||||
|
|
||||||
|
const g = globalThis as Record<string, unknown>;
|
||||||
|
|
||||||
|
if (typeof g.TextEncoder === 'undefined') {
|
||||||
|
g.TextEncoder = TextEncoder;
|
||||||
|
}
|
||||||
|
if (typeof g.TextDecoder === 'undefined') {
|
||||||
|
g.TextDecoder = TextDecoder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* 环境隔离测试 — 需要切换 document/window/localStorage 环境并调用 jest.resetModules()。
|
||||||
|
* 独立成文件:resetModules 会清空模块缓存,若与其他测试同文件会导致
|
||||||
|
* 后续 require 拿到新模块实例、与 import 时绑定的实例状态分裂。
|
||||||
|
* 注:jsdom 以 getter 暴露 document/window,普通赋值无效,须用 defineProperty 覆盖。
|
||||||
|
* @module tests
|
||||||
|
* @version 0.5.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** 临时移除全局对象,返回恢复函数 */
|
||||||
|
const hideGlobal = (key: string): (() => void) => {
|
||||||
|
const desc = Object.getOwnPropertyDescriptor(global, key);
|
||||||
|
Object.defineProperty(global, key, { value: undefined, configurable: true, writable: true });
|
||||||
|
return () => {
|
||||||
|
if (desc) Object.defineProperty(global, key, desc);
|
||||||
|
else delete (global as Record<string, unknown>)[key];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('环境隔离(SSR / localStorage 异常路径)', () => {
|
||||||
|
test('escapeHTML SSR 路径(无 document)', () => {
|
||||||
|
const restore = hideGlobal('document');
|
||||||
|
jest.resetModules();
|
||||||
|
const { escapeHTML } = require('../src/utils.js');
|
||||||
|
const result = escapeHTML('<script>alert("xss")</script>');
|
||||||
|
expect(result).not.toContain('<script>');
|
||||||
|
expect(result).toContain('<script>');
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('prefersDark 无 window 时返回 false', () => {
|
||||||
|
const restore = hideGlobal('window');
|
||||||
|
jest.resetModules();
|
||||||
|
const { prefersDark } = require('../src/utils.js');
|
||||||
|
expect(prefersDark()).toBe(false);
|
||||||
|
restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('saveTheme localStorage 异常被捕获', () => {
|
||||||
|
const orig = (global as Record<string, unknown>).localStorage;
|
||||||
|
(global as Record<string, unknown>).localStorage = undefined;
|
||||||
|
jest.resetModules();
|
||||||
|
const { saveTheme } = require('../src/themes.js');
|
||||||
|
expect(() => saveTheme('dark')).not.toThrow();
|
||||||
|
(global as Record<string, unknown>).localStorage = orig;
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loadLocale localStorage 异常返回 default', () => {
|
||||||
|
const orig = (global as Record<string, unknown>).localStorage;
|
||||||
|
(global as Record<string, unknown>).localStorage = {
|
||||||
|
getItem: () => { throw new Error('quota'); },
|
||||||
|
};
|
||||||
|
jest.resetModules();
|
||||||
|
const { loadLocale } = require('../src/i18n.js');
|
||||||
|
expect(typeof loadLocale()).toBe('string');
|
||||||
|
(global as Record<string, unknown>).localStorage = orig;
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user