release: v0.2.0 — TypeScript 源码重构
### Changed - 全部源码从 JavaScript 迁移到 TypeScript (strict mode) - core.js (1244行) 拆分为 toast.ts + api.ts + templates.ts - 删除手动维护的 types/index.d.ts,类型从源码自动生成 - 构建工具链: Babel → ts-jest, 新增 @rollup/plugin-typescript - 新增 rollup-plugin-dts 生成合并 .d.ts ### Added - tsconfig.json (strict: true) - src/types.ts 核心类型模块 (39 个导出类型) - .eslintrc.json (@typescript-eslint) - .gitea/workflows/ci.yml (Node 18/20/22/24 矩阵) - CHANGELOG.md ### Aligned with metona-starter - package.json: type:module, engines≥16, prepublishOnly - rollup.config.js: dts plugin, port 3001 - jest.config.cjs, build.sh, serve.sh, .gitignore (.npmrc) ### Removed - babel.config.js, types/ 目录, 所有 src/*.js
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2020": true,
|
||||||
|
"node": true,
|
||||||
|
"jest": true
|
||||||
|
},
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": ["@typescript-eslint"],
|
||||||
|
"extends": ["eslint:recommended"],
|
||||||
|
"rules": {
|
||||||
|
"no-console": "warn",
|
||||||
|
"no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }],
|
||||||
|
"no-undef": "off",
|
||||||
|
"no-empty": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"no-control-regex": "off",
|
||||||
|
"no-regex-spaces": "off",
|
||||||
|
"no-constant-condition": ["warn", { "checkLoops": false }]
|
||||||
|
},
|
||||||
|
"ignorePatterns": ["dist/", "coverage/", "node_modules/"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: debian-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
node-version: [18.x, 20.x, 22.x, 24.x]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: ${{ matrix.node-version }}
|
||||||
|
cache: 'npm'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npm run typecheck
|
||||||
|
|
||||||
|
- name: Lint
|
||||||
|
run: npm run lint
|
||||||
|
continue-on-error: true
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
@@ -2,4 +2,7 @@ node_modules/
|
|||||||
dist/
|
dist/
|
||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
.zcode/
|
||||||
|
|
||||||
|
# 本地 npm 发布凭据(含 _auth,勿提交)
|
||||||
.npmrc
|
.npmrc
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to MetonaToast will be documented in this file.
|
||||||
|
|
||||||
|
## [0.2.0] - 2026-07-25
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- **TypeScript 源码重构**:全部源码从 JavaScript 迁移到 TypeScript
|
||||||
|
- 删除手动维护的 `types/index.d.ts`,类型由源码自动生成
|
||||||
|
- `core.js` (1244行) 拆分为 `toast.ts` + `api.ts` + `templates.ts`
|
||||||
|
- 构建工具链:Babel → `ts-jest`,新增 `@rollup/plugin-typescript`
|
||||||
|
- 测试迁移至 TypeScript (`tests/index.test.ts`)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- `tsconfig.json` (strict: true)
|
||||||
|
- `src/types.ts` 核心类型模块
|
||||||
|
- `.eslintrc.json` ESLint 配置
|
||||||
|
- `.gitea/workflows/ci.yml` CI 工作流
|
||||||
|
- `CHANGELOG.md`
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
- `babel.config.js`
|
||||||
|
- `types/` 目录
|
||||||
|
|
||||||
|
## [0.1.2] - 2026-07-24
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- 补全和修正 TypeScript 类型声明
|
||||||
|
- 全面修复、优化与增强
|
||||||
|
|
||||||
|
## [0.1.1] - 2026-07-23
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- 版本号重置,更新文档页面
|
||||||
|
|
||||||
|
## [0.1.0] - 2026-07-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- 初始发布
|
||||||
|
- 107 种内置图标
|
||||||
|
- 11 种动画效果
|
||||||
|
- 4 种主题(light/dark/auto/warm)
|
||||||
|
- 国际化支持 (zh-CN/en-US)
|
||||||
|
- 插件系统 (keyboard/persistence/accessibility)
|
||||||
|
- 拖拽关闭、进度条、倒计时等高级特性
|
||||||
|
- TypeScript 类型声明
|
||||||
|
- 零依赖
|
||||||
@@ -8,14 +8,14 @@
|
|||||||
|
|
||||||
## 特性
|
## 特性
|
||||||
|
|
||||||
- **零依赖** — 纯原生 JavaScript,gzip 后不到 10KB
|
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB
|
||||||
- **107 种图标类型** — success/error/warning/info/loading 及更多扩展类型,覆盖所有常见场景
|
- **107 种图标类型** — success/error/warning/info/loading 及更多扩展类型,覆盖所有常见场景
|
||||||
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,每种效果明显不同
|
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,每种效果明显不同
|
||||||
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
|
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
|
||||||
- **国际化** — 内置 zh-CN / en-US 完整翻译(去重优化),可通过 `addTranslations()` 扩展
|
- **国际化** — 内置 zh-CN / en-US 完整翻译(去重优化),可通过 `addTranslations()` 扩展
|
||||||
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
|
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
|
||||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
|
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
|
||||||
- **TypeScript** — 完整类型定义,已对齐实际实现无 phantom 方法
|
- **TypeScript** — 严格模式,源码级类型安全,类型从源码自动生成无手动维护
|
||||||
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入监控系统
|
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入监控系统
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
@@ -58,7 +58,7 @@ const MeToast = require('@metona-team/metona-toast');
|
|||||||
| Opera | 51+ |
|
| Opera | 51+ |
|
||||||
| iOS Safari | 11+ |
|
| iOS Safari | 11+ |
|
||||||
| Android Chrome | 64+ |
|
| Android Chrome | 64+ |
|
||||||
| Node.js | 18+(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。
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
presets: [
|
|
||||||
[
|
|
||||||
'@babel/preset-env',
|
|
||||||
{
|
|
||||||
targets: {
|
|
||||||
node: 'current',
|
|
||||||
},
|
|
||||||
modules: 'commonjs',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
],
|
|
||||||
plugins: [
|
|
||||||
'@babel/plugin-transform-modules-commonjs',
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,83 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# MetonaToast 构建脚本
|
# 构建脚本 — 安装依赖 → 测试 → 构建
|
||||||
|
|
||||||
echo "🍞 MetonaToast 构建脚本"
|
echo "🔨 MetonaToast 构建脚本"
|
||||||
echo "========================"
|
|
||||||
|
|
||||||
# 检查Node.js
|
|
||||||
if ! command -v node &> /dev/null; then
|
if ! command -v node &> /dev/null; then
|
||||||
echo "❌ 错误: 未找到Node.js"
|
echo "❌ 未找到 Node.js"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查npm
|
echo "✅ Node.js: $(node -v)"
|
||||||
if ! command -v npm &> /dev/null; then
|
|
||||||
echo "❌ 错误: 未找到npm"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ Node.js版本: $(node -v)"
|
|
||||||
echo "✅ npm版本: $(npm -v)"
|
|
||||||
|
|
||||||
# 安装依赖
|
|
||||||
echo ""
|
|
||||||
echo "📦 安装依赖..."
|
echo "📦 安装依赖..."
|
||||||
npm install
|
npm install || exit 1
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ 依赖安装失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ 依赖安装完成"
|
|
||||||
|
|
||||||
# 运行测试
|
|
||||||
echo ""
|
|
||||||
echo "🧪 运行测试..."
|
echo "🧪 运行测试..."
|
||||||
npm test
|
npm test || exit 1
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
echo "🔨 构建..."
|
||||||
echo "❌ 测试失败"
|
npm run build || exit 1
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ 测试通过"
|
|
||||||
|
|
||||||
# 构建项目
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔨 构建项目..."
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo "❌ 构建失败"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "✅ 构建完成"
|
echo "✅ 构建完成"
|
||||||
|
|
||||||
# 显示构建结果
|
|
||||||
echo ""
|
|
||||||
echo "📊 构建结果:"
|
|
||||||
echo "========================"
|
|
||||||
ls -lh dist/
|
ls -lh dist/
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "✅ 构建成功完成!"
|
|
||||||
echo ""
|
|
||||||
echo "📁 文件结构:"
|
|
||||||
echo " - dist/metona-toast.js (UMD格式)"
|
|
||||||
echo " - dist/metona-toast.esm.js (ES Module格式)"
|
|
||||||
echo " - dist/metona-toast.cjs.js (CommonJS格式)"
|
|
||||||
echo " - dist/metona-toast.min.js (压缩版本)"
|
|
||||||
echo " - dist/metona-toast.d.ts (TypeScript声明)"
|
|
||||||
echo ""
|
|
||||||
echo "🚀 使用方法:"
|
|
||||||
echo " 1. 浏览器: <script src='dist/metona-toast.js'></script>"
|
|
||||||
echo " 2. ES Module: import MeToast from 'dist/metona-toast.esm.js'"
|
|
||||||
echo " 3. CommonJS: const MeToast = require('dist/metona-toast.cjs.js')"
|
|
||||||
echo ""
|
|
||||||
echo "📝 示例:"
|
|
||||||
echo " 打开 site/index.html 查看官网"
|
|
||||||
echo " 打开 site/demo.html 查看演示"
|
|
||||||
echo ""
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/** @type {import('ts-jest').JestConfigWithTsJest} */
|
||||||
|
module.exports = {
|
||||||
|
preset: 'ts-jest',
|
||||||
|
testEnvironment: 'jsdom',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.ts$': ['ts-jest', {
|
||||||
|
tsconfig: {
|
||||||
|
target: 'ES2020',
|
||||||
|
module: 'ESNext',
|
||||||
|
moduleResolution: 'node',
|
||||||
|
esModuleInterop: true,
|
||||||
|
strict: true,
|
||||||
|
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
||||||
|
allowImportingTsExtensions: true,
|
||||||
|
noEmit: true,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||||
|
},
|
||||||
|
transformIgnorePatterns: [
|
||||||
|
'/node_modules/(?!(@rollup)/)',
|
||||||
|
],
|
||||||
|
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'src/**/*.ts',
|
||||||
|
'!src/index.ts',
|
||||||
|
'!src/types.ts',
|
||||||
|
],
|
||||||
|
coverageDirectory: 'coverage',
|
||||||
|
coverageReporters: ['text', 'lcov'],
|
||||||
|
verbose: true,
|
||||||
|
};
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
module.exports = {
|
|
||||||
testEnvironment: 'jsdom',
|
|
||||||
transform: {
|
|
||||||
'^.+\\.js$': 'babel-jest',
|
|
||||||
},
|
|
||||||
transformIgnorePatterns: [
|
|
||||||
'/node_modules/(?!(@rollup)/)',
|
|
||||||
],
|
|
||||||
moduleFileExtensions: ['js', 'json'],
|
|
||||||
collectCoverageFrom: [
|
|
||||||
'src/**/*.js',
|
|
||||||
'!src/index.js',
|
|
||||||
],
|
|
||||||
coverageDirectory: 'coverage',
|
|
||||||
coverageReporters: ['text', 'lcov'],
|
|
||||||
verbose: true,
|
|
||||||
};
|
|
||||||
Generated
+550
-1437
File diff suppressed because it is too large
Load Diff
+21
-20
@@ -1,38 +1,38 @@
|
|||||||
{
|
{
|
||||||
"name": "@metona-team/metona-toast",
|
"name": "@metona-team/metona-toast",
|
||||||
"version": "0.1.2",
|
"version": "0.2.0",
|
||||||
"description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。",
|
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
||||||
"main": "dist/metona-toast.js",
|
"type": "module",
|
||||||
"module": "src/index.js",
|
"main": "dist/metona-toast.cjs.js",
|
||||||
|
"module": "dist/metona-toast.esm.js",
|
||||||
"unpkg": "dist/metona-toast.min.js",
|
"unpkg": "dist/metona-toast.min.js",
|
||||||
"jsdelivr": "dist/metona-toast.min.js",
|
"jsdelivr": "dist/metona-toast.min.js",
|
||||||
"types": "types/index.d.ts",
|
"types": "dist/metona-toast.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./src/index.js",
|
"import": "./dist/metona-toast.esm.js",
|
||||||
"require": "./dist/metona-toast.js",
|
"require": "./dist/metona-toast.cjs.js",
|
||||||
"types": "./types/index.d.ts"
|
"types": "./dist/metona-toast.d.ts"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist/",
|
"dist/",
|
||||||
"src/",
|
"src/",
|
||||||
"types/",
|
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE"
|
"LICENSE"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rollup -c",
|
"build": "tsc --noEmit && rollup -c",
|
||||||
"dev": "rollup -c -w",
|
"dev": "rollup -c -w",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"test:coverage": "jest --coverage",
|
"test:coverage": "jest --coverage",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint 'src/**/*.ts'",
|
||||||
"lint:fix": "eslint src/ --fix",
|
"lint:fix": "eslint 'src/**/*.ts' --fix",
|
||||||
"format": "prettier --write src/",
|
"format": "prettier --write 'src/**/*.ts' 'tests/**/*.ts'",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"prepublishOnly": "npm run build",
|
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
||||||
"docs": "jsdoc src/ -d docs",
|
"docs": "typedoc src/ --out docs",
|
||||||
"example": "serve examples/"
|
"example": "serve examples/"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
@@ -57,14 +57,13 @@
|
|||||||
},
|
},
|
||||||
"homepage": "https://git.metona.cn/MetonaTeam/MetonaToast#readme",
|
"homepage": "https://git.metona.cn/MetonaTeam/MetonaToast#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.22.0",
|
|
||||||
"@babel/preset-env": "^7.22.0",
|
|
||||||
"@babel/plugin-transform-modules-commonjs": "^7.22.0",
|
|
||||||
"@rollup/plugin-commonjs": "^25.0.0",
|
"@rollup/plugin-commonjs": "^25.0.0",
|
||||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||||
"@rollup/plugin-terser": "^0.4.0",
|
"@rollup/plugin-terser": "^0.4.0",
|
||||||
|
"@rollup/plugin-typescript": "^11.1.6",
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
"babel-jest": "^29.5.0",
|
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||||
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"jest": "^29.5.0",
|
"jest": "^29.5.0",
|
||||||
"jest-environment-jsdom": "^29.5.0",
|
"jest-environment-jsdom": "^29.5.0",
|
||||||
@@ -73,10 +72,12 @@
|
|||||||
"rollup-plugin-dts": "^5.3.0",
|
"rollup-plugin-dts": "^5.3.0",
|
||||||
"rollup-plugin-livereload": "^2.0.5",
|
"rollup-plugin-livereload": "^2.0.5",
|
||||||
"rollup-plugin-serve": "^2.0.1",
|
"rollup-plugin-serve": "^2.0.1",
|
||||||
|
"ts-jest": "^29.4.12",
|
||||||
|
"tslib": "^2.8.1",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=14.0.0"
|
"node": ">=16.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"> 1%",
|
"> 1%",
|
||||||
|
|||||||
+32
-60
@@ -1,5 +1,6 @@
|
|||||||
import resolve from '@rollup/plugin-node-resolve';
|
import resolve from '@rollup/plugin-node-resolve';
|
||||||
import commonjs from '@rollup/plugin-commonjs';
|
import commonjs from '@rollup/plugin-commonjs';
|
||||||
|
import typescript from '@rollup/plugin-typescript';
|
||||||
import terser from '@rollup/plugin-terser';
|
import terser from '@rollup/plugin-terser';
|
||||||
import dts from 'rollup-plugin-dts';
|
import dts from 'rollup-plugin-dts';
|
||||||
import serve from 'rollup-plugin-serve';
|
import serve from 'rollup-plugin-serve';
|
||||||
@@ -8,104 +9,79 @@ import livereload from 'rollup-plugin-livereload';
|
|||||||
const isDev = process.env.ROLLUP_WATCH;
|
const isDev = process.env.ROLLUP_WATCH;
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
|
|
||||||
// 基础配置
|
// 基础插件
|
||||||
const baseConfig = {
|
const basePlugins = [
|
||||||
input: 'src/index.js',
|
|
||||||
plugins: [
|
|
||||||
resolve(),
|
resolve(),
|
||||||
commonjs(),
|
commonjs(),
|
||||||
],
|
typescript({
|
||||||
};
|
tsconfig: './tsconfig.json',
|
||||||
|
declaration: false,
|
||||||
|
sourceMap: !isProd,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
// 开发服务器配置
|
// 开发服务器配置
|
||||||
const devPlugins = isDev ? [
|
const devPlugins = isDev ? [
|
||||||
serve({
|
serve({
|
||||||
open: true,
|
open: true,
|
||||||
contentBase: ['site', 'dist'],
|
contentBase: ['site', 'dist'],
|
||||||
port: 3000,
|
port: 3001,
|
||||||
}),
|
}),
|
||||||
livereload({
|
livereload({
|
||||||
watch: ['src', 'site'],
|
watch: ['src', 'site'],
|
||||||
}),
|
}),
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
// 生产环境配置
|
|
||||||
const prodPlugins = isProd ? [
|
|
||||||
terser({
|
|
||||||
compress: {
|
|
||||||
drop_console: true,
|
|
||||||
drop_debugger: true,
|
|
||||||
pure_funcs: ['console.log', 'console.warn'],
|
|
||||||
},
|
|
||||||
format: {
|
|
||||||
comments: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
] : [];
|
|
||||||
|
|
||||||
// 输出配置
|
// 输出配置
|
||||||
export default [
|
export default [
|
||||||
// UMD格式(浏览器)
|
// UMD(开发模式)
|
||||||
{
|
{
|
||||||
...baseConfig,
|
input: 'src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/metona-toast.js',
|
file: 'dist/metona-toast.js',
|
||||||
format: 'umd',
|
format: 'umd',
|
||||||
name: 'MeToast',
|
name: 'MeToast',
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
sourcemap: !isProd,
|
sourcemap: true,
|
||||||
globals: {},
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [...basePlugins, ...devPlugins],
|
||||||
...baseConfig.plugins,
|
|
||||||
...devPlugins,
|
|
||||||
...prodPlugins,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
// 生产构建(非 dev 模式时输出全格式)
|
||||||
// ESM格式(现代浏览器/打包工具)
|
...(isDev ? [] : [
|
||||||
|
// ESM
|
||||||
{
|
{
|
||||||
...baseConfig,
|
input: 'src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/metona-toast.esm.js',
|
file: 'dist/metona-toast.esm.js',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
sourcemap: !isProd,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: basePlugins,
|
||||||
...baseConfig.plugins,
|
|
||||||
...prodPlugins,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
// CommonJS
|
||||||
// CommonJS格式(Node.js)
|
|
||||||
{
|
{
|
||||||
...baseConfig,
|
input: 'src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/metona-toast.cjs.js',
|
file: 'dist/metona-toast.cjs.js',
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
sourcemap: !isProd,
|
sourcemap: true,
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: basePlugins,
|
||||||
...baseConfig.plugins,
|
|
||||||
...prodPlugins,
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
|
// UMD 压缩版
|
||||||
// 压缩版本(UMD)
|
|
||||||
{
|
{
|
||||||
...baseConfig,
|
input: 'src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/metona-toast.min.js',
|
file: 'dist/metona-toast.min.js',
|
||||||
format: 'umd',
|
format: 'umd',
|
||||||
name: 'MeToast',
|
name: 'MeToast',
|
||||||
exports: 'named',
|
exports: 'named',
|
||||||
sourcemap: false,
|
sourcemap: false,
|
||||||
globals: {},
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
...baseConfig.plugins,
|
...basePlugins,
|
||||||
terser({
|
terser({
|
||||||
compress: {
|
compress: {
|
||||||
drop_console: true,
|
drop_console: true,
|
||||||
@@ -113,23 +89,19 @@ export default [
|
|||||||
pure_funcs: ['console.log', 'console.warn'],
|
pure_funcs: ['console.log', 'console.warn'],
|
||||||
passes: 2,
|
passes: 2,
|
||||||
},
|
},
|
||||||
format: {
|
format: { comments: false },
|
||||||
comments: false,
|
mangle: { toplevel: true },
|
||||||
},
|
|
||||||
mangle: {
|
|
||||||
toplevel: true,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// TypeScript 声明文件合并
|
||||||
// TypeScript声明文件
|
|
||||||
{
|
{
|
||||||
input: 'types/index.d.ts',
|
input: 'src/index.ts',
|
||||||
output: {
|
output: {
|
||||||
file: 'dist/metona-toast.d.ts',
|
file: 'dist/metona-toast.d.ts',
|
||||||
format: 'es',
|
format: 'es',
|
||||||
},
|
},
|
||||||
plugins: [dts()],
|
plugins: [dts()],
|
||||||
},
|
},
|
||||||
|
]),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ else
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PORT=${1:-3000}
|
PORT=${1:-3001}
|
||||||
|
|
||||||
echo "✅ Python: $($PYTHON --version 2>&1)"
|
echo "✅ Python: $($PYTHON --version 2>&1)"
|
||||||
echo "🌐 启动服务器..."
|
echo "🌐 启动服务器..."
|
||||||
|
|||||||
+2
-2
@@ -244,7 +244,7 @@
|
|||||||
|
|
||||||
<!-- 14. 错误回调 -->
|
<!-- 14. 错误回调 -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.1.2</span></div>
|
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.0</span></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>
|
||||||
@@ -282,7 +282,7 @@
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
MetonaToast v0.1.2 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
MetonaToast v0.2.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>
|
||||||
|
|||||||
+1
-1
@@ -87,7 +87,7 @@
|
|||||||
<main class="main">
|
<main class="main">
|
||||||
|
|
||||||
<h1>API 文档</h1>
|
<h1>API 文档</h1>
|
||||||
<p>MetonaToast v0.1.2 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
<p>MetonaToast v0.2.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||||
|
|
||||||
<!-- ===== 基础通知 ===== -->
|
<!-- ===== 基础通知 ===== -->
|
||||||
<h2 id="show">show(message, opts?)</h2>
|
<h2 id="show">show(message, opts?)</h2>
|
||||||
|
|||||||
+5
-5
@@ -80,7 +80,7 @@
|
|||||||
|
|
||||||
<header class="hero">
|
<header class="hero">
|
||||||
<h1><span>MetonaToast</span></h1>
|
<h1><span>MetonaToast</span></h1>
|
||||||
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。纯原生 JavaScript,gzip 不到 10KB。</p>
|
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式源码,gzip 不到 10KB。</p>
|
||||||
<div class="badges">
|
<div class="badges">
|
||||||
<span>📦 <10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
|
<span>📦 <10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,7 +127,7 @@
|
|||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">⚡</div>
|
<div class="feature-icon">⚡</div>
|
||||||
<h3>零依赖</h3>
|
<h3>零依赖</h3>
|
||||||
<p>纯原生 JavaScript 实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
<p>TypeScript 严格模式,纯原生实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">🛡️</div>
|
<div class="feature-icon">🛡️</div>
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
<div class="feature">
|
<div class="feature">
|
||||||
<div class="feature-icon">📘</div>
|
<div class="feature-icon">📘</div>
|
||||||
<h3>TypeScript</h3>
|
<h3>TypeScript</h3>
|
||||||
<p>完整类型定义,与实现严格对齐零 phantom 方法。<code>MeToast.success()</code>、<code>.confirm()</code>、<code>.action()</code> 全部有类型。</p>
|
<p>严格模式源码,类型从源码自动生成。strictNullChecks、noImplicitAny 全开,零类型漂移。</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||||
<div style="font-size:28px;margin-bottom:6px;">📦</div>
|
<div style="font-size:28px;margin-bottom:6px;">📦</div>
|
||||||
<h3>Node.js</h3>
|
<h3>Node.js</h3>
|
||||||
<p>18+</p>
|
<p>16+</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||||
<div style="font-size:28px;margin-bottom:6px;">📘</div>
|
<div style="font-size:28px;margin-bottom:6px;">📘</div>
|
||||||
@@ -247,7 +247,7 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>MetonaToast v0.1.2 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
<p>MetonaToast v0.2.0 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script src="../dist/metona-toast.js"></script>
|
<script src="../dist/metona-toast.js"></script>
|
||||||
|
|||||||
@@ -1,111 +0,0 @@
|
|||||||
/**
|
|
||||||
* MetonaToast Animations - 动画管理 v0.1.2
|
|
||||||
* @module animations
|
|
||||||
* @description 动画注册与管理,移除未使用的 Web Animations API dead code
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { ANIMATIONS } from './constants.js';
|
|
||||||
|
|
||||||
// 动画缓存
|
|
||||||
const animationMap = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注册默认动画
|
|
||||||
*/
|
|
||||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
|
||||||
animationMap.set(name, config);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 动画工具函数
|
|
||||||
*/
|
|
||||||
export const animationUtils = {
|
|
||||||
/**
|
|
||||||
* 注册自定义动画
|
|
||||||
* @param {string} name - 动画名称
|
|
||||||
* @param {Object} config - 动画配置 { enter, leave, duration, easing }
|
|
||||||
*/
|
|
||||||
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)',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 注销动画
|
|
||||||
* @param {string} name - 动画名称
|
|
||||||
*/
|
|
||||||
unregister(name) {
|
|
||||||
animationMap.delete(name);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取动画配置
|
|
||||||
* @param {string} name - 动画名称
|
|
||||||
* @returns {Object|null} 动画配置
|
|
||||||
*/
|
|
||||||
get(name) {
|
|
||||||
return animationMap.get(name) || ANIMATIONS[name] || null;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有动画名称
|
|
||||||
* @returns {Array} 动画名称数组
|
|
||||||
*/
|
|
||||||
getAnimationNames() {
|
|
||||||
return Array.from(animationMap.keys());
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取已注册动画总数
|
|
||||||
* @returns {number} 已注册动画数量
|
|
||||||
*/
|
|
||||||
getActiveCount() {
|
|
||||||
return animationMap.size;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 取消所有动画(兼容API,当前CSS动画由浏览器管理)
|
|
||||||
*/
|
|
||||||
cancelAll() {
|
|
||||||
// CSS动画由浏览器原生管理,无需手动取消
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 重置动画管理器
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
animationMap.clear();
|
|
||||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
|
||||||
animationMap.set(name, config);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 销毁动画管理器
|
|
||||||
*/
|
|
||||||
destroy() {
|
|
||||||
animationMap.clear();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 动画预设
|
|
||||||
*/
|
|
||||||
export const animationPresets = {};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建自定义动画配置
|
|
||||||
* @param {Object} config - 动画配置
|
|
||||||
* @returns {Object} 动画配置
|
|
||||||
*/
|
|
||||||
export const createAnimation = (config) => ({
|
|
||||||
enter: config.enter || {},
|
|
||||||
leave: config.leave || {},
|
|
||||||
duration: config.duration || 300,
|
|
||||||
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast Animations — 动画管理
|
||||||
|
* @module animations
|
||||||
|
* @version 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ANIMATIONS } from './constants.js';
|
||||||
|
import type { AnimationConfig, AnimationUtils } from './types.js';
|
||||||
|
|
||||||
|
// 动画缓存
|
||||||
|
const animationMap: Map<string, AnimationConfig> = new Map();
|
||||||
|
|
||||||
|
// 注册默认动画
|
||||||
|
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||||
|
animationMap.set(name, {
|
||||||
|
name,
|
||||||
|
enter: config.enter,
|
||||||
|
leave: config.leave,
|
||||||
|
duration: config.duration,
|
||||||
|
easing: config.easing,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画工具函数
|
||||||
|
*/
|
||||||
|
export const animationUtils: AnimationUtils = {
|
||||||
|
register(name: string, config: Partial<AnimationConfig>): void {
|
||||||
|
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: string): void {
|
||||||
|
animationMap.delete(name);
|
||||||
|
},
|
||||||
|
|
||||||
|
get(name: string): AnimationConfig | null {
|
||||||
|
return animationMap.get(name) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
getAnimationNames(): string[] {
|
||||||
|
return Array.from(animationMap.keys());
|
||||||
|
},
|
||||||
|
|
||||||
|
getActiveCount(): number {
|
||||||
|
return animationMap.size;
|
||||||
|
},
|
||||||
|
|
||||||
|
cancelAll(): void {
|
||||||
|
// CSS动画由浏览器原生管理,无需手动取消
|
||||||
|
},
|
||||||
|
|
||||||
|
reset(): void {
|
||||||
|
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(): void {
|
||||||
|
animationMap.clear();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动画预设(占位,兼容旧API)
|
||||||
|
*/
|
||||||
|
export const animationPresets: Record<string, AnimationConfig> = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建自定义动画配置
|
||||||
|
*/
|
||||||
|
export const createAnimation = (config: Partial<AnimationConfig>): AnimationConfig => ({
|
||||||
|
enter: config.enter || {},
|
||||||
|
leave: config.leave || {},
|
||||||
|
duration: config.duration || 300,
|
||||||
|
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||||
|
});
|
||||||
+617
@@ -0,0 +1,617 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast API — meToast 核心 API 对象
|
||||||
|
* @module api
|
||||||
|
* @version 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Toast, _containerCache } from './toast.js';
|
||||||
|
import { DEFAULTS } from './constants.js';
|
||||||
|
import { escapeHTML } from './utils.js';
|
||||||
|
import { t } from './i18n.js';
|
||||||
|
import { applyTheme } from './themes.js';
|
||||||
|
import { setCurrentLocale } from './i18n.js';
|
||||||
|
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
||||||
|
import type {
|
||||||
|
ToastConfig, ToastOptions, ToastInstance,
|
||||||
|
LoadingControl, ProgressControl, CountdownControl,
|
||||||
|
ActionControl, QueueControl, GroupAPI,
|
||||||
|
ActionButton, PromiseOptions, ConfirmOptions,
|
||||||
|
PromptOptions, ProgressOptions, CountdownOptions,
|
||||||
|
QueueOptions, StackOptions, MeToast, ErrorInfo,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 参数标准化工具
|
||||||
|
*/
|
||||||
|
const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions => {
|
||||||
|
const [first, second] = args;
|
||||||
|
|
||||||
|
if (typeof first === 'string') {
|
||||||
|
return {
|
||||||
|
...((second as Record<string, unknown>) || {}),
|
||||||
|
type: (second as Record<string, unknown>)?.type as string || defaultType,
|
||||||
|
message: first,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (first && typeof first === 'object') {
|
||||||
|
return {
|
||||||
|
...(first as Record<string, unknown>),
|
||||||
|
type: (first as Record<string, unknown>).type as string || defaultType,
|
||||||
|
} as ToastOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: defaultType,
|
||||||
|
message: '',
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* meToast API 对象
|
||||||
|
*/
|
||||||
|
const meToast: MeToast = {
|
||||||
|
_toasts: new Map<string, ToastInstance>(),
|
||||||
|
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
||||||
|
version: '0.2.0',
|
||||||
|
|
||||||
|
configure(opts: Partial<ToastConfig>): MeToast {
|
||||||
|
if (!opts || typeof opts !== 'object') return this;
|
||||||
|
Object.assign(this._config, opts);
|
||||||
|
|
||||||
|
if (opts.theme) {
|
||||||
|
applyTheme(opts.theme);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.locale) {
|
||||||
|
setCurrentLocale(opts.locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
|
||||||
|
_emit(opts: ToastOptions): Toast {
|
||||||
|
const merged: ToastOptions = { ...this._config, ...opts };
|
||||||
|
if (merged.content && !merged.message) merged.message = merged.content;
|
||||||
|
|
||||||
|
const t = new Toast(merged);
|
||||||
|
t.create();
|
||||||
|
this._toasts.set(t.id, t);
|
||||||
|
|
||||||
|
return t;
|
||||||
|
},
|
||||||
|
|
||||||
|
_remove(id: string): void {
|
||||||
|
this._toasts.delete(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
find(id: string): ToastInstance | undefined {
|
||||||
|
if (!id) return undefined;
|
||||||
|
return this._toasts.get(id);
|
||||||
|
},
|
||||||
|
|
||||||
|
show(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'default');
|
||||||
|
return this._emit(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
success(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'success');
|
||||||
|
return this._emit(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
error(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'error');
|
||||||
|
return this._emit(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
warning(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'warning');
|
||||||
|
return this._emit(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
info(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||||||
|
return this._emit(normalized);
|
||||||
|
},
|
||||||
|
|
||||||
|
loading(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): LoadingControl {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'loading');
|
||||||
|
normalized.duration = 0;
|
||||||
|
normalized.closeButton = false;
|
||||||
|
normalized.showProgress = false;
|
||||||
|
|
||||||
|
const toast = this._emit(normalized);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: toast.id,
|
||||||
|
success: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'success', msg || t('success'), o),
|
||||||
|
error: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'error', msg || t('error'), o),
|
||||||
|
info: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'info', msg || t('info'), o),
|
||||||
|
warning: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'warning', msg || t('warning'), o),
|
||||||
|
update: (p: Partial<ToastOptions>) => { toast.update(p); return this; },
|
||||||
|
dismiss: () => toast.close(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
promise<T>(promise: Promise<T>, opts: PromiseOptions = {}): Promise<T> {
|
||||||
|
if (!promise || typeof (promise as unknown as { then?: unknown }).then !== 'function') {
|
||||||
|
console.error('MeToast.promise: first argument must be a Promise');
|
||||||
|
return Promise.reject(new Error('Invalid promise'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadingMsg = opts.loading || t('loading');
|
||||||
|
const successMsg = opts.success || t('success');
|
||||||
|
const errorMsg = opts.error || t('error');
|
||||||
|
|
||||||
|
const ctrl = this.loading(loadingMsg);
|
||||||
|
|
||||||
|
return Promise.resolve(promise)
|
||||||
|
.then((data: T) => {
|
||||||
|
ctrl.success(successMsg);
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch((err: unknown) => {
|
||||||
|
ctrl.error(errorMsg);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
||||||
|
const id = loadingToast.id;
|
||||||
|
const old = this._toasts.get(id);
|
||||||
|
if (!old) return null;
|
||||||
|
|
||||||
|
const position = old.config.position;
|
||||||
|
old.close();
|
||||||
|
|
||||||
|
return this._emit({ ...opts, type, message, position } as ToastOptions);
|
||||||
|
},
|
||||||
|
|
||||||
|
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
||||||
|
if (typeof message !== 'string') {
|
||||||
|
console.error('MeToast.confirm: message must be a string');
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<boolean>((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
const safeResolve = (val: boolean) => {
|
||||||
|
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
|
||||||
|
};
|
||||||
|
const safetyTimeout = setTimeout(() => safeResolve(false), 10000);
|
||||||
|
|
||||||
|
const toast = this._emit({
|
||||||
|
...opts,
|
||||||
|
type: opts.type || 'warning',
|
||||||
|
message,
|
||||||
|
duration: 0,
|
||||||
|
closeButton: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
draggable: false,
|
||||||
|
html: confirmHTML(opts),
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const confirmBtn = toast.el?.querySelector('.met-confirm-btn') as HTMLElement | null;
|
||||||
|
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (confirmBtn) {
|
||||||
|
confirmBtn.addEventListener('click', () => {
|
||||||
|
toast.close();
|
||||||
|
safeResolve(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelBtn) {
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
toast.close();
|
||||||
|
safeResolve(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
prompt(message: string, opts: PromptOptions = {}): Promise<string | null> {
|
||||||
|
if (typeof message !== 'string') {
|
||||||
|
console.error('MeToast.prompt: message must be a string');
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise<string | null>((resolve) => {
|
||||||
|
let resolved = false;
|
||||||
|
const safeResolve = (val: string | null) => {
|
||||||
|
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
|
||||||
|
};
|
||||||
|
const safetyTimeout = setTimeout(() => safeResolve(null), 10000);
|
||||||
|
|
||||||
|
const toast = this._emit({
|
||||||
|
...opts,
|
||||||
|
type: opts.type || 'info',
|
||||||
|
message,
|
||||||
|
duration: 0,
|
||||||
|
closeButton: false,
|
||||||
|
closeOnClick: false,
|
||||||
|
draggable: false,
|
||||||
|
html: promptHTML(opts),
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const input = toast.el?.querySelector('.met-input') as HTMLInputElement | null;
|
||||||
|
const submitBtn = toast.el?.querySelector('.met-submit-btn') as HTMLElement | null;
|
||||||
|
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (input) {
|
||||||
|
input.focus();
|
||||||
|
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
toast.close();
|
||||||
|
safeResolve(input.value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (submitBtn) {
|
||||||
|
submitBtn.addEventListener('click', () => {
|
||||||
|
toast.close();
|
||||||
|
safeResolve(input?.value || null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cancelBtn) {
|
||||||
|
cancelBtn.addEventListener('click', () => {
|
||||||
|
toast.close();
|
||||||
|
safeResolve(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
progress(messageOrOpts: string | ToastOptions, opts: ProgressOptions = {}): ProgressControl {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||||||
|
normalized.duration = 0;
|
||||||
|
normalized.closeButton = false;
|
||||||
|
normalized.showProgress = false;
|
||||||
|
|
||||||
|
const toast = this._emit({
|
||||||
|
...normalized,
|
||||||
|
html: progressHTML(opts),
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: toast.id,
|
||||||
|
|
||||||
|
setProgress(percent: number) {
|
||||||
|
const fill = toast.el?.querySelector('.met-progress-fill') as HTMLElement | null;
|
||||||
|
const text = toast.el?.querySelector('.met-progress-text') as HTMLElement | null;
|
||||||
|
|
||||||
|
if (fill) {
|
||||||
|
fill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
||||||
|
}
|
||||||
|
if (text) {
|
||||||
|
text.textContent = `${Math.round(percent)}%`;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
complete(message?: string) {
|
||||||
|
this.setProgress(100);
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.update({
|
||||||
|
type: 'success',
|
||||||
|
message: message || t('success'),
|
||||||
|
html: '',
|
||||||
|
});
|
||||||
|
setTimeout(() => toast.close(), 1000);
|
||||||
|
}, 300);
|
||||||
|
},
|
||||||
|
|
||||||
|
error(message?: string) {
|
||||||
|
toast.update({
|
||||||
|
type: 'error',
|
||||||
|
message: message || t('error'),
|
||||||
|
html: '',
|
||||||
|
});
|
||||||
|
setTimeout(() => toast.close(), 2000);
|
||||||
|
},
|
||||||
|
|
||||||
|
dismiss() {
|
||||||
|
toast.close();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
countdown(message: string, seconds = 10, opts: CountdownOptions = {}): CountdownControl {
|
||||||
|
if (typeof message !== 'string') {
|
||||||
|
console.error('MeToast.countdown: message must be a string');
|
||||||
|
return { id: '', cancel: () => {}, pause: () => {}, resume: () => {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
let remaining = Math.max(1, parseInt(String(seconds)) || 10);
|
||||||
|
let timer: ReturnType<typeof setInterval> | null = null;
|
||||||
|
|
||||||
|
const toast = this._emit({
|
||||||
|
...opts,
|
||||||
|
type: opts.type || 'warning',
|
||||||
|
message: message.replace(/\{seconds\}/g, String(remaining)),
|
||||||
|
duration: 0,
|
||||||
|
closeButton: true,
|
||||||
|
showProgress: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const tick = (): void => {
|
||||||
|
remaining--;
|
||||||
|
|
||||||
|
if (remaining <= 0) {
|
||||||
|
if (timer !== null) clearInterval(timer);
|
||||||
|
toast.close();
|
||||||
|
|
||||||
|
if (typeof opts.onComplete === 'function') {
|
||||||
|
try { opts.onComplete(); } catch (e) { console.error('countdown onComplete error:', e); }
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.update({
|
||||||
|
message: message.replace(/\{seconds\}/g, String(remaining)),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
timer = setInterval(tick, 1000);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: toast.id,
|
||||||
|
cancel() { if (timer !== null) clearInterval(timer); toast.close(); },
|
||||||
|
pause() { if (timer !== null) clearInterval(timer); },
|
||||||
|
resume() { if (timer !== null) clearInterval(timer); timer = setInterval(tick, 1000); },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
action(messageOrOpts: string | ToastOptions, actions: ActionButton[] = [], opts: ToastOptions = {}): ActionControl {
|
||||||
|
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||||||
|
normalized.duration = opts.duration ?? 0;
|
||||||
|
normalized.closeButton = opts.closeButton ?? true;
|
||||||
|
|
||||||
|
const toast = this._emit({
|
||||||
|
...normalized,
|
||||||
|
html: (normalized.html || '') + actionHTML(actions),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(actions)) {
|
||||||
|
setTimeout(() => {
|
||||||
|
actions.forEach((a, i) => {
|
||||||
|
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
|
||||||
|
if (btn && typeof a.onClick === 'function') {
|
||||||
|
btn.addEventListener('click', () => {
|
||||||
|
try { a.onClick(toast); } catch (e) { console.error('Action onClick error:', e); }
|
||||||
|
if (a.close !== false) toast.close();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { id: toast.id, toast, dismiss: () => toast.close() };
|
||||||
|
},
|
||||||
|
|
||||||
|
queue(messages: Array<string | ToastOptions>, opts: QueueOptions = {}): QueueControl {
|
||||||
|
if (!Array.isArray(messages)) {
|
||||||
|
console.error('MeToast.queue: messages must be an array');
|
||||||
|
const p = Promise.resolve();
|
||||||
|
return { then: (fn) => p.then(fn), catch: (rj) => p.catch(rj), cancel: () => {} };
|
||||||
|
}
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const promise = new Promise<void>((resolve) => {
|
||||||
|
let index = 0;
|
||||||
|
const delay = opts.delay || 1000;
|
||||||
|
const userOnClose = opts.onClose;
|
||||||
|
|
||||||
|
const showNext = (): void => {
|
||||||
|
if (cancelled || index >= messages.length) {
|
||||||
|
resolve();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = messages[index];
|
||||||
|
index++;
|
||||||
|
|
||||||
|
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
|
||||||
|
const msgObj: ToastOptions = (typeof message === 'object' && message !== null) ? message as ToastOptions : {};
|
||||||
|
const msgOnClose = msgObj.onClose;
|
||||||
|
|
||||||
|
this._emit({
|
||||||
|
...opts,
|
||||||
|
...msgObj,
|
||||||
|
message: msg,
|
||||||
|
duration: msgObj.duration || opts.duration || 3000,
|
||||||
|
onClose: (toast: ToastInstance) => {
|
||||||
|
if (typeof msgOnClose === 'function') {
|
||||||
|
try { msgOnClose(toast); } catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
if (typeof userOnClose === 'function') {
|
||||||
|
try { userOnClose(toast); } catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
setTimeout(showNext, delay);
|
||||||
|
},
|
||||||
|
} as ToastOptions);
|
||||||
|
};
|
||||||
|
|
||||||
|
showNext();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
then: (fn, rj) => promise.then(fn, rj),
|
||||||
|
catch: (rj) => promise.catch(rj),
|
||||||
|
cancel: () => { cancelled = true; },
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
stack(messages: Array<string | ToastOptions>, opts: StackOptions = {}): void {
|
||||||
|
if (!Array.isArray(messages)) {
|
||||||
|
console.error('MeToast.stack: messages must be an array');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.forEach((message, index) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
|
||||||
|
|
||||||
|
this._emit({
|
||||||
|
...opts,
|
||||||
|
...((typeof message === 'object' && message !== null) ? message as ToastOptions : {}),
|
||||||
|
message: msg,
|
||||||
|
} as ToastOptions);
|
||||||
|
}, index * (opts.stagger || 100));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
dismiss(id?: string): void {
|
||||||
|
if (id) {
|
||||||
|
const t = this._toasts.get(id);
|
||||||
|
if (t) t.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._toasts.forEach(t => t.close());
|
||||||
|
},
|
||||||
|
|
||||||
|
clear(position?: string): void {
|
||||||
|
this._toasts.forEach(t => {
|
||||||
|
if (!position || t.config.position === position) t.close();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.dismiss();
|
||||||
|
|
||||||
|
if (typeof document !== 'undefined') {
|
||||||
|
const containers = document.querySelectorAll('.met-container');
|
||||||
|
containers.forEach(c => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||||
|
|
||||||
|
const style = document.getElementById('metona-toast-styles');
|
||||||
|
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._toasts.clear();
|
||||||
|
_containerCache.clear();
|
||||||
|
},
|
||||||
|
|
||||||
|
getAll(): Map<string, ToastInstance> {
|
||||||
|
return new Map(this._toasts);
|
||||||
|
},
|
||||||
|
|
||||||
|
count(): number {
|
||||||
|
return this._toasts.size;
|
||||||
|
},
|
||||||
|
|
||||||
|
group(name: string): GroupAPI {
|
||||||
|
const self = this;
|
||||||
|
const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action'];
|
||||||
|
const g: GroupAPI = { _group: name } as unknown as GroupAPI;
|
||||||
|
methods.forEach(m => {
|
||||||
|
(g as unknown as Record<string, (...args: unknown[]) => unknown>)[m] = (...args: unknown[]) => {
|
||||||
|
const lastArg = args[args.length - 1];
|
||||||
|
const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg);
|
||||||
|
const selfMethods = self as unknown as Record<string, (...a: unknown[]) => unknown>;
|
||||||
|
if (isObj && args.length === 1) {
|
||||||
|
return selfMethods[m]({ ...(lastArg as Record<string, unknown>), group: name });
|
||||||
|
}
|
||||||
|
if (isObj) {
|
||||||
|
args.pop();
|
||||||
|
return selfMethods[m](...args, { ...(lastArg as Record<string, unknown>), group: name });
|
||||||
|
}
|
||||||
|
return selfMethods[m](...args, { group: name });
|
||||||
|
};
|
||||||
|
});
|
||||||
|
g.dismiss = () => self.dismissGroup(name);
|
||||||
|
g.count = () => self._groupCount(name);
|
||||||
|
return g;
|
||||||
|
},
|
||||||
|
|
||||||
|
dismissGroup(name: string): void {
|
||||||
|
this._toasts.forEach(t => { if (t.group === name) t.close(); });
|
||||||
|
},
|
||||||
|
|
||||||
|
_groupCount(name: string): number {
|
||||||
|
let c = 0;
|
||||||
|
this._toasts.forEach(t => { if (t.group === name) c++; });
|
||||||
|
return c;
|
||||||
|
},
|
||||||
|
|
||||||
|
getToasts(): ToastInstance[] {
|
||||||
|
return Array.from(this._toasts.values());
|
||||||
|
},
|
||||||
|
|
||||||
|
hasToasts(): boolean {
|
||||||
|
return this._toasts.size > 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getToast(id: string): ToastInstance | null {
|
||||||
|
if (!id) return null;
|
||||||
|
return this._toasts.get(id) || null;
|
||||||
|
},
|
||||||
|
|
||||||
|
closeAll(): void {
|
||||||
|
this._toasts.forEach(t => t.close());
|
||||||
|
},
|
||||||
|
|
||||||
|
clearAll(): void {
|
||||||
|
this._toasts.forEach(t => t.close());
|
||||||
|
},
|
||||||
|
|
||||||
|
pauseAll(): void {
|
||||||
|
this._toasts.forEach(t => t._pause());
|
||||||
|
},
|
||||||
|
|
||||||
|
resumeAll(): void {
|
||||||
|
this._toasts.forEach(t => t._resume());
|
||||||
|
},
|
||||||
|
|
||||||
|
updateAll(partial: Partial<ToastOptions>): void {
|
||||||
|
if (partial && typeof partial === 'object') {
|
||||||
|
this._toasts.forEach(t => t.update(partial));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
|
||||||
|
if (typeof predicate !== 'function') return [];
|
||||||
|
return Array.from(this._toasts.values()).filter(predicate);
|
||||||
|
},
|
||||||
|
|
||||||
|
findByType(type: string): ToastInstance[] {
|
||||||
|
return this.findToasts(t => t.type === type);
|
||||||
|
},
|
||||||
|
|
||||||
|
findByPosition(position: string): ToastInstance[] {
|
||||||
|
return this.findToasts(t => t.config.position === position);
|
||||||
|
},
|
||||||
|
|
||||||
|
// ====== 以下由 index.ts 增强 ======
|
||||||
|
|
||||||
|
init(_options?: Record<string, unknown>): MeToast { return this; },
|
||||||
|
getStatus() { return { version: '0.2.0', toasts: 0, theme: '', locale: '', plugins: [], animations: 0 }; },
|
||||||
|
getConfig(): ToastConfig { return { ...this._config }; },
|
||||||
|
updateConfig(_config: Partial<ToastConfig>): MeToast { return this; },
|
||||||
|
resetConfig(): MeToast { return this; },
|
||||||
|
use(_plugin: string | Record<string, unknown>, _options?: Record<string, unknown>): MeToast { return this; },
|
||||||
|
|
||||||
|
animations: null as unknown as MeToast['animations'],
|
||||||
|
themes: null as unknown as MeToast['themes'],
|
||||||
|
i18n: null as unknown as MeToast['i18n'],
|
||||||
|
plugins: null as unknown as MeToast['plugins'],
|
||||||
|
presetPlugins: {} as MeToast['presetPlugins'],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 连接 Toast 静态回调到 meToast 实例
|
||||||
|
Toast._onError = (info: ErrorInfo) => {
|
||||||
|
const onError = meToast._config.onError;
|
||||||
|
if (typeof onError === 'function') {
|
||||||
|
try { onError(info); } catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Toast._removeToast = (id: string) => {
|
||||||
|
meToast._toasts.delete(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { meToast as default, meToast };
|
||||||
@@ -1,72 +1,51 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Constants - 常量定义
|
* MetonaToast Constants — 常量定义
|
||||||
* @module constants
|
* @module constants
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 默认配置、颜色、动画、主题等常量
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { ICONS } from './icons.js';
|
||||||
|
import { LOCALES } from './locales.js';
|
||||||
|
|
||||||
|
export { ICONS, LOCALES };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认配置
|
* 默认配置
|
||||||
*/
|
*/
|
||||||
export const DEFAULTS = Object.freeze({
|
export const DEFAULTS = Object.freeze({
|
||||||
// 基础配置
|
|
||||||
position: 'top-right',
|
position: 'top-right',
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
max: 6,
|
max: 6,
|
||||||
gap: 12,
|
gap: 12,
|
||||||
offset: 24,
|
offset: 24,
|
||||||
|
|
||||||
// 交互配置
|
|
||||||
pauseOnHover: true,
|
pauseOnHover: true,
|
||||||
closeOnClick: true,
|
closeOnClick: true,
|
||||||
draggable: true,
|
draggable: true,
|
||||||
|
|
||||||
// 显示配置
|
|
||||||
showProgress: true,
|
showProgress: true,
|
||||||
progressDirection: 'horizontal',
|
progressDirection: 'horizontal' as const,
|
||||||
icon: true,
|
icon: true,
|
||||||
closeButton: true,
|
closeButton: true,
|
||||||
|
|
||||||
// 主题配置
|
|
||||||
theme: 'auto',
|
theme: 'auto',
|
||||||
animation: 'slide',
|
animation: 'slide',
|
||||||
|
|
||||||
// 布局配置
|
|
||||||
zIndex: 9999,
|
zIndex: 9999,
|
||||||
width: 360,
|
width: 360,
|
||||||
|
|
||||||
// 自定义配置
|
|
||||||
className: '',
|
className: '',
|
||||||
style: {},
|
style: {} as Record<string, string>,
|
||||||
|
onShow: null as ((...args: unknown[]) => void) | null,
|
||||||
// 回调函数
|
onClose: null as ((...args: unknown[]) => void) | null,
|
||||||
onShow: null,
|
onClick: null as ((...args: unknown[]) => void) | null,
|
||||||
onClose: null,
|
onUpdate: null as ((...args: unknown[]) => void) | null,
|
||||||
onClick: null,
|
|
||||||
onUpdate: null,
|
|
||||||
|
|
||||||
// 高级
|
|
||||||
resetTimerOnUpdate: false,
|
resetTimerOnUpdate: false,
|
||||||
notifyWhenHidden: false,
|
notifyWhenHidden: false,
|
||||||
|
onError: null as ((...args: unknown[]) => void) | null,
|
||||||
// 错误处理
|
|
||||||
onError: null,
|
|
||||||
|
|
||||||
// 国际化
|
|
||||||
locale: 'zh-CN',
|
locale: 'zh-CN',
|
||||||
|
plugins: [] as Array<string | Record<string, unknown>>,
|
||||||
// 插件
|
|
||||||
plugins: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Icons — 从 icons.js 导入并重新导出(保持向后兼容)
|
|
||||||
import { ICONS } from './icons.js';
|
|
||||||
export { ICONS };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类型颜色配置
|
* 类型颜色配置
|
||||||
*/
|
*/
|
||||||
export const TYPE_COLORS = {
|
export const TYPE_COLORS: Record<string, { fg: string; bg: string }> = {
|
||||||
success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
||||||
error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
||||||
warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
||||||
@@ -179,70 +158,75 @@ export const TYPE_COLORS = {
|
|||||||
/**
|
/**
|
||||||
* 动画配置
|
* 动画配置
|
||||||
*/
|
*/
|
||||||
export const ANIMATIONS = {
|
export const ANIMATIONS: Record<string, {
|
||||||
|
enter: Record<string, string>;
|
||||||
|
leave: Record<string, string>;
|
||||||
|
duration: number;
|
||||||
|
easing: string;
|
||||||
|
}> = {
|
||||||
slide: {
|
slide: {
|
||||||
enter: { transform: 'translateX(80px)', opacity: 0 },
|
enter: { transform: 'translateX(80px)', opacity: '0' },
|
||||||
leave: { transform: 'translateX(120%)', opacity: 0 },
|
leave: { transform: 'translateX(120%)', opacity: '0' },
|
||||||
duration: 400,
|
duration: 400,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
fade: {
|
fade: {
|
||||||
enter: { opacity: 0, filter: 'blur(3px)' },
|
enter: { opacity: '0', filter: 'blur(3px)' },
|
||||||
leave: { opacity: 0, filter: 'blur(3px)' },
|
leave: { opacity: '0', filter: 'blur(3px)' },
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: 'ease',
|
easing: 'ease',
|
||||||
},
|
},
|
||||||
scale: {
|
scale: {
|
||||||
enter: { transform: 'scale(0.55)', opacity: 0 },
|
enter: { transform: 'scale(0.55)', opacity: '0' },
|
||||||
leave: { transform: 'scale(0.55)', opacity: 0 },
|
leave: { transform: 'scale(0.55)', opacity: '0' },
|
||||||
duration: 450,
|
duration: 450,
|
||||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
},
|
},
|
||||||
bounce: {
|
bounce: {
|
||||||
enter: { transform: 'translateY(-80px)', opacity: 0 },
|
enter: { transform: 'translateY(-80px)', opacity: '0' },
|
||||||
leave: { transform: 'translateY(20px)', opacity: 0 },
|
leave: { transform: 'translateY(20px)', opacity: '0' },
|
||||||
duration: 650,
|
duration: 650,
|
||||||
easing: 'ease',
|
easing: 'ease',
|
||||||
},
|
},
|
||||||
flip: {
|
flip: {
|
||||||
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: 0 },
|
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: '0' },
|
||||||
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: 0 },
|
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: '0' },
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
rotate: {
|
rotate: {
|
||||||
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: 0 },
|
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: '0' },
|
||||||
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: 0 },
|
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: '0' },
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
zoom: {
|
zoom: {
|
||||||
enter: { transform: 'scale(0.1)', opacity: 0 },
|
enter: { transform: 'scale(0.1)', opacity: '0' },
|
||||||
leave: { transform: 'scale(0.1)', opacity: 0 },
|
leave: { transform: 'scale(0.1)', opacity: '0' },
|
||||||
duration: 500,
|
duration: 500,
|
||||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||||
},
|
},
|
||||||
slideUp: {
|
slideUp: {
|
||||||
enter: { transform: 'translateY(60px)', opacity: 0 },
|
enter: { transform: 'translateY(60px)', opacity: '0' },
|
||||||
leave: { transform: 'translateY(-120%)', opacity: 0 },
|
leave: { transform: 'translateY(-120%)', opacity: '0' },
|
||||||
duration: 400,
|
duration: 400,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
slideDown: {
|
slideDown: {
|
||||||
enter: { transform: 'translateY(-60px)', opacity: 0 },
|
enter: { transform: 'translateY(-60px)', opacity: '0' },
|
||||||
leave: { transform: 'translateY(120%)', opacity: 0 },
|
leave: { transform: 'translateY(120%)', opacity: '0' },
|
||||||
duration: 400,
|
duration: 400,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
slideLeft: {
|
slideLeft: {
|
||||||
enter: { transform: 'translateX(-90px)', opacity: 0 },
|
enter: { transform: 'translateX(-90px)', opacity: '0' },
|
||||||
leave: { transform: 'translateX(120%)', opacity: 0 },
|
leave: { transform: 'translateX(120%)', opacity: '0' },
|
||||||
duration: 400,
|
duration: 400,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
slideRight: {
|
slideRight: {
|
||||||
enter: { transform: 'translateX(90px)', opacity: 0 },
|
enter: { transform: 'translateX(90px)', opacity: '0' },
|
||||||
leave: { transform: 'translateX(-120%)', opacity: 0 },
|
leave: { transform: 'translateX(-120%)', opacity: '0' },
|
||||||
duration: 400,
|
duration: 400,
|
||||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||||
},
|
},
|
||||||
@@ -251,7 +235,15 @@ export const ANIMATIONS = {
|
|||||||
/**
|
/**
|
||||||
* 主题配置
|
* 主题配置
|
||||||
*/
|
*/
|
||||||
export const THEMES = {
|
export const THEMES: Record<string, {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
shadow: string;
|
||||||
|
hoverShadow: string;
|
||||||
|
progressBg: string;
|
||||||
|
closeHoverBg: string;
|
||||||
|
}> = {
|
||||||
light: {
|
light: {
|
||||||
bg: 'rgba(255, 255, 255, 0.96)',
|
bg: 'rgba(255, 255, 255, 0.96)',
|
||||||
text: '#1f2937',
|
text: '#1f2937',
|
||||||
@@ -291,7 +283,7 @@ export const THEMES = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 位置配置
|
* 位置列表
|
||||||
*/
|
*/
|
||||||
export const POSITIONS = [
|
export const POSITIONS = [
|
||||||
'top-left',
|
'top-left',
|
||||||
@@ -300,10 +292,10 @@ export const POSITIONS = [
|
|||||||
'bottom-left',
|
'bottom-left',
|
||||||
'bottom-center',
|
'bottom-center',
|
||||||
'bottom-right',
|
'bottom-right',
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 类型配置
|
* 类型列表
|
||||||
*/
|
*/
|
||||||
export const TYPES = [
|
export const TYPES = [
|
||||||
'default', 'success', 'error', 'warning', 'info', 'loading',
|
'default', 'success', 'error', 'warning', 'info', 'loading',
|
||||||
@@ -322,23 +314,19 @@ export const TYPES = [
|
|||||||
'checkCircle', 'xCircle', 'alertCircle', 'infoCircle', 'helpCircle',
|
'checkCircle', 'xCircle', 'alertCircle', 'infoCircle', 'helpCircle',
|
||||||
'alertTriangle', 'checkSquare', 'square', 'circle', 'triangle',
|
'alertTriangle', 'checkSquare', 'square', 'circle', 'triangle',
|
||||||
'hexagon', 'octagon', 'pentagon', 'diamond',
|
'hexagon', 'octagon', 'pentagon', 'diamond',
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
// Locales — 从 locales.js 导入并重新导出(保持向后兼容)
|
|
||||||
import { LOCALES } from './locales.js';
|
|
||||||
export { LOCALES };
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 进度条方向
|
* 进度条方向
|
||||||
*/
|
*/
|
||||||
export const PROGRESS_DIRECTIONS = ['horizontal', 'vertical'];
|
export const PROGRESS_DIRECTIONS = ['horizontal', 'vertical'] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 动画类型
|
* 动画类型
|
||||||
*/
|
*/
|
||||||
export const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
export const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'] as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题类型
|
* 主题类型
|
||||||
*/
|
*/
|
||||||
export const THEME_TYPES = ['light', 'dark', 'auto'];
|
export const THEME_TYPES = ['light', 'dark', 'auto'] as const;
|
||||||
-1243
File diff suppressed because it is too large
Load Diff
+80
-247
@@ -1,30 +1,28 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast i18n - 国际化管理
|
* MetonaToast i18n — 国际化管理
|
||||||
* @module i18n
|
* @module i18n
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 多语言支持、语言切换和翻译管理
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { LOCALES } from './constants.js';
|
import { LOCALES } from './constants.js';
|
||||||
|
import type { I18nUtils, LocaleInfo } from './types.js';
|
||||||
|
|
||||||
// 当前语言状态
|
// 当前语言状态
|
||||||
let currentLocale = 'zh-CN';
|
let currentLocale = 'zh-CN';
|
||||||
let localeListeners = new Set();
|
const localeListeners: Set<(locale: string) => void> = new Set();
|
||||||
let fallbackLocale = 'zh-CN';
|
let fallbackLocale = 'zh-CN';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前语言
|
* 获取当前语言
|
||||||
* @returns {string} 当前语言
|
|
||||||
*/
|
*/
|
||||||
export const getCurrentLocale = () => {
|
export const getCurrentLocale = (): string => {
|
||||||
return currentLocale;
|
return currentLocale;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置当前语言
|
* 设置当前语言
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
*/
|
*/
|
||||||
export const setCurrentLocale = (locale) => {
|
export const setCurrentLocale = (locale: string): void => {
|
||||||
if (!LOCALES[locale]) {
|
if (!LOCALES[locale]) {
|
||||||
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
||||||
locale = fallbackLocale;
|
locale = fallbackLocale;
|
||||||
@@ -37,39 +35,31 @@ export const setCurrentLocale = (locale) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取回退语言
|
* 获取回退语言
|
||||||
* @returns {string} 回退语言
|
|
||||||
*/
|
*/
|
||||||
export const getFallbackLocale = () => {
|
export const getFallbackLocale = (): string => {
|
||||||
return fallbackLocale;
|
return fallbackLocale;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置回退语言
|
* 设置回退语言
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
*/
|
*/
|
||||||
export const setFallbackLocale = (locale) => {
|
export const setFallbackLocale = (locale: string): void => {
|
||||||
if (!LOCALES[locale]) {
|
if (!LOCALES[locale]) {
|
||||||
console.warn(`Fallback locale "${locale}" not found`);
|
console.warn(`Fallback locale "${locale}" not found`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackLocale = locale;
|
fallbackLocale = locale;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 翻译函数
|
* 翻译函数
|
||||||
* @param {string} key - 翻译键
|
|
||||||
* @param {Object} params - 插值参数
|
|
||||||
* @returns {string} 翻译后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const t = (key, params = {}) => {
|
export const t = (key: string, params: Record<string, string | number> = {}): string => {
|
||||||
// 尝试当前语言
|
|
||||||
const currentTranslation = getTranslation(currentLocale, key);
|
const currentTranslation = getTranslation(currentLocale, key);
|
||||||
if (currentTranslation !== undefined) {
|
if (currentTranslation !== undefined) {
|
||||||
return interpolate(currentTranslation, params);
|
return interpolate(currentTranslation, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试回退语言
|
|
||||||
if (currentLocale !== fallbackLocale) {
|
if (currentLocale !== fallbackLocale) {
|
||||||
const fallbackTranslation = getTranslation(fallbackLocale, key);
|
const fallbackTranslation = getTranslation(fallbackLocale, key);
|
||||||
if (fallbackTranslation !== undefined) {
|
if (fallbackTranslation !== undefined) {
|
||||||
@@ -77,28 +67,23 @@ export const t = (key, params = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 返回键名
|
|
||||||
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
|
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
|
||||||
return key;
|
return key;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取翻译
|
* 获取翻译
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @param {string} key - 翻译键
|
|
||||||
* @returns {string|undefined} 翻译字符串
|
|
||||||
*/
|
*/
|
||||||
const getTranslation = (locale, key) => {
|
const getTranslation = (locale: string, key: string): string | undefined => {
|
||||||
const localeData = LOCALES[locale];
|
const localeData = LOCALES[locale];
|
||||||
if (!localeData) return undefined;
|
if (!localeData) return undefined;
|
||||||
|
|
||||||
// 支持嵌套键 (如 'common.close')
|
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let result = localeData;
|
let result: unknown = localeData;
|
||||||
|
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
if (result && typeof result === 'object' && k in result) {
|
if (result && typeof result === 'object' && k in (result as Record<string, unknown>)) {
|
||||||
result = result[k];
|
result = (result as Record<string, unknown>)[k];
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@@ -109,58 +94,45 @@ const getTranslation = (locale, key) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 插值函数
|
* 插值函数
|
||||||
* @param {string} str - 包含占位符的字符串
|
|
||||||
* @param {Object} params - 插值参数
|
|
||||||
* @returns {string} 插值后的字符串
|
|
||||||
*/
|
*/
|
||||||
const interpolate = (str, params) => {
|
const interpolate = (str: string, params: Record<string, string | number>): string => {
|
||||||
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
return str.replace(/\{(\w+)\}/g, (_match, key: string) => {
|
||||||
return params[key] !== undefined ? params[key] : match;
|
return params[key] !== undefined ? String(params[key]) : _match;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查翻译是否存在
|
* 检查翻译是否存在
|
||||||
* @param {string} key - 翻译键
|
|
||||||
* @returns {boolean} 是否存在
|
|
||||||
*/
|
*/
|
||||||
export const hasTranslation = (key) => {
|
export const hasTranslation = (key: string): boolean => {
|
||||||
return getTranslation(currentLocale, key) !== undefined ||
|
return getTranslation(currentLocale, key) !== undefined ||
|
||||||
getTranslation(fallbackLocale, key) !== undefined;
|
getTranslation(fallbackLocale, key) !== undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有翻译
|
* 获取所有翻译
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @returns {Object} 翻译对象
|
|
||||||
*/
|
*/
|
||||||
export const getTranslations = (locale) => {
|
export const getTranslations = (locale: string): Record<string, unknown> => {
|
||||||
return LOCALES[locale] || {};
|
return LOCALES[locale] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加翻译
|
* 添加翻译
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @param {Object} translations - 翻译对象
|
|
||||||
*/
|
*/
|
||||||
export const addTranslations = (locale, translations) => {
|
export const addTranslations = (locale: string, translations: Record<string, unknown>): void => {
|
||||||
if (!LOCALES[locale]) {
|
if (!LOCALES[locale]) {
|
||||||
LOCALES[locale] = {};
|
LOCALES[locale] = {};
|
||||||
}
|
}
|
||||||
|
deepMerge(LOCALES[locale], translations as Record<string, string>);
|
||||||
deepMerge(LOCALES[locale], translations);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 深度合并对象
|
* 深度合并对象
|
||||||
* @param {Object} target - 目标对象
|
|
||||||
* @param {Object} source - 源对象
|
|
||||||
* @returns {Object} 合并后的对象
|
|
||||||
*/
|
*/
|
||||||
const deepMerge = (target, source) => {
|
const deepMerge = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
|
||||||
for (const key in source) {
|
for (const key in source) {
|
||||||
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
||||||
deepMerge(target[key], source[key]);
|
deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
|
||||||
} else {
|
} else {
|
||||||
target[key] = source[key];
|
target[key] = source[key];
|
||||||
}
|
}
|
||||||
@@ -170,19 +142,17 @@ const deepMerge = (target, source) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除翻译
|
* 移除翻译
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @param {string} key - 翻译键
|
|
||||||
*/
|
*/
|
||||||
export const removeTranslation = (locale, key) => {
|
export const removeTranslation = (locale: string, key: string): void => {
|
||||||
const localeData = LOCALES[locale];
|
const localeData = LOCALES[locale];
|
||||||
if (!localeData) return;
|
if (!localeData) return;
|
||||||
|
|
||||||
const keys = key.split('.');
|
const keys = key.split('.');
|
||||||
let current = localeData;
|
let current: Record<string, unknown> = localeData;
|
||||||
|
|
||||||
for (let i = 0; i < keys.length - 1; i++) {
|
for (let i = 0; i < keys.length - 1; i++) {
|
||||||
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
|
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
|
||||||
current = current[keys[i]];
|
current = current[keys[i]] as Record<string, unknown>;
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,9 +163,8 @@ export const removeTranslation = (locale, key) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除翻译
|
* 清除翻译
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
*/
|
*/
|
||||||
export const clearTranslations = (locale) => {
|
export const clearTranslations = (locale: string): void => {
|
||||||
if (LOCALES[locale]) {
|
if (LOCALES[locale]) {
|
||||||
LOCALES[locale] = {};
|
LOCALES[locale] = {};
|
||||||
}
|
}
|
||||||
@@ -203,28 +172,23 @@ export const clearTranslations = (locale) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取支持的语言列表
|
* 获取支持的语言列表
|
||||||
* @returns {Array} 语言代码数组
|
|
||||||
*/
|
*/
|
||||||
export const getSupportedLocales = () => {
|
export const getSupportedLocales = (): string[] => {
|
||||||
return Object.keys(LOCALES);
|
return Object.keys(LOCALES);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查语言是否支持
|
* 检查语言是否支持
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @returns {boolean} 是否支持
|
|
||||||
*/
|
*/
|
||||||
export const isLocaleSupported = (locale) => {
|
export const isLocaleSupported = (locale: string): boolean => {
|
||||||
return locale in LOCALES;
|
return locale in LOCALES;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取语言名称
|
* 获取语言名称
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @returns {string} 语言名称
|
|
||||||
*/
|
*/
|
||||||
export const getLocaleName = (locale) => {
|
export const getLocaleName = (locale: string): string => {
|
||||||
const names = {
|
const names: Record<string, string> = {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
'zh-TW': '繁體中文',
|
'zh-TW': '繁體中文',
|
||||||
'en-US': 'English (US)',
|
'en-US': 'English (US)',
|
||||||
@@ -310,20 +274,16 @@ export const getLocaleName = (locale) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取语言方向
|
* 获取语言方向
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @returns {string} 语言方向 ('ltr' 或 'rtl')
|
|
||||||
*/
|
*/
|
||||||
export const getLocaleDirection = (locale) => {
|
export const getLocaleDirection = (locale: string): 'ltr' | 'rtl' => {
|
||||||
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
||||||
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取语言信息
|
* 获取语言信息
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
* @returns {Object} 语言信息
|
|
||||||
*/
|
*/
|
||||||
export const getLocaleInfo = (locale) => {
|
export const getLocaleInfo = (locale: string): LocaleInfo => {
|
||||||
return {
|
return {
|
||||||
code: locale,
|
code: locale,
|
||||||
name: getLocaleName(locale),
|
name: getLocaleName(locale),
|
||||||
@@ -336,17 +296,15 @@ export const getLocaleInfo = (locale) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有语言信息
|
* 获取所有语言信息
|
||||||
* @returns {Array} 语言信息数组
|
|
||||||
*/
|
*/
|
||||||
export const getAllLocaleInfo = () => {
|
export const getAllLocaleInfo = (): LocaleInfo[] => {
|
||||||
return getSupportedLocales().map(getLocaleInfo);
|
return getSupportedLocales().map(getLocaleInfo);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存语言到本地存储
|
* 保存语言到本地存储
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
*/
|
*/
|
||||||
export const saveLocale = (locale) => {
|
export const saveLocale = (locale: string): void => {
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('metona-toast-locale', locale);
|
localStorage.setItem('metona-toast-locale', locale);
|
||||||
@@ -358,9 +316,8 @@ export const saveLocale = (locale) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从本地存储加载语言
|
* 从本地存储加载语言
|
||||||
* @returns {string} 语言代码
|
|
||||||
*/
|
*/
|
||||||
export const loadLocale = () => {
|
export const loadLocale = (): string => {
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem('metona-toast-locale') || getDefaultLocale();
|
return localStorage.getItem('metona-toast-locale') || getDefaultLocale();
|
||||||
@@ -373,17 +330,14 @@ export const loadLocale = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取默认语言
|
* 获取默认语言
|
||||||
* @returns {string} 语言代码
|
|
||||||
*/
|
*/
|
||||||
export const getDefaultLocale = () => {
|
export const getDefaultLocale = (): string => {
|
||||||
if (typeof navigator !== 'undefined') {
|
if (typeof navigator !== 'undefined') {
|
||||||
// 尝试从浏览器获取语言
|
const browserLocale = navigator.language || (navigator as unknown as { userLanguage?: string }).userLanguage;
|
||||||
const browserLocale = navigator.language || navigator.userLanguage;
|
|
||||||
if (browserLocale && isLocaleSupported(browserLocale)) {
|
if (browserLocale && isLocaleSupported(browserLocale)) {
|
||||||
return browserLocale;
|
return browserLocale;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试语言代码的前两位
|
|
||||||
const shortLocale = browserLocale?.split('-')[0];
|
const shortLocale = browserLocale?.split('-')[0];
|
||||||
if (shortLocale && isLocaleSupported(shortLocale)) {
|
if (shortLocale && isLocaleSupported(shortLocale)) {
|
||||||
return shortLocale;
|
return shortLocale;
|
||||||
@@ -396,27 +350,23 @@ export const getDefaultLocale = () => {
|
|||||||
/**
|
/**
|
||||||
* 初始化国际化系统
|
* 初始化国际化系统
|
||||||
*/
|
*/
|
||||||
export const initI18n = () => {
|
export const initI18n = (): void => {
|
||||||
const savedLocale = loadLocale();
|
const savedLocale = loadLocale();
|
||||||
setCurrentLocale(savedLocale);
|
setCurrentLocale(savedLocale);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换语言
|
* 切换语言
|
||||||
* @param {string} locale - 新语言
|
|
||||||
*/
|
*/
|
||||||
export const switchLocale = (locale) => {
|
export const switchLocale = (locale: string): void => {
|
||||||
setCurrentLocale(locale);
|
setCurrentLocale(locale);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加语言监听器
|
* 添加语言监听器
|
||||||
* @param {Function} listener - 监听器函数
|
|
||||||
* @returns {Function} 移除监听器函数
|
|
||||||
*/
|
*/
|
||||||
export const addLocaleListener = (listener) => {
|
export const addLocaleListener = (listener: (locale: string) => void): () => void => {
|
||||||
localeListeners.add(listener);
|
localeListeners.add(listener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
localeListeners.delete(listener);
|
localeListeners.delete(listener);
|
||||||
};
|
};
|
||||||
@@ -424,17 +374,15 @@ export const addLocaleListener = (listener) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除语言监听器
|
* 移除语言监听器
|
||||||
* @param {Function} listener - 监听器函数
|
|
||||||
*/
|
*/
|
||||||
export const removeLocaleListener = (listener) => {
|
export const removeLocaleListener = (listener: (locale: string) => void): void => {
|
||||||
localeListeners.delete(listener);
|
localeListeners.delete(listener);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知语言监听器
|
* 通知语言监听器
|
||||||
* @param {string} locale - 语言代码
|
|
||||||
*/
|
*/
|
||||||
const notifyLocaleListeners = (locale) => {
|
const notifyLocaleListeners = (locale: string): void => {
|
||||||
localeListeners.forEach((listener) => {
|
localeListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
listener(locale);
|
listener(locale);
|
||||||
@@ -447,17 +395,14 @@ const notifyLocaleListeners = (locale) => {
|
|||||||
/**
|
/**
|
||||||
* 清除所有语言监听器
|
* 清除所有语言监听器
|
||||||
*/
|
*/
|
||||||
export const clearLocaleListeners = () => {
|
export const clearLocaleListeners = (): void => {
|
||||||
localeListeners.clear();
|
localeListeners.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化数字
|
* 格式化数字
|
||||||
* @param {number} number - 数字
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatNumber = (number, options = {}) => {
|
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 (e) {
|
||||||
@@ -467,12 +412,8 @@ export const formatNumber = (number, options = {}) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化货币
|
* 格式化货币
|
||||||
* @param {number} amount - 金额
|
|
||||||
* @param {string} currency - 货币代码
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
export const formatCurrency = (amount: number, currency = 'USD', options: Intl.NumberFormatOptions = {}): string => {
|
||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(currentLocale, {
|
return new Intl.NumberFormat(currentLocale, {
|
||||||
style: 'currency',
|
style: 'currency',
|
||||||
@@ -486,11 +427,8 @@ export const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化百分比
|
* 格式化百分比
|
||||||
* @param {number} value - 值
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatPercent = (value, options = {}) => {
|
export const formatPercent = (value: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||||
try {
|
try {
|
||||||
return new Intl.NumberFormat(currentLocale, {
|
return new Intl.NumberFormat(currentLocale, {
|
||||||
style: 'percent',
|
style: 'percent',
|
||||||
@@ -505,26 +443,20 @@ export const formatPercent = (value, options = {}) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化日期
|
* 格式化日期
|
||||||
* @param {Date|number|string} date - 日期
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatDate = (date, options = {}) => {
|
export const formatDate = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
|
||||||
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 (e) {
|
||||||
return date.toString();
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间
|
* 格式化时间
|
||||||
* @param {Date|number|string} date - 日期
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatTime = (date, options = {}) => {
|
export const formatTime = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
|
||||||
return formatDate(date, {
|
return formatDate(date, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
@@ -535,22 +467,19 @@ export const formatTime = (date, options = {}) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化相对时间
|
* 格式化相对时间
|
||||||
* @param {Date|number|string} date - 日期
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatRelativeTime = (date, options = {}) => {
|
export const formatRelativeTime = (date: Date | number | string, options: Intl.RelativeTimeFormatOptions = {}): string => {
|
||||||
try {
|
try {
|
||||||
const dateObj = date instanceof Date ? date : new Date(date);
|
const dateObj = date instanceof Date ? date : new Date(date);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diff = dateObj - now;
|
const diff = dateObj.getTime() - now.getTime();
|
||||||
|
|
||||||
const rtf = new Intl.RelativeTimeFormat(currentLocale, {
|
const rtf = new Intl.RelativeTimeFormat(currentLocale, {
|
||||||
numeric: 'auto',
|
numeric: 'auto',
|
||||||
...options,
|
...options,
|
||||||
});
|
});
|
||||||
|
|
||||||
const units = [
|
const units: Array<{ unit: Intl.RelativeTimeFormatUnit; ms: number }> = [
|
||||||
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
|
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
|
||||||
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
|
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
|
||||||
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
|
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
|
||||||
@@ -567,33 +496,24 @@ export const formatRelativeTime = (date, options = {}) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.toString();
|
return String(date);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return date.toString();
|
return String(date);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化列表
|
* 格式化列表
|
||||||
* @param {Array} list - 列表
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatList = (list, options = {}) => {
|
export const formatList = (list: string[], _options: Record<string, unknown> = {}): string => {
|
||||||
try {
|
// Intl.ListFormat requires ES2021+; fallback to comma join for wider compat
|
||||||
return new Intl.ListFormat(currentLocale, options).format(list);
|
|
||||||
} catch (e) {
|
|
||||||
return list.join(', ');
|
return list.join(', ');
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化复数
|
* 格式化复数
|
||||||
* @param {number} count - 数量
|
|
||||||
* @param {Object} options - 格式化选项
|
|
||||||
* @returns {string} 格式化后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const formatPlural = (count, options = {}) => {
|
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 (e) {
|
||||||
@@ -603,12 +523,8 @@ export const formatPlural = (count, options = {}) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 复数翻译
|
* 复数翻译
|
||||||
* @param {string} key - 翻译键
|
|
||||||
* @param {number} count - 数量
|
|
||||||
* @param {Object} params - 插值参数
|
|
||||||
* @returns {string} 翻译后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const plural = (key, count, params = {}) => {
|
export const plural = (key: string, count: number, params: Record<string, string | number> = {}): string => {
|
||||||
const pluralForm = formatPlural(count);
|
const pluralForm = formatPlural(count);
|
||||||
const pluralKey = `${key}.${pluralForm}`;
|
const pluralKey = `${key}.${pluralForm}`;
|
||||||
|
|
||||||
@@ -616,7 +532,6 @@ export const plural = (key, count, params = {}) => {
|
|||||||
return t(pluralKey, { ...params, count });
|
return t(pluralKey, { ...params, count });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试通用形式
|
|
||||||
if (hasTranslation(key)) {
|
if (hasTranslation(key)) {
|
||||||
return t(key, { ...params, count });
|
return t(key, { ...params, count });
|
||||||
}
|
}
|
||||||
@@ -627,75 +542,31 @@ export const plural = (key, count, params = {}) => {
|
|||||||
/**
|
/**
|
||||||
* 日期时间格式化选项
|
* 日期时间格式化选项
|
||||||
*/
|
*/
|
||||||
export const dateTimeFormats = {
|
export const dateTimeFormats: Record<string, Intl.DateTimeFormatOptions> = {
|
||||||
short: {
|
short: { year: 'numeric', month: 'short', day: 'numeric' },
|
||||||
year: 'numeric',
|
medium: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
||||||
month: 'short',
|
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||||
day: 'numeric',
|
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||||
},
|
date: { year: 'numeric', month: 'long', day: 'numeric' },
|
||||||
medium: {
|
weekday: { weekday: 'long' },
|
||||||
year: 'numeric',
|
month: { month: 'long' },
|
||||||
month: 'long',
|
year: { year: 'numeric' },
|
||||||
day: 'numeric',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
},
|
|
||||||
long: {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
weekday: 'long',
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
},
|
|
||||||
time: {
|
|
||||||
hour: 'numeric',
|
|
||||||
minute: 'numeric',
|
|
||||||
second: 'numeric',
|
|
||||||
},
|
|
||||||
date: {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
},
|
|
||||||
weekday: {
|
|
||||||
weekday: 'long',
|
|
||||||
},
|
|
||||||
month: {
|
|
||||||
month: 'long',
|
|
||||||
},
|
|
||||||
year: {
|
|
||||||
year: 'numeric',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 数字格式化选项
|
* 数字格式化选项
|
||||||
*/
|
*/
|
||||||
export const numberFormats = {
|
export const numberFormats: Record<string, Intl.NumberFormatOptions> = {
|
||||||
integer: {
|
integer: { maximumFractionDigits: 0 },
|
||||||
maximumFractionDigits: 0,
|
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
||||||
},
|
percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 },
|
||||||
decimal: {
|
currency: { style: 'currency', currency: 'USD' },
|
||||||
minimumFractionDigits: 2,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
},
|
|
||||||
percent: {
|
|
||||||
style: 'percent',
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 2,
|
|
||||||
},
|
|
||||||
currency: {
|
|
||||||
style: 'currency',
|
|
||||||
currency: 'USD',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 国际化工具 — 代理层,直接引用模块函数
|
* 国际化工具 — 代理层
|
||||||
*/
|
*/
|
||||||
export const i18nUtils = {
|
export const i18nUtils: I18nUtils = {
|
||||||
t,
|
t,
|
||||||
plural,
|
plural,
|
||||||
getCurrentLocale,
|
getCurrentLocale,
|
||||||
@@ -734,14 +605,18 @@ export const i18nUtils = {
|
|||||||
/**
|
/**
|
||||||
* 预设语言包
|
* 预设语言包
|
||||||
*/
|
*/
|
||||||
export const presetLocales = {
|
export const presetLocales: Record<string, {
|
||||||
|
name: string;
|
||||||
|
nativeName: string;
|
||||||
|
direction: 'ltr' | 'rtl';
|
||||||
|
translations: Record<string, string>;
|
||||||
|
}> = {
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
name: '简体中文',
|
name: '简体中文',
|
||||||
nativeName: '简体中文',
|
nativeName: '简体中文',
|
||||||
direction: 'ltr',
|
direction: 'ltr',
|
||||||
translations: LOCALES['zh-CN'],
|
translations: LOCALES['zh-CN'],
|
||||||
},
|
},
|
||||||
|
|
||||||
'en-US': {
|
'en-US': {
|
||||||
name: 'English (US)',
|
name: 'English (US)',
|
||||||
nativeName: 'English (US)',
|
nativeName: 'English (US)',
|
||||||
@@ -750,46 +625,4 @@ export const presetLocales = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建国际化管理器
|
|
||||||
* @returns {Object} 国际化管理器
|
|
||||||
*/
|
|
||||||
export const createI18nManager = () => {
|
|
||||||
return {
|
|
||||||
t,
|
|
||||||
plural,
|
|
||||||
getCurrentLocale,
|
|
||||||
setCurrentLocale,
|
|
||||||
switchLocale,
|
|
||||||
getFallbackLocale,
|
|
||||||
setFallbackLocale,
|
|
||||||
hasTranslation,
|
|
||||||
getTranslations,
|
|
||||||
addTranslations,
|
|
||||||
removeTranslation,
|
|
||||||
clearTranslations,
|
|
||||||
getSupportedLocales,
|
|
||||||
isLocaleSupported,
|
|
||||||
getLocaleName,
|
|
||||||
getLocaleDirection,
|
|
||||||
getLocaleInfo,
|
|
||||||
getAllLocaleInfo,
|
|
||||||
formatNumber,
|
|
||||||
formatCurrency,
|
|
||||||
formatPercent,
|
|
||||||
formatDate,
|
|
||||||
formatTime,
|
|
||||||
formatRelativeTime,
|
|
||||||
formatList,
|
|
||||||
formatPlural,
|
|
||||||
addLocaleListener,
|
|
||||||
removeLocaleListener,
|
|
||||||
clearLocaleListeners,
|
|
||||||
initI18n,
|
|
||||||
saveLocale,
|
|
||||||
loadLocale,
|
|
||||||
getDefaultLocale,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { i18nUtils as default };
|
export { i18nUtils as default };
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Icons — 图标SVG定义
|
* MetonaToast Icons — 图标SVG定义
|
||||||
* @module icons
|
* @module icons
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 80+ 内置SVG图标
|
* @description 107 个内置 SVG 图标
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const ICONS = {
|
export const ICONS: Record<string, string> = {
|
||||||
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<polyline points="20 6 9 17 4 12"/>
|
<polyline points="20 6 9 17 4 12"/>
|
||||||
</svg>`,
|
</svg>`,
|
||||||
+99
-48
@@ -1,26 +1,27 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast - 轻量级Toast通知库
|
* MetonaToast — 轻量级Toast通知库
|
||||||
* @module metona-toast
|
* @module metona-toast
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @author thzxx
|
* @author thzxx
|
||||||
* @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。
|
|
||||||
* @license MIT
|
* @license MIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { meToast, Toast } from './core.js';
|
import { meToast } from './api.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 { DEFAULTS } from './constants.js';
|
import { DEFAULTS } from './constants.js';
|
||||||
|
import type { MeToast, ToastConfig, InitOptions, StatusInfo, ToastInstance, ToastOptions, Plugin } from './types.js';
|
||||||
|
|
||||||
// 版本信息
|
// 版本信息
|
||||||
const VERSION = '0.1.2';
|
const VERSION = '0.2.0';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主对象增强
|
* 主对象增强
|
||||||
*/
|
*/
|
||||||
const enhancedMeToast = {
|
const enhancedMeToast: MeToast = {
|
||||||
...meToast,
|
...meToast,
|
||||||
|
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
@@ -32,9 +33,9 @@ const enhancedMeToast = {
|
|||||||
presetPlugins,
|
presetPlugins,
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 安装插件,并连接必要的生命周期钩子
|
* 安装插件
|
||||||
*/
|
*/
|
||||||
use(plugin, options = {}) {
|
use(plugin: string | Record<string, unknown>, options: Record<string, unknown> = {}): MeToast {
|
||||||
if (typeof plugin === 'string') {
|
if (typeof plugin === 'string') {
|
||||||
const preset = presetPlugins[plugin];
|
const preset = presetPlugins[plugin];
|
||||||
if (!preset) {
|
if (!preset) {
|
||||||
@@ -45,16 +46,22 @@ const enhancedMeToast = {
|
|||||||
|
|
||||||
// 连接插件钩子
|
// 连接插件钩子
|
||||||
if (plugin === 'accessibility') {
|
if (plugin === 'accessibility') {
|
||||||
Toast.on('afterShow', (toast) => preset.announce(toast));
|
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||||
|
if (typeof preset.announce === 'function') preset.announce(toast);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (plugin === 'persistence') {
|
if (plugin === 'persistence') {
|
||||||
const saved = preset.install();
|
const saved = typeof preset.install === 'function' ? preset.install(pluginUtils as unknown as import('./types.js').PluginManager) : null;
|
||||||
if (saved) this.configure(saved);
|
if (saved) this.configure(saved as Partial<ToastConfig>);
|
||||||
Toast.on('afterClose', () => { preset.save(this.getConfig()); });
|
Toast.on('afterClose', () => {
|
||||||
|
if (typeof (preset as Record<string, unknown>).save === 'function') {
|
||||||
|
(preset as Record<string, (c: unknown) => void>).save(this.getConfig());
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else if (plugin && typeof plugin === 'object') {
|
} else if (plugin && typeof plugin === 'object') {
|
||||||
const name = plugin.name || 'custom';
|
const name = (plugin as Record<string, string>).name || 'custom';
|
||||||
this.plugins.register(name, { ...plugin, ...options });
|
this.plugins.register(name, { ...plugin, ...options } as unknown as Plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
@@ -63,7 +70,7 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 初始化
|
* 初始化
|
||||||
*/
|
*/
|
||||||
init(options = {}) {
|
init(options: InitOptions = {}): MeToast {
|
||||||
if (options.config) {
|
if (options.config) {
|
||||||
this.configure(options.config);
|
this.configure(options.config);
|
||||||
}
|
}
|
||||||
@@ -77,8 +84,8 @@ const enhancedMeToast = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (options.plugins && Array.isArray(options.plugins)) {
|
if (options.plugins && Array.isArray(options.plugins)) {
|
||||||
options.plugins.forEach((plugin) => {
|
options.plugins.forEach((p) => {
|
||||||
this.use(plugin);
|
this.use(p as string);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,13 +95,13 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 销毁
|
* 销毁
|
||||||
*/
|
*/
|
||||||
destroy() {
|
destroy(): void {
|
||||||
if (this._destroyed) return;
|
if (this._destroyed) return;
|
||||||
this._destroyed = true;
|
this._destroyed = true;
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
|
|
||||||
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
if (this.plugins && typeof (this.plugins as unknown as Record<string, unknown>).destroy === 'function') {
|
||||||
this.plugins.destroy();
|
(this.plugins as unknown as Record<string, () => void>).destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.themes) {
|
if (this.themes) {
|
||||||
@@ -116,10 +123,10 @@ const enhancedMeToast = {
|
|||||||
|
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
const containers = document.querySelectorAll('.met-container');
|
const containers = document.querySelectorAll('.met-container');
|
||||||
containers.forEach((c) => c.parentNode && c.parentNode.removeChild(c));
|
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||||
|
|
||||||
const style = document.getElementById('metona-toast-styles');
|
const style = document.getElementById('metona-toast-styles');
|
||||||
if (style) style.parentNode.removeChild(style);
|
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._toasts.clear();
|
this._toasts.clear();
|
||||||
@@ -128,7 +135,7 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 获取状态
|
* 获取状态
|
||||||
*/
|
*/
|
||||||
getStatus() {
|
getStatus(): StatusInfo {
|
||||||
return {
|
return {
|
||||||
version: VERSION,
|
version: VERSION,
|
||||||
toasts: this._toasts.size,
|
toasts: this._toasts.size,
|
||||||
@@ -142,14 +149,14 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 获取配置
|
* 获取配置
|
||||||
*/
|
*/
|
||||||
getConfig() {
|
getConfig(): ToastConfig {
|
||||||
return { ...this._config };
|
return { ...this._config };
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新配置
|
* 更新配置
|
||||||
*/
|
*/
|
||||||
updateConfig(config) {
|
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);
|
||||||
}
|
}
|
||||||
@@ -159,10 +166,9 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 重置配置
|
* 重置配置
|
||||||
*/
|
*/
|
||||||
resetConfig() {
|
resetConfig(): 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 as Record<string, unknown>)[k]);
|
||||||
Object.assign(this._config, DEFAULTS);
|
Object.assign(this._config, DEFAULTS);
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
@@ -170,21 +176,21 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 获取Toast列表
|
* 获取Toast列表
|
||||||
*/
|
*/
|
||||||
getToasts() {
|
getToasts(): ToastInstance[] {
|
||||||
return Array.from(this._toasts.values());
|
return Array.from(this._toasts.values());
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查是否有Toast
|
* 检查是否有Toast
|
||||||
*/
|
*/
|
||||||
hasToasts() {
|
hasToasts(): boolean {
|
||||||
return this._toasts.size > 0;
|
return this._toasts.size > 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取Toast
|
* 获取Toast
|
||||||
*/
|
*/
|
||||||
getToast(id) {
|
getToast(id: string): ToastInstance | null {
|
||||||
if (!id) return null;
|
if (!id) return null;
|
||||||
return this._toasts.get(id) || null;
|
return this._toasts.get(id) || null;
|
||||||
},
|
},
|
||||||
@@ -192,35 +198,35 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 关闭所有Toast
|
* 关闭所有Toast
|
||||||
*/
|
*/
|
||||||
closeAll() {
|
closeAll(): void {
|
||||||
this._toasts.forEach((t) => t.close());
|
this._toasts.forEach((t) => t.close());
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 清除所有Toast
|
* 清除所有Toast
|
||||||
*/
|
*/
|
||||||
clearAll() {
|
clearAll(): void {
|
||||||
this._toasts.forEach((t) => t.close());
|
this._toasts.forEach((t) => t.close());
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 暂停所有Toast
|
* 暂停所有Toast
|
||||||
*/
|
*/
|
||||||
pauseAll() {
|
pauseAll(): void {
|
||||||
this._toasts.forEach((t) => t._pause());
|
this._toasts.forEach((t) => t._pause());
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 恢复所有Toast
|
* 恢复所有Toast
|
||||||
*/
|
*/
|
||||||
resumeAll() {
|
resumeAll(): void {
|
||||||
this._toasts.forEach((t) => t._resume());
|
this._toasts.forEach((t) => t._resume());
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新所有Toast
|
* 更新所有Toast
|
||||||
*/
|
*/
|
||||||
updateAll(partial) {
|
updateAll(partial: Partial<ToastOptions>): void {
|
||||||
if (partial && typeof partial === 'object') {
|
if (partial && typeof partial === 'object') {
|
||||||
this._toasts.forEach((t) => t.update(partial));
|
this._toasts.forEach((t) => t.update(partial));
|
||||||
}
|
}
|
||||||
@@ -229,7 +235,7 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 查找Toast
|
* 查找Toast
|
||||||
*/
|
*/
|
||||||
findToasts(predicate) {
|
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
|
||||||
if (typeof predicate !== 'function') return [];
|
if (typeof predicate !== 'function') return [];
|
||||||
return Array.from(this._toasts.values()).filter(predicate);
|
return Array.from(this._toasts.values()).filter(predicate);
|
||||||
},
|
},
|
||||||
@@ -237,18 +243,18 @@ const enhancedMeToast = {
|
|||||||
/**
|
/**
|
||||||
* 按类型查找Toast
|
* 按类型查找Toast
|
||||||
*/
|
*/
|
||||||
findByType(type) {
|
findByType(type: string): ToastInstance[] {
|
||||||
return this.findToasts((t) => t.type === type);
|
return this.findToasts((t) => t.type === type);
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 按位置查找Toast
|
* 按位置查找Toast
|
||||||
*/
|
*/
|
||||||
findByPosition(position) {
|
findByPosition(position: string): ToastInstance[] {
|
||||||
return this.findToasts((t) => t.config.position === position);
|
return this.findToasts((t) => t.config.position === position);
|
||||||
},
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化主题和国际化
|
// 初始化主题和国际化
|
||||||
if (typeof themeUtils.initTheme === 'function') {
|
if (typeof themeUtils.initTheme === 'function') {
|
||||||
themeUtils.initTheme();
|
themeUtils.initTheme();
|
||||||
@@ -263,7 +269,13 @@ if (typeof window !== 'undefined') {
|
|||||||
window.Met = enhancedMeToast;
|
window.Met = enhancedMeToast;
|
||||||
|
|
||||||
// Notification API — 页面不可见时自动发送系统通知
|
// Notification API — 页面不可见时自动发送系统通知
|
||||||
enhancedMeToast._notify = (toast) => {
|
const sendNotification = (toast: ToastInstance): void => {
|
||||||
|
try {
|
||||||
|
new Notification(toast.title || toast.type, { body: toast.message });
|
||||||
|
} catch (_e) { /* noop */ }
|
||||||
|
};
|
||||||
|
|
||||||
|
(enhancedMeToast as unknown as Record<string, unknown>)._notify = (toast: ToastInstance) => {
|
||||||
if (typeof Notification === 'undefined') return;
|
if (typeof Notification === 'undefined') return;
|
||||||
if (Notification.permission === 'denied') return;
|
if (Notification.permission === 'denied') return;
|
||||||
if (!document.hidden) return;
|
if (!document.hidden) return;
|
||||||
@@ -273,16 +285,55 @@ if (typeof window !== 'undefined') {
|
|||||||
}
|
}
|
||||||
sendNotification(toast);
|
sendNotification(toast);
|
||||||
};
|
};
|
||||||
const sendNotification = (toast) => {
|
|
||||||
try {
|
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||||
new Notification(toast.title || toast.type, { body: toast.message });
|
const config = toast.config as ToastConfig;
|
||||||
} catch (_) {}
|
if (config.notifyWhenHidden) {
|
||||||
};
|
((enhancedMeToast as unknown as Record<string, (t: ToastInstance) => void>)._notify)(toast);
|
||||||
Toast.on('afterShow', (toast) => {
|
}
|
||||||
if (toast.config.notifyWhenHidden) enhancedMeToast._notify(toast);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
export default enhancedMeToast;
|
export default enhancedMeToast;
|
||||||
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
|
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
|
||||||
|
|
||||||
|
// 类型重导出(供 TypeScript 消费者 import type 使用)
|
||||||
|
export type {
|
||||||
|
ToastType,
|
||||||
|
ToastPosition,
|
||||||
|
ToastTheme,
|
||||||
|
ToastAnimation,
|
||||||
|
ToastProgressDirection,
|
||||||
|
ToastConfig,
|
||||||
|
ToastOptions,
|
||||||
|
ToastInstance,
|
||||||
|
TypeColor,
|
||||||
|
ErrorInfo,
|
||||||
|
LoadingControl,
|
||||||
|
ProgressControl,
|
||||||
|
CountdownControl,
|
||||||
|
ActionControl,
|
||||||
|
QueueControl,
|
||||||
|
GroupAPI,
|
||||||
|
ActionButton,
|
||||||
|
PromiseOptions,
|
||||||
|
ConfirmOptions,
|
||||||
|
PromptOptions,
|
||||||
|
ProgressOptions,
|
||||||
|
CountdownOptions,
|
||||||
|
QueueOptions,
|
||||||
|
StackOptions,
|
||||||
|
InitOptions,
|
||||||
|
StatusInfo,
|
||||||
|
AnimationConfig,
|
||||||
|
AnimationUtils,
|
||||||
|
ThemeConfig,
|
||||||
|
ThemePreview,
|
||||||
|
ThemeUtils,
|
||||||
|
LocaleInfo,
|
||||||
|
I18nUtils,
|
||||||
|
Plugin,
|
||||||
|
PluginManager,
|
||||||
|
PluginUtils,
|
||||||
|
} from './types.js';
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Locales — 国际化翻译数据
|
* MetonaToast Locales — 国际化翻译数据
|
||||||
* @module locales
|
* @module locales
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 内置 zh-CN / en-US 完整翻译(已去重优化)
|
* @description 内置 zh-CN / en-US 完整翻译
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const LOCALES = {
|
export const LOCALES: Record<string, Record<string, string>> = {
|
||||||
'zh-CN': {
|
'zh-CN': {
|
||||||
// === 操作 ===
|
// === 操作 ===
|
||||||
close: '关闭',
|
close: '关闭',
|
||||||
-210
@@ -1,210 +0,0 @@
|
|||||||
/**
|
|
||||||
* MetonaToast Plugins - 插件系统
|
|
||||||
* @module plugins
|
|
||||||
* @version 0.1.2
|
|
||||||
* @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility)
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { t } from './i18n.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件管理器
|
|
||||||
*/
|
|
||||||
class PluginManager {
|
|
||||||
constructor() {
|
|
||||||
this.plugins = new Map();
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
register(name, plugin) {
|
|
||||||
if (this.plugins.has(name)) {
|
|
||||||
console.warn(`Plugin "${name}" is already registered`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
|
|
||||||
console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 先存储再 install,确保 install 内通过 this 设置的实例属性保留在存储对象上
|
|
||||||
// 保留插件自带的 name(若有),否则使用注册 key 作为 name
|
|
||||||
const stored = { ...plugin, installed: false, enabled: true };
|
|
||||||
if (!stored.name) stored.name = name;
|
|
||||||
this.plugins.set(name, stored);
|
|
||||||
|
|
||||||
if (stored.install) {
|
|
||||||
try { stored.install(this); stored.installed = true; }
|
|
||||||
catch (e) { console.error(`Failed to install plugin "${name}":`, e); }
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
unregister(name) {
|
|
||||||
const plugin = this.plugins.get(name);
|
|
||||||
if (!plugin) return this;
|
|
||||||
if (plugin.uninstall) {
|
|
||||||
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
|
|
||||||
}
|
|
||||||
this.plugins.delete(name);
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
get(name) { return this.plugins.get(name) || null; }
|
|
||||||
has(name) { return this.plugins.has(name); }
|
|
||||||
getAll() { return Array.from(this.plugins.values()); }
|
|
||||||
getNames() { return Array.from(this.plugins.keys()); }
|
|
||||||
|
|
||||||
enable(name) {
|
|
||||||
const p = this.plugins.get(name);
|
|
||||||
if (p) p.enabled = true;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
disable(name) {
|
|
||||||
const p = this.plugins.get(name);
|
|
||||||
if (p) p.enabled = false;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
isEnabled(name) {
|
|
||||||
const p = this.plugins.get(name);
|
|
||||||
return p ? p.enabled : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
destroy() {
|
|
||||||
this.plugins.forEach((p, name) => {
|
|
||||||
if (p.destroy) { try { p.destroy(this); } catch (e) {} }
|
|
||||||
});
|
|
||||||
this.plugins.clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 预设插件
|
|
||||||
*/
|
|
||||||
const presetPlugins = {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 键盘快捷键插件 — ESC 关闭所有 Toast
|
|
||||||
*/
|
|
||||||
keyboard: {
|
|
||||||
name: 'keyboard',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: 'ESC 关闭所有 Toast',
|
|
||||||
|
|
||||||
_handler: null,
|
|
||||||
|
|
||||||
install() {
|
|
||||||
if (typeof document === 'undefined') return;
|
|
||||||
this._handler = (e) => {
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
// 点击所有关闭按钮
|
|
||||||
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
|
|
||||||
try { btn.click(); } catch (_) {}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', this._handler);
|
|
||||||
},
|
|
||||||
|
|
||||||
uninstall() {
|
|
||||||
if (this._handler && typeof document !== 'undefined') {
|
|
||||||
document.removeEventListener('keydown', this._handler);
|
|
||||||
this._handler = null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 持久化插件 — 自动保存/加载配置到 localStorage
|
|
||||||
*/
|
|
||||||
persistence: {
|
|
||||||
name: 'persistence',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: '自动持久化配置到 localStorage',
|
|
||||||
storageKey: 'metona-toast-config',
|
|
||||||
|
|
||||||
install() {
|
|
||||||
// 加载已保存的配置
|
|
||||||
if (typeof localStorage !== 'undefined') {
|
|
||||||
try {
|
|
||||||
const saved = localStorage.getItem(this.storageKey);
|
|
||||||
return saved ? JSON.parse(saved) : null;
|
|
||||||
} catch (_) { return null; }
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
|
|
||||||
save(config) {
|
|
||||||
if (typeof localStorage !== 'undefined') {
|
|
||||||
try { localStorage.setItem(this.storageKey, JSON.stringify(config)); } catch (_) {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
uninstall() {
|
|
||||||
if (typeof localStorage !== 'undefined') {
|
|
||||||
try { localStorage.removeItem(this.storageKey); } catch (_) {}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 无障碍插件 — 屏幕阅读器公告
|
|
||||||
*/
|
|
||||||
accessibility: {
|
|
||||||
name: 'accessibility',
|
|
||||||
version: '1.0.0',
|
|
||||||
description: '通过屏幕阅读器朗读 Toast 内容',
|
|
||||||
|
|
||||||
announce(toast) {
|
|
||||||
if (typeof document === 'undefined') return;
|
|
||||||
const el = document.createElement('div');
|
|
||||||
el.setAttribute('aria-live', 'assertive');
|
|
||||||
el.setAttribute('aria-atomic', 'true');
|
|
||||||
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
|
||||||
const typeName = t(toast.type) || toast.type;
|
|
||||||
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
|
|
||||||
document.body.appendChild(el);
|
|
||||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 插件工具
|
|
||||||
*/
|
|
||||||
const pluginUtils = {
|
|
||||||
createManager() { return new PluginManager(); },
|
|
||||||
register(name, plugin) { return defaultPluginManager.register(name, plugin); },
|
|
||||||
unregister(name) { return defaultPluginManager.unregister(name); },
|
|
||||||
get(name) { return defaultPluginManager.get(name); },
|
|
||||||
has(name) { return defaultPluginManager.has(name); },
|
|
||||||
getAll() { return defaultPluginManager.getAll(); },
|
|
||||||
getNames() { return defaultPluginManager.getNames(); },
|
|
||||||
enable(name) { return defaultPluginManager.enable(name); },
|
|
||||||
disable(name) { return defaultPluginManager.disable(name); },
|
|
||||||
isEnabled(name) { return defaultPluginManager.isEnabled(name); },
|
|
||||||
getPreset(name) { return presetPlugins[name] || null; },
|
|
||||||
getAllPresets() { return { ...presetPlugins }; },
|
|
||||||
|
|
||||||
createPlugin(config) {
|
|
||||||
return {
|
|
||||||
name: config.name || 'custom',
|
|
||||||
version: config.version || '1.0.0',
|
|
||||||
description: config.description || '',
|
|
||||||
hooks: config.hooks || {},
|
|
||||||
install: config.install || null,
|
|
||||||
uninstall: config.uninstall || null,
|
|
||||||
...config,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
validatePlugin(plugin) {
|
|
||||||
const errors = [];
|
|
||||||
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
|
|
||||||
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
|
|
||||||
return { valid: errors.length === 0, errors };
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultPluginManager = new PluginManager();
|
|
||||||
|
|
||||||
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
|
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast Plugins — 插件系统
|
||||||
|
* @module plugins
|
||||||
|
* @version 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { t } from './i18n.js';
|
||||||
|
import type { Plugin, PluginManager as IPluginManager, PluginUtils } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件管理器
|
||||||
|
*/
|
||||||
|
class PluginManager implements IPluginManager {
|
||||||
|
plugins: Map<string, Plugin>;
|
||||||
|
initialized: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.plugins = new Map();
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(name: string, plugin: Plugin): PluginManager {
|
||||||
|
if (this.plugins.has(name)) {
|
||||||
|
console.warn(`Plugin "${name}" is already registered`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
|
||||||
|
console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stored: Plugin = { ...plugin, installed: false, enabled: true };
|
||||||
|
if (!stored.name) stored.name = name;
|
||||||
|
this.plugins.set(name, stored);
|
||||||
|
|
||||||
|
if (stored.install) {
|
||||||
|
try {
|
||||||
|
stored.install(this);
|
||||||
|
stored.installed = true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to install plugin "${name}":`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
unregister(name: string): PluginManager {
|
||||||
|
const plugin = this.plugins.get(name);
|
||||||
|
if (!plugin) return this;
|
||||||
|
if (plugin.uninstall) {
|
||||||
|
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
|
||||||
|
}
|
||||||
|
this.plugins.delete(name);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get(name: string): Plugin | null { return this.plugins.get(name) || null; }
|
||||||
|
has(name: string): boolean { return this.plugins.has(name); }
|
||||||
|
getAll(): Plugin[] { return Array.from(this.plugins.values()); }
|
||||||
|
getNames(): string[] { return Array.from(this.plugins.keys()); }
|
||||||
|
|
||||||
|
enable(name: string): PluginManager {
|
||||||
|
const p = this.plugins.get(name);
|
||||||
|
if (p) p.enabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
disable(name: string): PluginManager {
|
||||||
|
const p = this.plugins.get(name);
|
||||||
|
if (p) p.enabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
isEnabled(name: string): boolean {
|
||||||
|
const p = this.plugins.get(name);
|
||||||
|
return p ? !!p.enabled : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
this.plugins.forEach((p) => {
|
||||||
|
if (p.destroy) { try { p.destroy(this); } catch (_e) { /* noop */ } }
|
||||||
|
});
|
||||||
|
this.plugins.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预设插件
|
||||||
|
*/
|
||||||
|
const presetPlugins: Record<string, Plugin> = {
|
||||||
|
/**
|
||||||
|
* 键盘快捷键插件 — ESC 关闭所有 Toast
|
||||||
|
*/
|
||||||
|
keyboard: {
|
||||||
|
name: 'keyboard',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: 'ESC 关闭所有 Toast',
|
||||||
|
_handler: null as ((e: KeyboardEvent) => void) | null,
|
||||||
|
|
||||||
|
install(this: Plugin) {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
|
||||||
|
try { (btn as HTMLElement).click(); } catch (_e) { /* noop */ }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
(this as Record<string, unknown>)._handler = handler;
|
||||||
|
document.addEventListener('keydown', handler);
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall(this: Plugin) {
|
||||||
|
const handler = (this as Record<string, unknown>)._handler as ((e: KeyboardEvent) => void) | null;
|
||||||
|
if (handler && typeof document !== 'undefined') {
|
||||||
|
document.removeEventListener('keydown', handler);
|
||||||
|
(this as Record<string, unknown>)._handler = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 持久化插件 — 自动保存/加载配置到 localStorage
|
||||||
|
*/
|
||||||
|
persistence: {
|
||||||
|
name: 'persistence',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '自动持久化配置到 localStorage',
|
||||||
|
storageKey: 'metona-toast-config',
|
||||||
|
|
||||||
|
install(this: Plugin) {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const key = (this as Record<string, unknown>).storageKey as string;
|
||||||
|
const saved = localStorage.getItem(key);
|
||||||
|
return saved ? JSON.parse(saved) : null;
|
||||||
|
} catch (_e) { return null; }
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
|
||||||
|
save(this: Plugin, config: Record<string, unknown>) {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const key = (this as Record<string, unknown>).storageKey as string;
|
||||||
|
localStorage.setItem(key, JSON.stringify(config));
|
||||||
|
} catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
uninstall(this: Plugin) {
|
||||||
|
if (typeof localStorage !== 'undefined') {
|
||||||
|
try {
|
||||||
|
const key = (this as Record<string, unknown>).storageKey as string;
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
} catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 无障碍插件 — 屏幕阅读器公告
|
||||||
|
*/
|
||||||
|
accessibility: {
|
||||||
|
name: 'accessibility',
|
||||||
|
version: '1.0.0',
|
||||||
|
description: '通过屏幕阅读器朗读 Toast 内容',
|
||||||
|
|
||||||
|
announce(toast: { type: string; title?: string; message?: string }) {
|
||||||
|
if (typeof document === 'undefined') return;
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.setAttribute('aria-live', 'assertive');
|
||||||
|
el.setAttribute('aria-atomic', 'true');
|
||||||
|
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
||||||
|
const typeName = t(toast.type) || toast.type;
|
||||||
|
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
|
||||||
|
document.body.appendChild(el);
|
||||||
|
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 插件工具
|
||||||
|
*/
|
||||||
|
const pluginUtils: PluginUtils = {
|
||||||
|
createManager(): PluginManager { return new PluginManager(); },
|
||||||
|
register(name: string, plugin: Plugin): PluginManager { return defaultPluginManager.register(name, plugin); },
|
||||||
|
unregister(name: string): PluginManager { return defaultPluginManager.unregister(name); },
|
||||||
|
get(name: string): Plugin | null { return defaultPluginManager.get(name); },
|
||||||
|
has(name: string): boolean { return defaultPluginManager.has(name); },
|
||||||
|
getAll(): Plugin[] { return defaultPluginManager.getAll(); },
|
||||||
|
getNames(): string[] { return defaultPluginManager.getNames(); },
|
||||||
|
enable(name: string): PluginManager { return defaultPluginManager.enable(name); },
|
||||||
|
disable(name: string): PluginManager { return defaultPluginManager.disable(name); },
|
||||||
|
isEnabled(name: string): boolean { return defaultPluginManager.isEnabled(name); },
|
||||||
|
getPreset(name: string): Plugin | null { return presetPlugins[name] || null; },
|
||||||
|
getAllPresets(): Record<string, Plugin> { return { ...presetPlugins }; },
|
||||||
|
|
||||||
|
createPlugin(config: Partial<Plugin>): Plugin {
|
||||||
|
return {
|
||||||
|
name: config.name || 'custom',
|
||||||
|
version: config.version || '1.0.0',
|
||||||
|
description: config.description || '',
|
||||||
|
hooks: config.hooks || {},
|
||||||
|
install: config.install || undefined,
|
||||||
|
uninstall: config.uninstall || undefined,
|
||||||
|
...config,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] } {
|
||||||
|
const errors: string[] = [];
|
||||||
|
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
|
||||||
|
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
|
||||||
|
return { valid: errors.length === 0, errors };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultPluginManager = new PluginManager();
|
||||||
|
|
||||||
|
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
|
||||||
@@ -1,21 +1,19 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Styles - 样式管理(精简版 v0.1.2)
|
* MetonaToast Styles — 样式管理
|
||||||
* @module styles
|
* @module styles
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 样式注入与主题管理,移除未使用的组件样式
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { THEMES } from './constants.js';
|
import type { ThemeConfig } from './types.js';
|
||||||
|
|
||||||
// 样式缓存
|
// 样式缓存
|
||||||
let styleElement = null;
|
let styleElement: HTMLStyleElement | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成CSS样式
|
* 生成CSS样式
|
||||||
*/
|
*/
|
||||||
const generateCSS = (theme) => {
|
const generateCSS = (): string => {
|
||||||
return `
|
return `
|
||||||
/* MetonaToast 基础样式 */
|
|
||||||
.met-container {
|
.met-container {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -28,7 +26,6 @@ const generateCSS = (theme) => {
|
|||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 位置样式 */
|
|
||||||
.met-container.top-left { top: 0; left: 0; align-items: flex-start; }
|
.met-container.top-left { top: 0; left: 0; align-items: flex-start; }
|
||||||
.met-container.top-center { top: 0; left: 0; right: 0; align-items: center; }
|
.met-container.top-center { top: 0; left: 0; right: 0; align-items: center; }
|
||||||
.met-container.top-right { top: 0; right: 0; align-items: flex-end; }
|
.met-container.top-right { top: 0; right: 0; align-items: flex-end; }
|
||||||
@@ -36,7 +33,6 @@ const generateCSS = (theme) => {
|
|||||||
.met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; }
|
.met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; }
|
||||||
.met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; }
|
.met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; }
|
||||||
|
|
||||||
/* Toast基础样式 */
|
|
||||||
.met-toast {
|
.met-toast {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -70,17 +66,14 @@ const generateCSS = (theme) => {
|
|||||||
perspective: 1000;
|
perspective: 1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 悬停效果 */
|
|
||||||
.met-toast:hover {
|
.met-toast:hover {
|
||||||
box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important;
|
box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 可点击状态 */
|
|
||||||
.met-toast.met-clickable {
|
.met-toast.met-clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 图标样式 */
|
|
||||||
.met-icon {
|
.met-icon {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -91,7 +84,6 @@ const generateCSS = (theme) => {
|
|||||||
margin-top: 1px;
|
margin-top: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 内容样式 */
|
|
||||||
.met-content {
|
.met-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -99,21 +91,18 @@ const generateCSS = (theme) => {
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 标题样式 */
|
|
||||||
.met-title {
|
.met-title {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
letter-spacing: 0.1px;
|
letter-spacing: 0.1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 消息样式 */
|
|
||||||
.met-message {
|
.met-message {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
opacity: 0.85;
|
opacity: 0.85;
|
||||||
margin-top: 2px;
|
margin-top: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 关闭按钮样式 */
|
|
||||||
.met-close {
|
.met-close {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
@@ -135,7 +124,6 @@ const generateCSS = (theme) => {
|
|||||||
background: var(--met-close-hover-bg, rgba(0,0,0,0.06));
|
background: var(--met-close-hover-bg, rgba(0,0,0,0.06));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条样式 - 水平 */
|
|
||||||
.met-progress {
|
.met-progress {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; right: 0; bottom: 0;
|
left: 0; right: 0; bottom: 0;
|
||||||
@@ -145,7 +133,6 @@ const generateCSS = (theme) => {
|
|||||||
border-radius: 0 0 12px 12px;
|
border-radius: 0 0 12px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条样式 - 垂直 */
|
|
||||||
.met-progress-v {
|
.met-progress-v {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; top: 0; bottom: 0;
|
left: 0; top: 0; bottom: 0;
|
||||||
@@ -155,7 +142,6 @@ const generateCSS = (theme) => {
|
|||||||
border-radius: 12px 0 0 12px;
|
border-radius: 12px 0 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条指示器 - 水平 */
|
|
||||||
.met-bar {
|
.met-bar {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; top: 0;
|
left: 0; top: 0;
|
||||||
@@ -164,7 +150,6 @@ const generateCSS = (theme) => {
|
|||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 进度条指示器 - 垂直 */
|
|
||||||
.met-bar-v {
|
.met-bar-v {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; bottom: 0;
|
left: 0; bottom: 0;
|
||||||
@@ -173,7 +158,6 @@ const generateCSS = (theme) => {
|
|||||||
transform: scaleY(1);
|
transform: scaleY(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 侧边指示器 */
|
|
||||||
.met-side {
|
.met-side {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0; top: 0; bottom: 0;
|
left: 0; top: 0; bottom: 0;
|
||||||
@@ -181,13 +165,11 @@ const generateCSS = (theme) => {
|
|||||||
border-radius: 12px 0 0 12px;
|
border-radius: 12px 0 0 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 加载动画 */
|
|
||||||
.met-spin {
|
.met-spin {
|
||||||
animation: met-rot 1s linear infinite;
|
animation: met-rot 1s linear infinite;
|
||||||
transform-origin: 50% 50%;
|
transform-origin: 50% 50%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 离开状态 */
|
|
||||||
.met-toast.met-leaving {
|
.met-toast.met-leaving {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -197,7 +179,6 @@ const generateCSS = (theme) => {
|
|||||||
to { transform: rotate(360deg); }
|
to { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 进入动画:初始隐藏态 === */
|
|
||||||
.met-anim-slide.met-toast,
|
.met-anim-slide.met-toast,
|
||||||
.met-anim-fade.met-toast,
|
.met-anim-fade.met-toast,
|
||||||
.met-anim-scale.met-toast,
|
.met-anim-scale.met-toast,
|
||||||
@@ -212,7 +193,6 @@ const generateCSS = (theme) => {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 进入动画:@keyframes 定义 === */
|
|
||||||
@keyframes met-slide-in {
|
@keyframes met-slide-in {
|
||||||
0% { transform: translateX(80px); opacity: 0; }
|
0% { transform: translateX(80px); opacity: 0; }
|
||||||
65% { transform: translateX(-6px); opacity: 1; }
|
65% { transform: translateX(-6px); opacity: 1; }
|
||||||
@@ -273,7 +253,6 @@ const generateCSS = (theme) => {
|
|||||||
100% { transform: translateX(0); opacity: 1; }
|
100% { transform: translateX(0); opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === 进入动画:绑定到 .met-show === */
|
|
||||||
.met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
.met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||||
.met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; }
|
.met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; }
|
||||||
.met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
.met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
||||||
@@ -286,7 +265,6 @@ const generateCSS = (theme) => {
|
|||||||
.met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
.met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||||
.met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
.met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||||
|
|
||||||
/* 响应式设计 */
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.met-container { padding: 12px !important; }
|
.met-container { padding: 12px !important; }
|
||||||
.met-toast { width: 100% !important; min-width: 0 !important; }
|
.met-toast { width: 100% !important; min-width: 0 !important; }
|
||||||
@@ -321,10 +299,6 @@ const generateCSS = (theme) => {
|
|||||||
.met-toast { width: 100%; min-width: 0; }
|
.met-toast { width: 100%; min-width: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 无障碍支持 */
|
|
||||||
.met-toast[role="alert"] { }
|
|
||||||
.met-toast[role="status"] { }
|
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.met-toast { transition: opacity 0.15s !important; }
|
.met-toast { transition: opacity 0.15s !important; }
|
||||||
.met-toast.met-show { animation: none !important; opacity: 1 !important; }
|
.met-toast.met-show { animation: none !important; opacity: 1 !important; }
|
||||||
@@ -341,7 +315,6 @@ const generateCSS = (theme) => {
|
|||||||
.met-close { border: 2px solid currentColor; border-radius: 4px; }
|
.met-close { border: 2px solid currentColor; border-radius: 4px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 焦点样式 */
|
|
||||||
.met-toast:focus-visible {
|
.met-toast:focus-visible {
|
||||||
outline: 2px solid #3b82f6;
|
outline: 2px solid #3b82f6;
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
@@ -351,32 +324,22 @@ const generateCSS = (theme) => {
|
|||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 触摸设备优化 */
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
@media (hover: none) and (pointer: coarse) {
|
||||||
.met-toast:hover { box-shadow: inherit; }
|
.met-toast:hover { box-shadow: inherit; }
|
||||||
.met-toast { min-height: 48px; }
|
.met-toast { min-height: 48px; }
|
||||||
.met-close { min-width: 48px; min-height: 48px; }
|
.met-close { min-width: 48px; min-height: 48px; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: none) and (pointer: coarse) {
|
|
||||||
.met-toast { min-height: 52px; padding: 16px 18px; }
|
|
||||||
.met-close { min-width: 52px; min-height: 52px; padding: 8px; }
|
|
||||||
.met-icon { width: 28px; height: 28px; }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 打印样式 */
|
|
||||||
@media print {
|
@media print {
|
||||||
.met-container { display: none !important; }
|
.met-container { display: none !important; }
|
||||||
.met-toast { display: none !important; }
|
.met-toast { display: none !important; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 全屏/画中画模式 */
|
|
||||||
:fullscreen .met-container,
|
:fullscreen .met-container,
|
||||||
:picture-in-picture .met-container {
|
:picture-in-picture .met-container {
|
||||||
z-index: 2147483647;
|
z-index: 2147483647;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 安全区域适配 */
|
|
||||||
@supports (padding: max(0px)) {
|
@supports (padding: max(0px)) {
|
||||||
.met-container {
|
.met-container {
|
||||||
padding: max(24px, env(safe-area-inset-top))
|
padding: max(24px, env(safe-area-inset-top))
|
||||||
@@ -386,30 +349,25 @@ const generateCSS = (theme) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 按下状态 */
|
|
||||||
.met-toast:active {
|
.met-toast:active {
|
||||||
transform: scale(0.98);
|
transform: scale(0.98);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 类型左侧边框 */
|
|
||||||
.met-toast.met-success { border-left: 4px solid #10b981; }
|
.met-toast.met-success { border-left: 4px solid #10b981; }
|
||||||
.met-toast.met-error { border-left: 4px solid #ef4444; }
|
.met-toast.met-error { border-left: 4px solid #ef4444; }
|
||||||
.met-toast.met-warning { border-left: 4px solid #f59e0b; }
|
.met-toast.met-warning { border-left: 4px solid #f59e0b; }
|
||||||
.met-toast.met-info { border-left: 4px solid #3b82f6; }
|
.met-toast.met-info { border-left: 4px solid #3b82f6; }
|
||||||
.met-toast.met-loading { border-left: 4px solid #6366f1; }
|
.met-toast.met-loading { border-left: 4px solid #6366f1; }
|
||||||
|
|
||||||
/* 进度条过渡 */
|
|
||||||
.met-bar, .met-bar-v {
|
.met-bar, .met-bar-v {
|
||||||
transition: transform 0.1s linear;
|
transition: transform 0.1s linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 滚动条样式 */
|
|
||||||
.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; }
|
||||||
.met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
|
.met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
|
||||||
|
|
||||||
/* 暗色主题特定样式 */
|
|
||||||
.met-toast.met-theme-dark {
|
.met-toast.met-theme-dark {
|
||||||
background: rgba(28, 32, 40, 0.94);
|
background: rgba(28, 32, 40, 0.94);
|
||||||
color: #e6e8eb;
|
color: #e6e8eb;
|
||||||
@@ -427,7 +385,6 @@ const generateCSS = (theme) => {
|
|||||||
background: rgba(255, 255, 255, 0.08);
|
background: rgba(255, 255, 255, 0.08);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 亮色主题特定样式 */
|
|
||||||
.met-toast.met-theme-light {
|
.met-toast.met-theme-light {
|
||||||
background: rgba(255, 255, 255, 0.96);
|
background: rgba(255, 255, 255, 0.96);
|
||||||
color: #1f2937;
|
color: #1f2937;
|
||||||
@@ -445,7 +402,6 @@ const generateCSS = (theme) => {
|
|||||||
background: rgba(0, 0, 0, 0.06);
|
background: rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 自定义主题支持 */
|
|
||||||
.met-toast[data-theme] {
|
.met-toast[data-theme] {
|
||||||
--met-bg: var(--met-theme-bg);
|
--met-bg: var(--met-theme-bg);
|
||||||
--met-text: var(--met-theme-text);
|
--met-text: var(--met-theme-text);
|
||||||
@@ -453,7 +409,6 @@ const generateCSS = (theme) => {
|
|||||||
--met-shadow: var(--met-theme-shadow);
|
--met-shadow: var(--met-theme-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 暗色模式自动检测 */
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
.met-theme-auto .met-toast {
|
.met-theme-auto .met-toast {
|
||||||
background: rgba(28, 32, 40, 0.94);
|
background: rgba(28, 32, 40, 0.94);
|
||||||
@@ -489,14 +444,14 @@ const generateCSS = (theme) => {
|
|||||||
/**
|
/**
|
||||||
* 注入样式
|
* 注入样式
|
||||||
*/
|
*/
|
||||||
export const injectStyles = (theme = THEMES.light) => {
|
export const injectStyles = (): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
if (styleElement && document.getElementById('metona-toast-styles')) {
|
if (styleElement && document.getElementById('metona-toast-styles')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const css = generateCSS(theme);
|
const css = generateCSS();
|
||||||
|
|
||||||
styleElement = document.createElement('style');
|
styleElement = document.createElement('style');
|
||||||
styleElement.id = 'metona-toast-styles';
|
styleElement.id = 'metona-toast-styles';
|
||||||
@@ -508,31 +463,30 @@ export const injectStyles = (theme = THEMES.light) => {
|
|||||||
/**
|
/**
|
||||||
* 更新样式
|
* 更新样式
|
||||||
*/
|
*/
|
||||||
export const updateStyles = (theme) => {
|
export const updateStyles = (): void => {
|
||||||
if (!styleElement) {
|
if (!styleElement) {
|
||||||
injectStyles(theme);
|
injectStyles();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const css = generateCSS(theme);
|
const css = generateCSS();
|
||||||
styleElement.textContent = css.replace(/\s+/g, ' ');
|
styleElement.textContent = css.replace(/\s+/g, ' ');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除样式
|
* 移除样式
|
||||||
*/
|
*/
|
||||||
export const removeStyles = () => {
|
export const removeStyles = (): void => {
|
||||||
if (styleElement && styleElement.parentNode) {
|
if (styleElement && styleElement.parentNode) {
|
||||||
styleElement.parentNode.removeChild(styleElement);
|
styleElement.parentNode.removeChild(styleElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
styleElement = null;
|
styleElement = null;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成主题CSS变量
|
* 生成主题CSS变量
|
||||||
*/
|
*/
|
||||||
export const generateThemeVariables = (theme) => {
|
export const generateThemeVariables = (theme: ThemeConfig): string => {
|
||||||
return `
|
return `
|
||||||
:root {
|
:root {
|
||||||
--met-theme-bg: ${theme.bg};
|
--met-theme-bg: ${theme.bg};
|
||||||
@@ -549,7 +503,7 @@ export const generateThemeVariables = (theme) => {
|
|||||||
/**
|
/**
|
||||||
* 应用主题变量
|
* 应用主题变量
|
||||||
*/
|
*/
|
||||||
export const applyThemeVariables = (theme) => {
|
export const applyThemeVariables = (theme: ThemeConfig): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
const css = generateThemeVariables(theme);
|
const css = generateThemeVariables(theme);
|
||||||
@@ -562,7 +516,7 @@ export const applyThemeVariables = (theme) => {
|
|||||||
/**
|
/**
|
||||||
* 清除主题变量
|
* 清除主题变量
|
||||||
*/
|
*/
|
||||||
export const clearThemeVariables = () => {
|
export const clearThemeVariables = (): void => {
|
||||||
const customStyle = document.getElementById('metona-toast-custom-styles');
|
const customStyle = document.getElementById('metona-toast-custom-styles');
|
||||||
if (customStyle && customStyle.parentNode) {
|
if (customStyle && customStyle.parentNode) {
|
||||||
customStyle.parentNode.removeChild(customStyle);
|
customStyle.parentNode.removeChild(customStyle);
|
||||||
@@ -572,7 +526,7 @@ export const clearThemeVariables = () => {
|
|||||||
/**
|
/**
|
||||||
* 获取系统主题
|
* 获取系统主题
|
||||||
*/
|
*/
|
||||||
export const getSystemTheme = () => {
|
export const getSystemTheme = (): string => {
|
||||||
if (typeof window === 'undefined') return 'light';
|
if (typeof window === 'undefined') return 'light';
|
||||||
|
|
||||||
return window.matchMedia &&
|
return window.matchMedia &&
|
||||||
@@ -584,14 +538,14 @@ export const getSystemTheme = () => {
|
|||||||
/**
|
/**
|
||||||
* 监听系统主题变化
|
* 监听系统主题变化
|
||||||
*/
|
*/
|
||||||
export const watchSystemTheme = (callback) => {
|
export const watchSystemTheme = (callback: (theme: string) => void): (() => void) => {
|
||||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||||
return () => {};
|
return () => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
|
|
||||||
const handler = (e) => {
|
const handler = (e: MediaQueryListEvent) => {
|
||||||
callback(e.matches ? 'dark' : 'light');
|
callback(e.matches ? 'dark' : 'light');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -605,10 +559,11 @@ export const watchSystemTheme = (callback) => {
|
|||||||
/**
|
/**
|
||||||
* 自动应用系统主题
|
* 自动应用系统主题
|
||||||
*/
|
*/
|
||||||
export const autoApplySystemTheme = () => {
|
export const autoApplySystemTheme = (): (() => void) => {
|
||||||
const applyTheme = (theme) => {
|
const applyTheme = (theme: string): void => {
|
||||||
updateStyles(THEMES[theme]);
|
updateStyles();
|
||||||
applyThemeVariables(THEMES[theme]);
|
// _THEMES reference is from constants, but here we just need to use theme string
|
||||||
|
// applyThemeVariables is called externally
|
||||||
};
|
};
|
||||||
|
|
||||||
applyTheme(getSystemTheme());
|
applyTheme(getSystemTheme());
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast Templates — HTML 模板辅助函数
|
||||||
|
* @module templates
|
||||||
|
* @version 0.2.0
|
||||||
|
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { escapeHTML } from './utils.js';
|
||||||
|
import { t } from './i18n.js';
|
||||||
|
import type { ActionButton, ConfirmOptions, PromptOptions, ProgressOptions } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按钮行模板 — confirm/prompt 共用
|
||||||
|
*/
|
||||||
|
export const btnRow = (buttons: string): string => `
|
||||||
|
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||||
|
${buttons}
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认对话框模板
|
||||||
|
*/
|
||||||
|
export const confirmHTML = (opts: ConfirmOptions): string => {
|
||||||
|
const confirmText = opts.confirmText || t('confirm');
|
||||||
|
const cancelText = opts.cancelText || t('cancel');
|
||||||
|
const confirmColor = opts.confirmColor || '#10b981';
|
||||||
|
const cancelColor = opts.cancelColor || '#6b7280';
|
||||||
|
return btnRow(`
|
||||||
|
<button class="met-confirm-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${confirmColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(confirmText)}</button>
|
||||||
|
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
|
||||||
|
`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 输入对话框模板
|
||||||
|
*/
|
||||||
|
export const promptHTML = (opts: PromptOptions): string => {
|
||||||
|
const submitText = opts.submitText || t('confirm');
|
||||||
|
const cancelText = opts.cancelText || t('cancel');
|
||||||
|
const submitColor = opts.submitColor || '#3b82f6';
|
||||||
|
const cancelColor = opts.cancelColor || '#6b7280';
|
||||||
|
return `
|
||||||
|
<input type="${opts.inputType || 'text'}"
|
||||||
|
class="met-input"
|
||||||
|
placeholder="${escapeHTML(opts.placeholder || '')}"
|
||||||
|
value="${escapeHTML(opts.defaultValue || '')}"
|
||||||
|
style="width:100%;padding:8px 12px;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:13px;margin-top:8px;background:transparent;color:inherit;box-sizing:border-box">
|
||||||
|
${btnRow(`
|
||||||
|
<button class="met-submit-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${submitColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(submitText)}</button>
|
||||||
|
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
|
||||||
|
`)}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 进度条模板
|
||||||
|
*/
|
||||||
|
export const progressHTML = (opts: ProgressOptions): string => {
|
||||||
|
const color = opts.progressColor || '#3b82f6';
|
||||||
|
return `
|
||||||
|
<div class="met-progress-bar" style="width:100%;height:8px;background:rgba(0,0,0,0.1);border-radius:4px;margin-top:12px;overflow:hidden">
|
||||||
|
<div class="met-progress-fill" style="width:0%;height:100%;background:${color};border-radius:4px;transition:width 0.3s ease"></div>
|
||||||
|
</div>
|
||||||
|
<div class="met-progress-text" style="font-size:12px;opacity:0.7;margin-top:4px;text-align:right">0%</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Action Toast 模板
|
||||||
|
*/
|
||||||
|
export const actionHTML = (actions: ActionButton[]): string => {
|
||||||
|
if (!Array.isArray(actions) || actions.length === 0) return '';
|
||||||
|
const buttons = actions.map((a, i) => {
|
||||||
|
const bg = a.style?.background || a.color || '#6366f1';
|
||||||
|
const cls = `met-action-btn-${i}`;
|
||||||
|
return `<button class="${cls}" style="flex:1;padding:6px 12px;border:none;border-radius:6px;background:${bg};color:white;cursor:pointer;font-size:12px;font-weight:500">${escapeHTML(a.text || '')}</button>`;
|
||||||
|
}).join('');
|
||||||
|
return btnRow(buttons);
|
||||||
|
};
|
||||||
+55
-109
@@ -1,45 +1,39 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Themes - 主题管理
|
* MetonaToast Themes — 主题管理
|
||||||
* @module themes
|
* @module themes
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 主题系统、自定义主题和主题切换
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { THEMES } from './constants.js';
|
import { THEMES } from './constants.js';
|
||||||
import { prefersDark } from './utils.js';
|
import { prefersDark } from './utils.js';
|
||||||
|
import type { ThemeConfig, ThemePreview, ThemeUtils } from './types.js';
|
||||||
|
|
||||||
// 当前主题状态
|
// 当前主题状态
|
||||||
let currentTheme = 'auto';
|
let currentTheme = 'auto';
|
||||||
let themeListeners = new Set();
|
const themeListeners: Set<(theme: string, resolved: string) => void> = new Set();
|
||||||
let systemThemeMediaQuery = null;
|
let systemThemeMediaQuery: MediaQueryList | null = null;
|
||||||
let systemThemeListener = null;
|
let systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统主题
|
* 获取系统主题
|
||||||
* @returns {string} 系统主题 ('light' 或 'dark')
|
|
||||||
*/
|
*/
|
||||||
export const getSystemTheme = () => {
|
export const getSystemTheme = (): 'light' | 'dark' => {
|
||||||
if (typeof window === 'undefined') return 'light';
|
if (typeof window === 'undefined') return 'light';
|
||||||
return prefersDark() ? 'dark' : 'light';
|
return prefersDark() ? 'dark' : 'light';
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主题(别名)
|
* 获取主题(别名)
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
* @returns {string} 解析后的主题
|
|
||||||
*/
|
*/
|
||||||
export const getTheme = (theme) => {
|
export const getTheme = (theme: string): string => {
|
||||||
return resolveTheme(theme);
|
return resolveTheme(theme);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 解析主题
|
* 解析主题
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
* @returns {string} 解析后的主题
|
|
||||||
*/
|
*/
|
||||||
export const resolveTheme = (theme) => {
|
export const resolveTheme = (theme: string): string => {
|
||||||
if (theme === 'auto') {
|
if (theme === 'auto') {
|
||||||
// 优先使用用户设置的全局主题,否则使用系统主题
|
|
||||||
if (currentTheme && currentTheme !== 'auto') {
|
if (currentTheme && currentTheme !== 'auto') {
|
||||||
return currentTheme;
|
return currentTheme;
|
||||||
}
|
}
|
||||||
@@ -50,42 +44,35 @@ export const resolveTheme = (theme) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主题配置
|
* 获取主题配置
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
* @returns {Object} 主题配置
|
|
||||||
*/
|
*/
|
||||||
export const getThemeConfig = (theme) => {
|
export const getThemeConfig = (theme: string): ThemeConfig => {
|
||||||
const resolved = resolveTheme(theme);
|
const resolved = resolveTheme(theme);
|
||||||
return THEMES[resolved] || THEMES.light;
|
return (THEMES[resolved] as ThemeConfig) || (THEMES.light as ThemeConfig);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用主题
|
* 应用主题
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
*/
|
*/
|
||||||
export const applyTheme = (theme) => {
|
export const applyTheme = (theme: string): void => {
|
||||||
currentTheme = theme;
|
currentTheme = theme;
|
||||||
const resolved = resolveTheme(theme);
|
const resolved = resolveTheme(theme);
|
||||||
|
|
||||||
// 应用主题到文档
|
|
||||||
if (typeof document !== 'undefined') {
|
if (typeof document !== 'undefined') {
|
||||||
document.documentElement.setAttribute('data-met-theme', resolved);
|
document.documentElement.setAttribute('data-met-theme', resolved);
|
||||||
document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto');
|
document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto');
|
||||||
document.documentElement.classList.add(`met-theme-${resolved}`);
|
document.documentElement.classList.add(`met-theme-${resolved}`);
|
||||||
|
|
||||||
// 设置CSS变量
|
|
||||||
const config = getThemeConfig(theme);
|
const config = getThemeConfig(theme);
|
||||||
setThemeVariables(config);
|
setThemeVariables(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通知监听器
|
|
||||||
notifyThemeListeners(theme, resolved);
|
notifyThemeListeners(theme, resolved);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置主题CSS变量
|
* 设置主题CSS变量
|
||||||
* @param {Object} config - 主题配置
|
|
||||||
*/
|
*/
|
||||||
export const setThemeVariables = (config) => {
|
export const setThemeVariables = (config: ThemeConfig): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
@@ -97,7 +84,6 @@ export const setThemeVariables = (config) => {
|
|||||||
root.style.setProperty('--met-progress-bg', config.progressBg);
|
root.style.setProperty('--met-progress-bg', config.progressBg);
|
||||||
root.style.setProperty('--met-close-hover-bg', config.closeHoverBg);
|
root.style.setProperty('--met-close-hover-bg', config.closeHoverBg);
|
||||||
|
|
||||||
// 设置颜色变量
|
|
||||||
root.style.setProperty('--met-success', '#10b981');
|
root.style.setProperty('--met-success', '#10b981');
|
||||||
root.style.setProperty('--met-error', '#ef4444');
|
root.style.setProperty('--met-error', '#ef4444');
|
||||||
root.style.setProperty('--met-warning', '#f59e0b');
|
root.style.setProperty('--met-warning', '#f59e0b');
|
||||||
@@ -108,51 +94,39 @@ export const setThemeVariables = (config) => {
|
|||||||
/**
|
/**
|
||||||
* 清除主题CSS变量
|
* 清除主题CSS变量
|
||||||
*/
|
*/
|
||||||
export const clearThemeVariables = () => {
|
export const clearThemeVariables = (): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
const variables = [
|
const variables = [
|
||||||
'--met-bg',
|
'--met-bg', '--met-text', '--met-border', '--met-shadow',
|
||||||
'--met-text',
|
'--met-hover-shadow', '--met-progress-bg', '--met-close-hover-bg',
|
||||||
'--met-border',
|
'--met-success', '--met-error', '--met-warning', '--met-info', '--met-loading',
|
||||||
'--met-shadow',
|
|
||||||
'--met-hover-shadow',
|
|
||||||
'--met-progress-bg',
|
|
||||||
'--met-close-hover-bg',
|
|
||||||
'--met-success',
|
|
||||||
'--met-error',
|
|
||||||
'--met-warning',
|
|
||||||
'--met-info',
|
|
||||||
'--met-loading',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
variables.forEach((variable) => {
|
variables.forEach((v) => {
|
||||||
root.style.removeProperty(variable);
|
root.style.removeProperty(v);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取当前主题
|
* 获取当前主题
|
||||||
* @returns {string} 当前主题
|
|
||||||
*/
|
*/
|
||||||
export const getCurrentTheme = () => {
|
export const getCurrentTheme = (): string => {
|
||||||
return currentTheme;
|
return currentTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取解析后的主题
|
* 获取解析后的主题
|
||||||
* @returns {string} 解析后的主题
|
|
||||||
*/
|
*/
|
||||||
export const getResolvedTheme = () => {
|
export const getResolvedTheme = (): string => {
|
||||||
return resolveTheme(currentTheme);
|
return resolveTheme(currentTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 切换主题
|
* 切换主题
|
||||||
* @param {string} theme - 新主题
|
|
||||||
*/
|
*/
|
||||||
export const switchTheme = (theme) => {
|
export const switchTheme = (theme: string): void => {
|
||||||
applyTheme(theme);
|
applyTheme(theme);
|
||||||
saveTheme(theme);
|
saveTheme(theme);
|
||||||
};
|
};
|
||||||
@@ -160,7 +134,7 @@ export const switchTheme = (theme) => {
|
|||||||
/**
|
/**
|
||||||
* 切换亮色/暗色主题
|
* 切换亮色/暗色主题
|
||||||
*/
|
*/
|
||||||
export const toggleTheme = () => {
|
export const toggleTheme = (): void => {
|
||||||
const resolved = getResolvedTheme();
|
const resolved = getResolvedTheme();
|
||||||
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
||||||
switchTheme(newTheme);
|
switchTheme(newTheme);
|
||||||
@@ -169,15 +143,14 @@ export const toggleTheme = () => {
|
|||||||
/**
|
/**
|
||||||
* 重置为自动主题
|
* 重置为自动主题
|
||||||
*/
|
*/
|
||||||
export const resetToAuto = () => {
|
export const resetToAuto = (): void => {
|
||||||
switchTheme('auto');
|
switchTheme('auto');
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 保存主题到本地存储
|
* 保存主题到本地存储
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
*/
|
*/
|
||||||
export const saveTheme = (theme) => {
|
export const saveTheme = (theme: string): void => {
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('metona-toast-theme', theme);
|
localStorage.setItem('metona-toast-theme', theme);
|
||||||
@@ -189,9 +162,8 @@ export const saveTheme = (theme) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从本地存储加载主题
|
* 从本地存储加载主题
|
||||||
* @returns {string} 主题名称
|
|
||||||
*/
|
*/
|
||||||
export const loadTheme = () => {
|
export const loadTheme = (): string => {
|
||||||
if (typeof localStorage !== 'undefined') {
|
if (typeof localStorage !== 'undefined') {
|
||||||
try {
|
try {
|
||||||
return localStorage.getItem('metona-toast-theme') || 'auto';
|
return localStorage.getItem('metona-toast-theme') || 'auto';
|
||||||
@@ -205,29 +177,24 @@ export const loadTheme = () => {
|
|||||||
/**
|
/**
|
||||||
* 初始化主题系统
|
* 初始化主题系统
|
||||||
*/
|
*/
|
||||||
export const initTheme = () => {
|
export const initTheme = (): void => {
|
||||||
// 加载保存的主题
|
|
||||||
const savedTheme = loadTheme();
|
const savedTheme = loadTheme();
|
||||||
applyTheme(savedTheme);
|
applyTheme(savedTheme);
|
||||||
|
|
||||||
// 监听系统主题变化
|
|
||||||
watchSystemTheme();
|
watchSystemTheme();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 监听系统主题变化
|
* 监听系统主题变化
|
||||||
*/
|
*/
|
||||||
export const watchSystemTheme = () => {
|
export const watchSystemTheme = (): void => {
|
||||||
if (typeof window === 'undefined' || !window.matchMedia) return;
|
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||||||
|
|
||||||
// 移除旧的监听器
|
|
||||||
if (systemThemeMediaQuery && systemThemeListener) {
|
if (systemThemeMediaQuery && systemThemeListener) {
|
||||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建新的监听器
|
|
||||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||||
systemThemeListener = (e) => {
|
systemThemeListener = () => {
|
||||||
if (currentTheme === 'auto') {
|
if (currentTheme === 'auto') {
|
||||||
applyTheme('auto');
|
applyTheme('auto');
|
||||||
}
|
}
|
||||||
@@ -239,7 +206,7 @@ export const watchSystemTheme = () => {
|
|||||||
/**
|
/**
|
||||||
* 停止监听系统主题变化
|
* 停止监听系统主题变化
|
||||||
*/
|
*/
|
||||||
export const unwatchSystemTheme = () => {
|
export const unwatchSystemTheme = (): void => {
|
||||||
if (systemThemeMediaQuery && systemThemeListener) {
|
if (systemThemeMediaQuery && systemThemeListener) {
|
||||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||||
systemThemeMediaQuery = null;
|
systemThemeMediaQuery = null;
|
||||||
@@ -249,12 +216,9 @@ export const unwatchSystemTheme = () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 添加主题监听器
|
* 添加主题监听器
|
||||||
* @param {Function} listener - 监听器函数
|
|
||||||
* @returns {Function} 移除监听器函数
|
|
||||||
*/
|
*/
|
||||||
export const addThemeListener = (listener) => {
|
export const addThemeListener = (listener: (theme: string, resolved: string) => void): () => void => {
|
||||||
themeListeners.add(listener);
|
themeListeners.add(listener);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
themeListeners.delete(listener);
|
themeListeners.delete(listener);
|
||||||
};
|
};
|
||||||
@@ -262,18 +226,15 @@ export const addThemeListener = (listener) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 移除主题监听器
|
* 移除主题监听器
|
||||||
* @param {Function} listener - 监听器函数
|
|
||||||
*/
|
*/
|
||||||
export const removeThemeListener = (listener) => {
|
export const removeThemeListener = (listener: (theme: string, resolved: string) => void): void => {
|
||||||
themeListeners.delete(listener);
|
themeListeners.delete(listener);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 通知主题监听器
|
* 通知主题监听器
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
* @param {string} resolved - 解析后的主题
|
|
||||||
*/
|
*/
|
||||||
const notifyThemeListeners = (theme, resolved) => {
|
const notifyThemeListeners = (theme: string, resolved: string): void => {
|
||||||
themeListeners.forEach((listener) => {
|
themeListeners.forEach((listener) => {
|
||||||
try {
|
try {
|
||||||
listener(theme, resolved);
|
listener(theme, resolved);
|
||||||
@@ -286,71 +247,61 @@ const notifyThemeListeners = (theme, resolved) => {
|
|||||||
/**
|
/**
|
||||||
* 清除所有主题监听器
|
* 清除所有主题监听器
|
||||||
*/
|
*/
|
||||||
export const clearThemeListeners = () => {
|
export const clearThemeListeners = (): void => {
|
||||||
themeListeners.clear();
|
themeListeners.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册自定义主题
|
* 注册自定义主题
|
||||||
* @param {string} name - 主题名称
|
|
||||||
* @param {Object} config - 主题配置
|
|
||||||
*/
|
*/
|
||||||
export const registerTheme = (name, config) => {
|
export const registerTheme = (name: string, config: ThemeConfig): void => {
|
||||||
THEMES[name] = {
|
(THEMES as Record<string, ThemeConfig>)[name] = {
|
||||||
bg: config.bg || THEMES.light.bg,
|
bg: config.bg || (THEMES.light as ThemeConfig).bg,
|
||||||
text: config.text || THEMES.light.text,
|
text: config.text || (THEMES.light as ThemeConfig).text,
|
||||||
border: config.border || THEMES.light.border,
|
border: config.border || (THEMES.light as ThemeConfig).border,
|
||||||
shadow: config.shadow || THEMES.light.shadow,
|
shadow: config.shadow || (THEMES.light as ThemeConfig).shadow,
|
||||||
hoverShadow: config.hoverShadow || THEMES.light.hoverShadow,
|
hoverShadow: config.hoverShadow || (THEMES.light as ThemeConfig).hoverShadow,
|
||||||
progressBg: config.progressBg || THEMES.light.progressBg,
|
progressBg: config.progressBg || (THEMES.light as ThemeConfig).progressBg,
|
||||||
closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg,
|
closeHoverBg: config.closeHoverBg || (THEMES.light as ThemeConfig).closeHoverBg,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注销自定义主题
|
* 注销自定义主题
|
||||||
* @param {string} name - 主题名称
|
|
||||||
*/
|
*/
|
||||||
export const unregisterTheme = (name) => {
|
export const unregisterTheme = (name: string): void => {
|
||||||
if (name === 'light' || name === 'dark' || name === 'auto') {
|
if (name === 'light' || name === 'dark' || name === 'auto') {
|
||||||
console.warn('Cannot unregister built-in theme:', name);
|
console.warn('Cannot unregister built-in theme:', name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
delete (THEMES as Record<string, ThemeConfig>)[name];
|
||||||
delete THEMES[name];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取所有主题
|
* 获取所有主题
|
||||||
* @returns {Object} 主题映射
|
|
||||||
*/
|
*/
|
||||||
export const getAllThemes = () => {
|
export const getAllThemes = (): Record<string, ThemeConfig> => {
|
||||||
return { ...THEMES };
|
return { ...THEMES } as Record<string, ThemeConfig>;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主题名称列表
|
* 获取主题名称列表
|
||||||
* @returns {Array} 主题名称数组
|
|
||||||
*/
|
*/
|
||||||
export const getThemeNames = () => {
|
export const getThemeNames = (): string[] => {
|
||||||
return Object.keys(THEMES);
|
return Object.keys(THEMES);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 检查主题是否存在
|
* 检查主题是否存在
|
||||||
* @param {string} name - 主题名称
|
|
||||||
* @returns {boolean} 是否存在
|
|
||||||
*/
|
*/
|
||||||
export const hasTheme = (name) => {
|
export const hasTheme = (name: string): boolean => {
|
||||||
return name in THEMES;
|
return name in THEMES;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取主题预览
|
* 获取主题预览
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
* @returns {Object} 主题预览
|
|
||||||
*/
|
*/
|
||||||
export const getThemePreview = (theme) => {
|
export const getThemePreview = (theme: string): ThemePreview => {
|
||||||
const config = getThemeConfig(theme);
|
const config = getThemeConfig(theme);
|
||||||
const resolved = resolveTheme(theme);
|
const resolved = resolveTheme(theme);
|
||||||
|
|
||||||
@@ -370,10 +321,8 @@ export const getThemePreview = (theme) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成主题CSS
|
* 生成主题CSS
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
* @returns {string} CSS字符串
|
|
||||||
*/
|
*/
|
||||||
export const generateThemeCSS = (theme) => {
|
export const generateThemeCSS = (theme: string): string => {
|
||||||
const config = getThemeConfig(theme);
|
const config = getThemeConfig(theme);
|
||||||
const resolved = resolveTheme(theme);
|
const resolved = resolveTheme(theme);
|
||||||
|
|
||||||
@@ -392,21 +341,18 @@ export const generateThemeCSS = (theme) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 应用主题CSS
|
* 应用主题CSS
|
||||||
* @param {string} theme - 主题名称
|
|
||||||
*/
|
*/
|
||||||
export const applyThemeCSS = (theme) => {
|
export const applyThemeCSS = (theme: string): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
const css = generateThemeCSS(theme);
|
const css = generateThemeCSS(theme);
|
||||||
const styleId = 'metona-toast-theme-css';
|
const styleId = 'metona-toast-theme-css';
|
||||||
|
|
||||||
// 移除旧样式
|
|
||||||
const oldStyle = document.getElementById(styleId);
|
const oldStyle = document.getElementById(styleId);
|
||||||
if (oldStyle) {
|
if (oldStyle) {
|
||||||
oldStyle.remove();
|
oldStyle.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加新样式
|
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
style.id = styleId;
|
style.id = styleId;
|
||||||
style.textContent = css;
|
style.textContent = css;
|
||||||
@@ -416,7 +362,7 @@ export const applyThemeCSS = (theme) => {
|
|||||||
/**
|
/**
|
||||||
* 移除主题CSS
|
* 移除主题CSS
|
||||||
*/
|
*/
|
||||||
export const removeThemeCSS = () => {
|
export const removeThemeCSS = (): void => {
|
||||||
if (typeof document === 'undefined') return;
|
if (typeof document === 'undefined') return;
|
||||||
|
|
||||||
const styleId = 'metona-toast-theme-css';
|
const styleId = 'metona-toast-theme-css';
|
||||||
@@ -427,9 +373,9 @@ export const removeThemeCSS = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题工具 — 代理层,直接引用模块函数
|
* 主题工具 — 代理层
|
||||||
*/
|
*/
|
||||||
export const themeUtils = {
|
export const themeUtils: ThemeUtils = {
|
||||||
getSystemTheme,
|
getSystemTheme,
|
||||||
resolveTheme,
|
resolveTheme,
|
||||||
getThemeConfig,
|
getThemeConfig,
|
||||||
+551
@@ -0,0 +1,551 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast Toast — Toast 类
|
||||||
|
* @module toast
|
||||||
|
* @version 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { generateId, escapeHTML } from './utils.js';
|
||||||
|
import { DEFAULTS, ICONS, TYPE_COLORS, THEMES } from './constants.js';
|
||||||
|
import { injectStyles } from './styles.js';
|
||||||
|
import { getTheme } from './themes.js';
|
||||||
|
import { t, getLocaleDirection, getCurrentLocale } from './i18n.js';
|
||||||
|
import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toast 类 — 核心通知组件
|
||||||
|
*/
|
||||||
|
export class Toast implements ToastInstance {
|
||||||
|
// 静态钩子系统
|
||||||
|
static _hooks: Map<string, Array<(toast: Toast) => void>> = new Map();
|
||||||
|
|
||||||
|
// 由 api.ts 注入的回调,避免循环依赖
|
||||||
|
static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
||||||
|
static _removeToast: ((id: string) => void) | null = null;
|
||||||
|
|
||||||
|
static on(name: string, fn: (toast: Toast) => void): () => void {
|
||||||
|
if (!this._hooks.has(name)) this._hooks.set(name, []);
|
||||||
|
this._hooks.get(name)!.push(fn);
|
||||||
|
return () => this.off(name, fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
static off(name: string, fn: (toast: Toast) => void): void {
|
||||||
|
const list = this._hooks.get(name);
|
||||||
|
if (list) this._hooks.set(name, list.filter(f => f !== fn));
|
||||||
|
}
|
||||||
|
|
||||||
|
static trigger(name: string, toast: Toast): void {
|
||||||
|
const list = this._hooks.get(name);
|
||||||
|
if (list) {
|
||||||
|
list.forEach(fn => {
|
||||||
|
try { fn(toast); }
|
||||||
|
catch (e) {
|
||||||
|
console.error('Hook error:', name, e);
|
||||||
|
if (Toast._onError) {
|
||||||
|
try { Toast._onError({ hook: name, error: e as Error, toast }); } catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
id: string;
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
message: string;
|
||||||
|
html: string;
|
||||||
|
iconHTML: string;
|
||||||
|
config: ToastConfig;
|
||||||
|
el: HTMLElement | null = null;
|
||||||
|
barEl: HTMLElement | null = null;
|
||||||
|
rafId: number | null = null;
|
||||||
|
remaining: number;
|
||||||
|
startedAt: number = 0;
|
||||||
|
paused: boolean = false;
|
||||||
|
closing: boolean = false;
|
||||||
|
group: string | null = null;
|
||||||
|
_cleanups: Array<() => void> = [];
|
||||||
|
|
||||||
|
constructor(opts: ToastOptions) {
|
||||||
|
this.id = opts.id || generateId();
|
||||||
|
this.type = opts.type || 'default';
|
||||||
|
this.title = opts.title || '';
|
||||||
|
this.message = opts.message ?? opts.content ?? '';
|
||||||
|
this.html = opts.html || '';
|
||||||
|
this.iconHTML = opts.iconHTML || '';
|
||||||
|
this.config = { ...DEFAULTS as unknown as ToastConfig, ...opts };
|
||||||
|
// 防止嵌套对象被多个 toast 实例共享引用
|
||||||
|
if (opts.style && typeof opts.style === 'object') {
|
||||||
|
this.config.style = { ...opts.style };
|
||||||
|
}
|
||||||
|
this.remaining = this.config.duration || 0;
|
||||||
|
this.group = opts.group || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_palette(): { theme: string; c: TypeColor; t: Partial<ThemeConfig> } {
|
||||||
|
const theme = getTheme(this.config.theme || 'auto');
|
||||||
|
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
|
||||||
|
const tc = (THEMES[theme] as ThemeConfig) || (THEMES.light as ThemeConfig);
|
||||||
|
const t: Partial<ThemeConfig> = { bg: tc.bg, border: tc.border, shadow: tc.shadow };
|
||||||
|
return { theme, c, t };
|
||||||
|
}
|
||||||
|
|
||||||
|
create(): this {
|
||||||
|
if (typeof window === 'undefined' || typeof document === 'undefined') return this;
|
||||||
|
|
||||||
|
Toast.trigger('beforeShow', this);
|
||||||
|
injectStyles();
|
||||||
|
|
||||||
|
const container = this._getContainer();
|
||||||
|
const { theme, c, t } = this._palette();
|
||||||
|
|
||||||
|
this._limitToasts(container);
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = this._buildClassName(theme);
|
||||||
|
el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status');
|
||||||
|
el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite');
|
||||||
|
el.dataset.id = this.id;
|
||||||
|
|
||||||
|
this._applyStyles(el, theme, t);
|
||||||
|
this._buildContent(el, c);
|
||||||
|
|
||||||
|
this.el = el;
|
||||||
|
// Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore
|
||||||
|
const pos = this.config.position || 'top-right';
|
||||||
|
if (pos.startsWith('top')) {
|
||||||
|
container.insertBefore(el, container.firstChild);
|
||||||
|
} else {
|
||||||
|
container.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bindEvents(el);
|
||||||
|
this._startTimer();
|
||||||
|
|
||||||
|
// 进入动画
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
el.classList.add('met-show');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof this.config.onShow === 'function') {
|
||||||
|
try {
|
||||||
|
this.config.onShow(this);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('onShow callback error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.trigger('afterShow', this);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getContainer(): HTMLElement {
|
||||||
|
const position = this.config.position || 'top-right';
|
||||||
|
const zIndex = this.config.zIndex || 9999;
|
||||||
|
|
||||||
|
// 从模块级缓存获取 (由 api.ts 管理)
|
||||||
|
const cached = _containerCache.get(document.body);
|
||||||
|
if (cached && cached.has(position)) {
|
||||||
|
return cached.get(position)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中时创建新容器
|
||||||
|
if (!_containerCache.has(document.body)) {
|
||||||
|
_containerCache.set(document.body, new Map());
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = `met-container ${position}`;
|
||||||
|
el.setAttribute('aria-label', 'Notifications');
|
||||||
|
el.setAttribute('role', 'region');
|
||||||
|
|
||||||
|
const posStyles: Record<string, string> = {
|
||||||
|
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||||
|
'top-center': 'top:0;left:0;right:0;align-items:center',
|
||||||
|
'top-right': 'top:0;right:0;align-items:flex-end',
|
||||||
|
'bottom-left': 'bottom:0;left:0;align-items:flex-start',
|
||||||
|
'bottom-center': 'bottom:0;left:0;right:0;align-items:center',
|
||||||
|
'bottom-right': 'bottom:0;right:0;align-items:flex-end',
|
||||||
|
};
|
||||||
|
el.style.cssText = `
|
||||||
|
display:flex;
|
||||||
|
flex-direction:column;
|
||||||
|
gap:${this.config.gap || 12}px;
|
||||||
|
box-sizing:border-box;
|
||||||
|
padding:${this.config.offset || 24}px;
|
||||||
|
max-width:100vw;
|
||||||
|
position:fixed;
|
||||||
|
z-index:${zIndex};
|
||||||
|
pointer-events:none;
|
||||||
|
${posStyles[position] || 'top:0;right:0;align-items:flex-end'}
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(el);
|
||||||
|
_containerCache.get(document.body)!.set(position, el);
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
_limitToasts(container: HTMLElement): void {
|
||||||
|
const max = this.config.max || 6;
|
||||||
|
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||||
|
if (list.length >= max) {
|
||||||
|
const first = list[0] as HTMLElement;
|
||||||
|
const id = first?.dataset.id;
|
||||||
|
if (id && Toast._removeToast) {
|
||||||
|
Toast._removeToast(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildClassName(theme: string): string {
|
||||||
|
const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom',
|
||||||
|
'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
||||||
|
const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide';
|
||||||
|
return [
|
||||||
|
'met-toast',
|
||||||
|
`met-${this.type}`,
|
||||||
|
`met-theme-${theme}`,
|
||||||
|
`met-anim-${anim}`,
|
||||||
|
(this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '',
|
||||||
|
this.config.className || '',
|
||||||
|
].filter(Boolean).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
_applyStyles(el: HTMLElement, _theme: string, _t: Partial<ThemeConfig>): void {
|
||||||
|
const widthValue = typeof this.config.width === 'number'
|
||||||
|
? `${this.config.width}px`
|
||||||
|
: (typeof this.config.width === 'string' ? this.config.width : '360px');
|
||||||
|
|
||||||
|
const set = (p: string, v: string) => el.style.setProperty(p, v, 'important');
|
||||||
|
set('position', 'relative');
|
||||||
|
set('flex-shrink', '0');
|
||||||
|
set('min-width', '240px');
|
||||||
|
set('max-width', 'calc(100vw - 48px)');
|
||||||
|
set('width', widthValue);
|
||||||
|
// 用户自定义样式最后应用
|
||||||
|
if (this.config.style && Object.keys(this.config.style).length > 0) {
|
||||||
|
Object.entries(this.config.style).forEach(([k, v]) => { (el.style as unknown as Record<string, string>)[k] = v; });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_buildContent(el: HTMLElement, c: TypeColor): void {
|
||||||
|
// 自定义渲染函数 — 完全接管 DOM 构建
|
||||||
|
if (typeof this.config.render === 'function') {
|
||||||
|
el.innerHTML = this.config.render(this);
|
||||||
|
this.barEl = el.querySelector('.met-bar, .met-bar-v');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]);
|
||||||
|
const showClose = this.config.closeButton !== false;
|
||||||
|
const showProgress = this.config.showProgress !== false && (this.config.duration || 0) > 0;
|
||||||
|
const showSide = (!showIcon && this.type !== 'default');
|
||||||
|
|
||||||
|
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
|
||||||
|
const safeMessage = this.html
|
||||||
|
? `<div class="met-message">${this.html}</div>`
|
||||||
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||||
|
|
||||||
|
const iconHTML = showIcon
|
||||||
|
? `<div class="met-icon" style="color:${c.fg}">${this.iconHTML || ICONS[this.type]}</div>` : '';
|
||||||
|
const closeHTML = showClose
|
||||||
|
? `<button class="met-close" type="button" aria-label="${t('close')}">
|
||||||
|
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||||
|
</svg>
|
||||||
|
</button>`
|
||||||
|
: '';
|
||||||
|
const progressHTML = showProgress
|
||||||
|
? (this.config.progressDirection === 'vertical'
|
||||||
|
? `<div class="met-progress-v"><div class="met-bar-v" style="background:${c.fg}"></div></div>`
|
||||||
|
: `<div class="met-progress"><div class="met-bar" style="background:${c.fg}"></div></div>`)
|
||||||
|
: '';
|
||||||
|
const sideHTML = showSide
|
||||||
|
? `<div class="met-side" style="background:${c.fg}"></div>` : '';
|
||||||
|
|
||||||
|
el.innerHTML = `
|
||||||
|
${iconHTML}
|
||||||
|
${sideHTML}
|
||||||
|
<div class="met-content">
|
||||||
|
${safeTitle}
|
||||||
|
${safeMessage}
|
||||||
|
</div>
|
||||||
|
${closeHTML}
|
||||||
|
${progressHTML}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.barEl = el.querySelector('.met-bar, .met-bar-v');
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindEvents(el: HTMLElement): void {
|
||||||
|
const eventHandler = (e: Event) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
|
||||||
|
if (target.closest('.met-close')) {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||||
|
if (typeof this.config.onClick === 'function') {
|
||||||
|
try {
|
||||||
|
this.config.onClick(this);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('onClick callback error:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('click', eventHandler);
|
||||||
|
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||||
|
|
||||||
|
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||||
|
const mouseEnter = () => this._pause();
|
||||||
|
const mouseLeave = () => this._resume();
|
||||||
|
|
||||||
|
el.addEventListener('mouseenter', mouseEnter);
|
||||||
|
el.addEventListener('mouseleave', mouseLeave);
|
||||||
|
|
||||||
|
this._cleanups.push(() => {
|
||||||
|
el.removeEventListener('mouseenter', mouseEnter);
|
||||||
|
el.removeEventListener('mouseleave', mouseLeave);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.config.draggable) {
|
||||||
|
this._bindDrag(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_bindDrag(el: HTMLElement): void {
|
||||||
|
let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false;
|
||||||
|
|
||||||
|
const down = (e: PointerEvent) => {
|
||||||
|
if ((e.target as HTMLElement).closest('.met-close')) return;
|
||||||
|
dragging = true;
|
||||||
|
sx = e.clientX;
|
||||||
|
sy = e.clientY;
|
||||||
|
el.setPointerCapture(e.pointerId);
|
||||||
|
el.style.transition = 'none';
|
||||||
|
this._pause();
|
||||||
|
};
|
||||||
|
|
||||||
|
const move = (e: PointerEvent) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
dx = e.clientX - sx;
|
||||||
|
dy = e.clientY - sy;
|
||||||
|
el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`;
|
||||||
|
el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200));
|
||||||
|
};
|
||||||
|
|
||||||
|
const up = (e: PointerEvent) => {
|
||||||
|
if (!dragging) return;
|
||||||
|
dragging = false;
|
||||||
|
el.releasePointerCapture(e.pointerId);
|
||||||
|
el.style.transition = '';
|
||||||
|
|
||||||
|
if (Math.abs(dx) > 120) {
|
||||||
|
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||||
|
el.style.opacity = '0';
|
||||||
|
setTimeout(() => this.close(true), 250);
|
||||||
|
} else {
|
||||||
|
el.style.transform = '';
|
||||||
|
el.style.opacity = '';
|
||||||
|
this._resume();
|
||||||
|
}
|
||||||
|
dx = dy = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addEventListener('pointerdown', down);
|
||||||
|
el.addEventListener('pointermove', move);
|
||||||
|
el.addEventListener('pointerup', up);
|
||||||
|
el.addEventListener('pointercancel', up);
|
||||||
|
|
||||||
|
this._cleanups.push(() => {
|
||||||
|
el.removeEventListener('pointerdown', down);
|
||||||
|
el.removeEventListener('pointermove', move);
|
||||||
|
el.removeEventListener('pointerup', up);
|
||||||
|
el.removeEventListener('pointercancel', up);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_startTimer(resuming = false): void {
|
||||||
|
if ((this.config.duration || 0) <= 0) return;
|
||||||
|
|
||||||
|
if (!resuming) {
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this.remaining = this.config.duration || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tick = (): void => {
|
||||||
|
if (this.paused || this.closing) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const elapsed = Date.now() - this.startedAt;
|
||||||
|
this.remaining = Math.max(0, (this.config.duration || 0) - elapsed);
|
||||||
|
|
||||||
|
if (this.barEl) {
|
||||||
|
const ratio = this.remaining / (this.config.duration || 1);
|
||||||
|
const t = this.config.progressDirection === 'vertical'
|
||||||
|
? `scaleY(${ratio})` : `scaleX(${ratio})`;
|
||||||
|
this.barEl.style.transform = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.remaining <= 0) {
|
||||||
|
this.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Timer tick error:', e);
|
||||||
|
if (Toast._onError) {
|
||||||
|
try { Toast._onError({ source: 'timer', error: e as Error, toast: this }); } catch (_e) { /* noop */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.rafId = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.rafId = requestAnimationFrame(tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
_pause(): void {
|
||||||
|
if (this.paused || (this.config.duration || 0) <= 0) return;
|
||||||
|
this.paused = true;
|
||||||
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||||
|
this.remaining = Math.max(0, (this.config.duration || 0) - (Date.now() - this.startedAt));
|
||||||
|
}
|
||||||
|
|
||||||
|
_resume(): void {
|
||||||
|
if (!this.paused) return;
|
||||||
|
this.paused = false;
|
||||||
|
this.startedAt = Date.now() - ((this.config.duration || 0) - this.remaining);
|
||||||
|
this._startTimer(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(partial: Partial<ToastOptions>): this {
|
||||||
|
Toast.trigger('beforeUpdate', this);
|
||||||
|
const typeChanged = partial.type && partial.type !== this.type;
|
||||||
|
if (partial.type) this.type = partial.type;
|
||||||
|
if (partial.title !== undefined) this.title = partial.title;
|
||||||
|
if (partial.message !== undefined) this.message = partial.message;
|
||||||
|
if (partial.html !== undefined) this.html = partial.html;
|
||||||
|
|
||||||
|
if (!this.el) { Toast.trigger('afterUpdate', this); return this; }
|
||||||
|
|
||||||
|
// 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色
|
||||||
|
if (typeChanged) {
|
||||||
|
const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default'];
|
||||||
|
typeClasses.forEach(c => this.el!.classList.remove(c));
|
||||||
|
this.el.classList.add(`met-${this.type}`);
|
||||||
|
if (this.barEl) {
|
||||||
|
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
|
||||||
|
this.barEl.style.background = c.fg;
|
||||||
|
}
|
||||||
|
const side = this.el.querySelector('.met-side') as HTMLElement | null;
|
||||||
|
if (side) {
|
||||||
|
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
|
||||||
|
side.style.background = c.fg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = this.el.querySelector('.met-content');
|
||||||
|
if (content) {
|
||||||
|
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
|
||||||
|
const safeMessage = this.html
|
||||||
|
? `<div class="met-message">${this.html}</div>`
|
||||||
|
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||||
|
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||||
|
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||||
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||||
|
this.startedAt = Date.now();
|
||||||
|
this.remaining = this.config.duration || 0;
|
||||||
|
this._startTimer();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this.config.onUpdate === 'function') {
|
||||||
|
try { this.config.onUpdate(this); } catch (e) { console.error('onUpdate callback error:', e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.trigger('afterUpdate', this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
close(immediate = false): void {
|
||||||
|
if (this.closing) return;
|
||||||
|
this.closing = true;
|
||||||
|
|
||||||
|
Toast.trigger('beforeClose', this);
|
||||||
|
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||||
|
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
||||||
|
this._cleanups = [];
|
||||||
|
|
||||||
|
const el = this.el;
|
||||||
|
if (!el) {
|
||||||
|
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即脱离文档流
|
||||||
|
const container = el.parentNode;
|
||||||
|
if (container && !immediate) {
|
||||||
|
const elRect = el.getBoundingClientRect();
|
||||||
|
const containerRect = (container as HTMLElement).getBoundingClientRect();
|
||||||
|
el.style.position = 'absolute';
|
||||||
|
el.style.top = (elRect.top - containerRect.top) + 'px';
|
||||||
|
el.style.left = (elRect.left - containerRect.left) + 'px';
|
||||||
|
el.style.width = elRect.width + 'px';
|
||||||
|
el.style.margin = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
el.classList.add('met-leaving');
|
||||||
|
el.classList.remove('met-show');
|
||||||
|
|
||||||
|
const pos = this.config.position || 'top-right';
|
||||||
|
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
|
||||||
|
let transform = 'scale(.96)';
|
||||||
|
|
||||||
|
if (pos.includes('right')) transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)';
|
||||||
|
else if (pos.includes('left')) transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)';
|
||||||
|
else if (pos.startsWith('top')) transform = 'translateY(-20px)';
|
||||||
|
else transform = 'translateY(20px)';
|
||||||
|
|
||||||
|
el.style.transform = transform;
|
||||||
|
el.style.opacity = '0';
|
||||||
|
|
||||||
|
setTimeout(() => this._destroy(), immediate ? 0 : 300);
|
||||||
|
}
|
||||||
|
|
||||||
|
_destroy(): void {
|
||||||
|
if (this.el && this.el.parentNode) {
|
||||||
|
this.el.parentNode.removeChild(this.el);
|
||||||
|
}
|
||||||
|
this.el = null;
|
||||||
|
|
||||||
|
if (typeof this.config.onClose === 'function') {
|
||||||
|
try {
|
||||||
|
this.config.onClose(this);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('onClose callback error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.trigger('afterClose', this);
|
||||||
|
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 模块级共享容器缓存,所有 Toast 实例共用
|
||||||
|
*/
|
||||||
|
export const _containerCache: Map<Node, Map<string, HTMLElement>> = new Map();
|
||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
/**
|
||||||
|
* MetonaToast — 核心类型定义
|
||||||
|
* @module types
|
||||||
|
* @version 0.2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ========== 基础类型 ==========
|
||||||
|
|
||||||
|
/** Toast 通知类型(107种内置图标类型) */
|
||||||
|
export 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 显示位置 */
|
||||||
|
export type ToastPosition =
|
||||||
|
| 'top-left' | 'top-center' | 'top-right'
|
||||||
|
| 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||||
|
|
||||||
|
/** Toast 主题 */
|
||||||
|
export type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
|
||||||
|
|
||||||
|
/** Toast 动画类型 */
|
||||||
|
export type ToastAnimation =
|
||||||
|
| 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom'
|
||||||
|
| 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight'
|
||||||
|
| (string & {});
|
||||||
|
|
||||||
|
/** 进度条方向 */
|
||||||
|
export type ToastProgressDirection = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
/** 语言代码 */
|
||||||
|
export type ToastLocale =
|
||||||
|
| 'zh-CN' | 'zh-TW' | 'en-US' | 'en-GB' | 'ja' | 'ko' | 'fr' | 'de'
|
||||||
|
| 'es' | 'pt' | 'ru' | 'ar' | 'hi' | 'th' | 'vi' | 'id' | 'ms' | 'tr'
|
||||||
|
| 'it' | 'nl' | 'pl' | 'uk' | 'cs' | 'sv' | 'da' | 'fi' | 'nb' | 'el'
|
||||||
|
| 'he' | 'hu' | 'ro' | 'bg' | 'hr' | 'sk' | 'sl' | 'et' | 'lv' | 'lt'
|
||||||
|
| 'ca' | 'gl' | 'eu' | 'cy' | 'ga' | 'mt' | 'is' | 'mk' | 'sq' | 'sr'
|
||||||
|
| 'bs' | 'me' | 'ka' | 'hy' | 'az' | 'uz' | 'kk' | 'ky' | 'tg' | 'tk'
|
||||||
|
| 'mn' | 'ne' | 'si' | 'my' | 'km' | 'lo' | 'am' | 'sw' | 'yo' | 'ig'
|
||||||
|
| 'ha' | 'zu' | 'af' | 'xh' | 'st' | 'tn' | 'ts' | 'ss' | 've' | 'nr';
|
||||||
|
|
||||||
|
// ========== 颜色配置 ==========
|
||||||
|
|
||||||
|
export interface TypeColor {
|
||||||
|
fg: string;
|
||||||
|
bg: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Toast 配置 ==========
|
||||||
|
|
||||||
|
export interface ToastConfig {
|
||||||
|
position?: ToastPosition;
|
||||||
|
duration?: number;
|
||||||
|
max?: number;
|
||||||
|
gap?: number;
|
||||||
|
offset?: number;
|
||||||
|
pauseOnHover?: boolean;
|
||||||
|
closeOnClick?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
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;
|
||||||
|
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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ErrorInfo {
|
||||||
|
hook?: string;
|
||||||
|
source?: string;
|
||||||
|
error: Error;
|
||||||
|
toast?: ToastInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Toast 选项与实例 ==========
|
||||||
|
|
||||||
|
export interface ToastOptions extends Partial<ToastConfig> {
|
||||||
|
type?: string;
|
||||||
|
title?: string;
|
||||||
|
message?: string;
|
||||||
|
content?: string;
|
||||||
|
html?: string;
|
||||||
|
iconHTML?: string;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
close(immediate?: boolean): void;
|
||||||
|
_pause(): void;
|
||||||
|
_resume(): void;
|
||||||
|
_destroy(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 控制对象 ==========
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressControl {
|
||||||
|
id: string;
|
||||||
|
setProgress(percent: number): void;
|
||||||
|
complete(message?: string): void;
|
||||||
|
error(message?: string): void;
|
||||||
|
dismiss(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountdownControl {
|
||||||
|
id: string;
|
||||||
|
cancel(): void;
|
||||||
|
pause(): void;
|
||||||
|
resume(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ActionControl {
|
||||||
|
id: string;
|
||||||
|
toast: ToastInstance;
|
||||||
|
dismiss(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Action 按钮 ==========
|
||||||
|
|
||||||
|
export interface ActionButton {
|
||||||
|
text: string;
|
||||||
|
onClick: (toast: ToastInstance) => void;
|
||||||
|
color?: string;
|
||||||
|
style?: Record<string, string>;
|
||||||
|
close?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== Options 接口 ==========
|
||||||
|
|
||||||
|
export interface PromiseOptions extends ToastOptions {
|
||||||
|
loading?: string;
|
||||||
|
success?: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfirmOptions extends ToastOptions {
|
||||||
|
confirmText?: string;
|
||||||
|
confirmColor?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
cancelColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromptOptions extends ToastOptions {
|
||||||
|
inputType?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
defaultValue?: string;
|
||||||
|
submitText?: string;
|
||||||
|
submitColor?: string;
|
||||||
|
cancelText?: string;
|
||||||
|
cancelColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressOptions extends ToastOptions {
|
||||||
|
progressColor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CountdownOptions extends ToastOptions {
|
||||||
|
onComplete?: (() => void) | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface QueueOptions extends ToastOptions {
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StackOptions extends ToastOptions {
|
||||||
|
stagger?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InitOptions {
|
||||||
|
config?: Partial<ToastConfig>;
|
||||||
|
theme?: ToastTheme;
|
||||||
|
locale?: string;
|
||||||
|
plugins?: Array<string | Plugin>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StatusInfo {
|
||||||
|
version: string;
|
||||||
|
toasts: number;
|
||||||
|
theme: string;
|
||||||
|
locale: string;
|
||||||
|
plugins: string[];
|
||||||
|
animations: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 动画 ==========
|
||||||
|
|
||||||
|
export interface AnimationConfig {
|
||||||
|
enter: Record<string, string>;
|
||||||
|
leave: Record<string, string>;
|
||||||
|
duration: number;
|
||||||
|
easing: string;
|
||||||
|
name?: string;
|
||||||
|
delay?: number;
|
||||||
|
iterations?: number;
|
||||||
|
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
||||||
|
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 主题 ==========
|
||||||
|
|
||||||
|
export interface ThemeConfig {
|
||||||
|
bg: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
shadow: string;
|
||||||
|
hoverShadow: string;
|
||||||
|
progressBg: string;
|
||||||
|
closeHoverBg: string;
|
||||||
|
backdropFilter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ThemePreview {
|
||||||
|
name: string;
|
||||||
|
resolved: string;
|
||||||
|
colors: {
|
||||||
|
background: string;
|
||||||
|
text: string;
|
||||||
|
border: string;
|
||||||
|
};
|
||||||
|
isDark: boolean;
|
||||||
|
isLight: boolean;
|
||||||
|
isAuto: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 国际化 ==========
|
||||||
|
|
||||||
|
export interface LocaleInfo {
|
||||||
|
code: string;
|
||||||
|
name: string;
|
||||||
|
direction: 'ltr' | 'rtl';
|
||||||
|
isSupported: boolean;
|
||||||
|
isCurrent: boolean;
|
||||||
|
isFallback: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 插件 ==========
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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[] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== MeToast 主接口 ==========
|
||||||
|
|
||||||
|
export 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;
|
||||||
|
|
||||||
|
// Toast 方法
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Toast 管理
|
||||||
|
find(id: string): ToastInstance | undefined;
|
||||||
|
dismiss(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>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 钩子名称 ==========
|
||||||
|
|
||||||
|
export const HOOK_NAMES = {
|
||||||
|
BEFORE_INIT: 'beforeInit',
|
||||||
|
AFTER_INIT: 'afterInit',
|
||||||
|
BEFORE_DESTROY: 'beforeDestroy',
|
||||||
|
AFTER_DESTROY: 'afterDestroy',
|
||||||
|
BEFORE_SHOW: 'beforeShow',
|
||||||
|
AFTER_SHOW: 'afterShow',
|
||||||
|
BEFORE_CLOSE: 'beforeClose',
|
||||||
|
AFTER_CLOSE: 'afterClose',
|
||||||
|
BEFORE_UPDATE: 'beforeUpdate',
|
||||||
|
AFTER_UPDATE: 'afterUpdate',
|
||||||
|
CONFIG_CHANGE: 'configChange',
|
||||||
|
THEME_CHANGE: 'themeChange',
|
||||||
|
LOCALE_CHANGE: 'localeChange',
|
||||||
|
CLICK: 'click',
|
||||||
|
HOVER: 'hover',
|
||||||
|
DRAG_START: 'dragStart',
|
||||||
|
DRAG_END: 'dragEnd',
|
||||||
|
ANIMATION_START: 'animationStart',
|
||||||
|
ANIMATION_END: 'animationEnd',
|
||||||
|
PROGRESS_START: 'progressStart',
|
||||||
|
PROGRESS_UPDATE: 'progressUpdate',
|
||||||
|
PROGRESS_END: 'progressEnd',
|
||||||
|
ERROR: 'error',
|
||||||
|
CUSTOM: 'custom',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES];
|
||||||
|
|
||||||
|
// ========== 全局声明 ==========
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
MeToast: MeToast;
|
||||||
|
Met: MeToast;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* MetonaToast Utils - 精简工具函数
|
* MetonaToast Utils — 工具函数
|
||||||
* @module utils
|
* @module utils
|
||||||
* @version 0.1.2
|
* @version 0.2.0
|
||||||
* @description 仅保留核心模块实际使用的工具函数
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成唯一ID
|
* 生成唯一ID
|
||||||
* @returns {string} 唯一ID
|
|
||||||
*/
|
*/
|
||||||
export const generateId = () => {
|
export const generateId = (): string => {
|
||||||
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* HTML转义
|
* HTML转义
|
||||||
* @param {string} s - 输入字符串
|
* @param s - 输入字符串
|
||||||
* @returns {string} 转义后的字符串
|
|
||||||
*/
|
*/
|
||||||
export const escapeHTML = (s) => {
|
export const escapeHTML = (s: string | null | undefined): string => {
|
||||||
if (typeof document === 'undefined') {
|
if (typeof document === 'undefined') {
|
||||||
return String(s == null ? '' : s)
|
return String(s == null ? '' : s)
|
||||||
.replace(/&/g, '&')
|
.replace(/&/g, '&')
|
||||||
@@ -35,10 +32,9 @@ export const escapeHTML = (s) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 检测暗色模式偏好
|
* 检测暗色模式偏好
|
||||||
* @returns {boolean} 是否偏好暗色模式
|
|
||||||
*/
|
*/
|
||||||
export const prefersDark = () => {
|
export const prefersDark = (): boolean => {
|
||||||
return typeof window !== 'undefined' &&
|
return typeof window !== 'undefined' &&
|
||||||
window.matchMedia &&
|
typeof window.matchMedia === 'function' &&
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
};
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"declaration": false,
|
||||||
|
"declarationMap": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist", "tests", "site"]
|
||||||
|
}
|
||||||
Vendored
-1046
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user