final changes
This commit is contained in:
@@ -7,3 +7,6 @@ Archive_*/**
|
||||
|
||||
#ignore dist folder
|
||||
**/dist/**
|
||||
|
||||
#ignore log files
|
||||
**/*.log
|
||||
@@ -1,29 +0,0 @@
|
||||
# Development Environment Variables for Local Build
|
||||
# These are used when running build scripts outside of Docker containers
|
||||
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
|
||||
# Database Configuration (Docker containers)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
DB_NAME=serpentrace
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# Redis Configuration (Docker containers)
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# JWT Configuration
|
||||
JWT_SECRET=dev_jwt_secret_change_in_production
|
||||
JWT_EXPIRATION=24h
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
|
||||
# MinIO Configuration (Docker containers)
|
||||
MINIO_ENDPOINT=localhost
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=serpentrace
|
||||
MINIO_SECRET_KEY=serpentrace123!
|
||||
MINIO_USE_SSL=false
|
||||
@@ -20,10 +20,28 @@ REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://localhost:6379
|
||||
|
||||
# JWT CONFIGURATION
|
||||
JWT_SECRET=your_super_secret_jwt_key_change_in_production
|
||||
JWT_EXPIRY=86400
|
||||
JWT_EXPIRATION=24h
|
||||
# JWT AUTHENTICATION CONFIGURATION
|
||||
JWT_SECRET=your-super-secure-secret-key-here
|
||||
JWT_REFRESH_SECRET=your-super-secure-refresh-secret-key-here
|
||||
|
||||
# Access Token Expiry (choose ONE option, priority order listed):
|
||||
JWT_ACCESS_TOKEN_EXPIRY=1800 # Seconds (recommended for production)
|
||||
# JWT_ACCESS_TOKEN_EXPIRATION=30m # Duration string (user-friendly)
|
||||
# JWT_EXPIRY=1800 # Legacy: seconds
|
||||
# JWT_EXPIRATION=30m # Legacy: duration string
|
||||
|
||||
# Refresh Token Expiry (choose ONE option, priority order listed):
|
||||
JWT_REFRESH_TOKEN_EXPIRY=604800 # Seconds (7 days)
|
||||
# JWT_REFRESH_TOKEN_EXPIRATION=7d # Duration string (recommended)
|
||||
# JWT_REFRESH_EXPIRATION=7d # Legacy: duration string
|
||||
|
||||
# Cookie Names (optional)
|
||||
JWT_COOKIE_NAME=auth_token
|
||||
JWT_REFRESH_COOKIE_NAME=refresh_token
|
||||
|
||||
# Legacy JWT Configuration (deprecated - use above options)
|
||||
# JWT_EXPIRY=86400
|
||||
# JWT_EXPIRATION=24h
|
||||
GAME_TOKEN_EXPIRY=86400
|
||||
|
||||
# EMAIL SERVICE CONFIGURATION
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
# JWT Refresh Token Implementation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
The JWT authentication system supports both **cookie-based** and **header-based** (Bearer token) authentication with comprehensive refresh token functionality and proper logout logic. **All authentication methods now use refresh tokens** - there is no legacy single-token mode.
|
||||
|
||||
## Features
|
||||
|
||||
- **Dual Authentication Methods**: Support for both cookie-based and Bearer token authentication
|
||||
- **Universal Refresh Tokens**: All logins receive both access and refresh tokens
|
||||
- **Automatic Token Refresh**: Tokens are refreshed when 75% of their lifetime has passed
|
||||
- **Logout Functionality**: Proper token blacklisting and cleanup
|
||||
- **Security**: Short-lived access tokens (30 minutes) and longer-lived refresh tokens (7 days)
|
||||
|
||||
## Authentication Methods
|
||||
|
||||
### 1. Cookie-Based Authentication
|
||||
- Access token stored in `auth_token` cookie
|
||||
- Refresh token stored in `refresh_token` cookie
|
||||
- Suitable for web applications with same-origin requests
|
||||
- Tokens also returned in response body
|
||||
|
||||
### 2. Bearer Token Authentication
|
||||
- Access token sent in `Authorization: Bearer <token>` header
|
||||
- Refresh token sent in `X-Refresh-Token` header
|
||||
- Suitable for mobile apps, SPAs, and API integrations
|
||||
- Tokens returned in response body
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Login
|
||||
```http
|
||||
POST /api/user/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "user@example.com",
|
||||
"password": "password123"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (all logins):**
|
||||
```json
|
||||
{
|
||||
"user": { ... },
|
||||
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
||||
}
|
||||
```
|
||||
|
||||
For cookie-based auth, tokens are also set as httpOnly cookies.
|
||||
|
||||
### Refresh Token
|
||||
```http
|
||||
POST /api/user/refresh-token
|
||||
```
|
||||
|
||||
**For Cookie-based auth:**
|
||||
- Refresh token is read from `refresh_token` cookie
|
||||
- New tokens are set as cookies AND returned in response body
|
||||
|
||||
**For Bearer token auth:**
|
||||
```http
|
||||
POST /api/user/refresh-token
|
||||
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Tokens refreshed successfully",
|
||||
"accessToken": "new_access_token",
|
||||
"refreshToken": "new_refresh_token"
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
```http
|
||||
POST /api/user/logout
|
||||
Authorization: Bearer <access_token>
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```env
|
||||
# JWT Configuration
|
||||
JWT_SECRET=your-secret-key-for-access-tokens
|
||||
JWT_REFRESH_SECRET=your-secret-key-for-refresh-tokens
|
||||
|
||||
# Access Token Expiry (use one of these)
|
||||
JWT_ACCESS_TOKEN_EXPIRY=1800 # Access token expiry in seconds (30 minutes)
|
||||
JWT_ACCESS_TOKEN_EXPIRATION=30m # Access token expiry (supports s, m, h, d)
|
||||
JWT_EXPIRY=1800 # Legacy: Access token expiry in seconds
|
||||
JWT_EXPIRATION=30m # Legacy: Access token expiry with duration
|
||||
|
||||
# Refresh Token Expiry (use one of these)
|
||||
JWT_REFRESH_TOKEN_EXPIRY=604800 # Refresh token expiry in seconds (7 days)
|
||||
JWT_REFRESH_TOKEN_EXPIRATION=7d # Refresh token expiry (supports s, m, h, d)
|
||||
JWT_REFRESH_EXPIRATION=7d # Legacy: Refresh token expiry with duration
|
||||
|
||||
# Cookie Names (optional)
|
||||
JWT_COOKIE_NAME=auth_token # Access token cookie name (default: auth_token)
|
||||
JWT_REFRESH_COOKIE_NAME=refresh_token # Refresh token cookie name (default: refresh_token)
|
||||
```
|
||||
|
||||
### Environment Variable Priority
|
||||
|
||||
**Access Token Expiry** (checked in order):
|
||||
1. `JWT_ACCESS_TOKEN_EXPIRY` (seconds)
|
||||
2. `JWT_ACCESS_TOKEN_EXPIRATION` (duration string)
|
||||
3. `JWT_EXPIRY` (seconds) - legacy
|
||||
4. `JWT_EXPIRATION` (duration string) - legacy
|
||||
5. Default: 1800 seconds (30 minutes)
|
||||
|
||||
**Refresh Token Expiry** (checked in order):
|
||||
1. `JWT_REFRESH_TOKEN_EXPIRY` (seconds)
|
||||
2. `JWT_REFRESH_TOKEN_EXPIRATION` (duration string)
|
||||
3. `JWT_REFRESH_EXPIRATION` (duration string) - legacy
|
||||
4. Default: 604800 seconds (7 days)
|
||||
|
||||
### Duration String Format
|
||||
Supports: `s` (seconds), `m` (minutes), `h` (hours), `d` (days)
|
||||
Examples: `30s`, `15m`, `2h`, `7d`
|
||||
|
||||
## Token Structure
|
||||
|
||||
### Access Token Payload
|
||||
```json
|
||||
{
|
||||
"userId": "user-uuid",
|
||||
"authLevel": 0,
|
||||
"userStatus": 1,
|
||||
"orgId": "org-uuid",
|
||||
"type": "access",
|
||||
"iat": 1640995200,
|
||||
"exp": 1640997000
|
||||
}
|
||||
```
|
||||
|
||||
### Refresh Token Payload
|
||||
```json
|
||||
{
|
||||
"userId": "user-uuid",
|
||||
"orgId": "org-uuid",
|
||||
"type": "refresh",
|
||||
"iat": 1640995200,
|
||||
"exp": 1641600000
|
||||
}
|
||||
```
|
||||
|
||||
## Automatic Token Refresh
|
||||
|
||||
The system automatically refreshes tokens when:
|
||||
- Token is within 25% of its expiration time (75% of lifetime has passed)
|
||||
- Valid refresh token is available
|
||||
- User makes an authenticated request
|
||||
|
||||
**✅ Automatic refresh happens on every authenticated API call** - no manual intervention needed!
|
||||
|
||||
### Response Headers
|
||||
For Bearer token authentication, refresh responses include:
|
||||
- `X-New-Access-Token`: New access token
|
||||
- `X-New-Refresh-Token`: New refresh token
|
||||
- `X-Token-Refreshed`: "true" indicator
|
||||
|
||||
### Manual Refresh (Optional)
|
||||
|
||||
While automatic refresh handles most scenarios, manual refresh is available for:
|
||||
- **Proactive refresh**: Before critical operations
|
||||
- **Background apps**: Long-running applications that need fresh tokens
|
||||
- **Offline recovery**: When app reconnects after being offline
|
||||
|
||||
```http
|
||||
POST /api/user/refresh-token
|
||||
X-Refresh-Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## Client Implementation Examples
|
||||
|
||||
### JavaScript/TypeScript (Fetch API)
|
||||
|
||||
```typescript
|
||||
class ApiClient {
|
||||
private accessToken: string = '';
|
||||
private refreshToken: string = '';
|
||||
|
||||
async login(username: string, password: string) {
|
||||
const response = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
this.accessToken = data.token;
|
||||
this.refreshToken = data.refreshToken; // Always present now
|
||||
return data;
|
||||
}
|
||||
|
||||
async makeAuthenticatedRequest(url: string, options: RequestInit = {}) {
|
||||
const headers = {
|
||||
'Authorization': `Bearer ${this.accessToken}`,
|
||||
...options.headers
|
||||
};
|
||||
|
||||
let response = await fetch(url, { ...options, headers });
|
||||
|
||||
// Automatically handle token refresh (tokens updated in response headers)
|
||||
if (response.headers.get('X-Token-Refreshed') === 'true') {
|
||||
const newAccessToken = response.headers.get('X-New-Access-Token');
|
||||
const newRefreshToken = response.headers.get('X-New-Refresh-Token');
|
||||
|
||||
if (newAccessToken) this.accessToken = newAccessToken;
|
||||
if (newRefreshToken) this.refreshToken = newRefreshToken;
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
// Optional: Manual refresh (usually not needed due to automatic refresh)
|
||||
async refreshTokens() {
|
||||
const response = await fetch('/api/user/refresh-token', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Refresh-Token': this.refreshToken
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
this.accessToken = data.accessToken;
|
||||
this.refreshToken = data.refreshToken;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async logout() {
|
||||
await fetch('/api/user/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${this.accessToken}` }
|
||||
});
|
||||
|
||||
this.accessToken = '';
|
||||
this.refreshToken = '';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### React Hook Example
|
||||
|
||||
```typescript
|
||||
import { useState, useCallback } from 'react';
|
||||
|
||||
export const useAuth = () => {
|
||||
const [accessToken, setAccessToken] = useState<string>('');
|
||||
const [refreshToken, setRefreshToken] = useState<string>('');
|
||||
|
||||
const login = useCallback(async (username: string, password: string) => {
|
||||
const response = await fetch('/api/user/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
setAccessToken(data.token);
|
||||
setRefreshToken(data.refreshToken); // Always present
|
||||
return data;
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(async () => {
|
||||
if (accessToken) {
|
||||
await fetch('/api/user/logout', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': `Bearer ${accessToken}` }
|
||||
});
|
||||
}
|
||||
setAccessToken('');
|
||||
setRefreshToken('');
|
||||
}, [accessToken]);
|
||||
|
||||
return { accessToken, refreshToken, login, logout };
|
||||
};
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Token Blacklisting**: Logout tokens are blacklisted in Redis with TTL matching token expiration
|
||||
2. **Short-lived Access Tokens**: 30-minute expiry reduces exposure window
|
||||
3. **Secure Cookies**: httpOnly, secure, sameSite attributes for cookie-based auth
|
||||
4. **Token Rotation**: Refresh tokens are rotated on each refresh
|
||||
5. **Environment-specific Secrets**: Different secrets for access and refresh tokens
|
||||
|
||||
## Migration Guide
|
||||
|
||||
### From Single Token to Refresh Token System
|
||||
|
||||
Since this is a new implementation, all clients should expect:
|
||||
|
||||
1. **Login Response**: Always includes both `token` (access) and `refreshToken`
|
||||
2. **Token Storage**: Store both tokens securely
|
||||
3. **API Requests**: Use access token in Authorization header
|
||||
4. **Automatic Refresh**: Tokens refresh automatically - just watch for response headers
|
||||
5. **Logout**: Call logout endpoint to invalidate tokens
|
||||
|
||||
**Key Point**: Manual refresh is optional since automatic refresh handles token renewal seamlessly.
|
||||
|
||||
**No backward compatibility needed** - this is the only authentication method.
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Login and get tokens
|
||||
curl -X POST http://localhost:3000/api/user/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "test@example.com", "password": "password"}'
|
||||
|
||||
# Use access token
|
||||
curl -X GET http://localhost:3000/api/user/profile \
|
||||
-H "Authorization: Bearer <access_token>"
|
||||
|
||||
# Refresh tokens
|
||||
curl -X POST http://localhost:3000/api/user/refresh-token \
|
||||
-H "X-Refresh-Token: <refresh_token>"
|
||||
|
||||
# Logout
|
||||
curl -X POST http://localhost:3000/api/user/logout \
|
||||
-H "Authorization: Bearer <access_token>"
|
||||
```
|
||||
@@ -10,12 +10,13 @@ import chatRouter from './routers/chatRouter';
|
||||
import contactRouter from './routers/contactRouter';
|
||||
import adminRouter from './routers/adminRouter';
|
||||
import deckImportExportRouter from './routers/deckImportExportRouter';
|
||||
<<<<<<< HEAD
|
||||
import gameRouter from './routers/gameRouter';
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
import { LoggingService, logStartup, logConnection, logError, logRequest } from '../Application/Services/Logger';
|
||||
import { WebSocketService } from '../Application/Services/WebSocketService';
|
||||
import { GameWebSocketService } from '../Application/Services/GameWebSocketService';
|
||||
import { GameRepository } from '../Infrastructure/Repository/GameRepository';
|
||||
import { UserRepository } from '../Infrastructure/Repository/UserRepository';
|
||||
import { RedisService } from '../Application/Services/RedisService';
|
||||
import { setupSwagger } from './swagger/swaggerUiSetup';
|
||||
|
||||
const app = express();
|
||||
@@ -135,10 +136,7 @@ app.use('/api/chats', chatRouter);
|
||||
app.use('/api/contacts', contactRouter);
|
||||
app.use('/api/admin', adminRouter);
|
||||
app.use('/api/deck-import-export', deckImportExportRouter);
|
||||
<<<<<<< HEAD
|
||||
app.use('/api/games', gameRouter);
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
|
||||
// Global error handler (must be after routes)
|
||||
app.use(loggingService.errorLoggingMiddleware());
|
||||
@@ -167,6 +165,7 @@ app.use((req: express.Request, res: express.Response) => {
|
||||
|
||||
// Initialize WebSocket service after database connection
|
||||
let webSocketService: WebSocketService;
|
||||
let gameWebSocketService: GameWebSocketService;
|
||||
|
||||
// Initialize database connection
|
||||
AppDataSource.initialize()
|
||||
@@ -183,6 +182,19 @@ AppDataSource.initialize()
|
||||
logStartup('WebSocket service initialized', {
|
||||
chatInactivityTimeout: process.env.CHAT_INACTIVITY_TIMEOUT_MINUTES || '30'
|
||||
});
|
||||
|
||||
// Initialize Game WebSocket service for /game namespace
|
||||
const gameRepository = new GameRepository();
|
||||
const userRepository = new UserRepository();
|
||||
const redisService = RedisService.getInstance();
|
||||
|
||||
gameWebSocketService = new GameWebSocketService(
|
||||
webSocketService['io'], // Access the io property directly
|
||||
gameRepository,
|
||||
userRepository,
|
||||
redisService
|
||||
);
|
||||
logStartup('Game WebSocket service initialized for /game namespace');
|
||||
})
|
||||
.catch((error) => {
|
||||
const dbOptions = AppDataSource.options as any;
|
||||
@@ -254,5 +266,5 @@ process.on('unhandledRejection', (reason, promise) => {
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Export WebSocket service for game integration
|
||||
export { webSocketService };
|
||||
// Export WebSocket services for game integration
|
||||
export { webSocketService, gameWebSocketService };
|
||||
|
||||
@@ -107,41 +107,6 @@ router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Res
|
||||
}
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
// Get users by page (admin only) - RECOMMENDED
|
||||
router.get('/users/page/:from/:to', adminRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
const from = parseInt(req.params.from);
|
||||
const to = parseInt(req.params.to);
|
||||
const includeDeleted = req.query.includeDeleted === 'true';
|
||||
|
||||
if (isNaN(from) || isNaN(to) || from < 0 || to < from) {
|
||||
return res.status(400).json({ error: 'Invalid page parameters. "from" and "to" must be valid numbers with to >= from >= 0' });
|
||||
}
|
||||
|
||||
logRequest('Admin get users by page endpoint accessed', req, res, { from, to, includeDeleted });
|
||||
|
||||
const result = includeDeleted
|
||||
? await container.userRepository.findByPageIncludingDeleted(from, to)
|
||||
: await container.userRepository.findByPage(from, to);
|
||||
|
||||
logRequest('Admin users page retrieved successfully', req, res, {
|
||||
from,
|
||||
to,
|
||||
count: result.users.length,
|
||||
total: result.totalCount,
|
||||
includeDeleted
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Admin get users by page endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
|
||||
>>>>>>> origin/main
|
||||
// Get user by ID including soft-deleted ones
|
||||
router.get('/users/:userId',
|
||||
adminRequired,
|
||||
@@ -176,7 +141,6 @@ router.get('/users/:userId',
|
||||
});
|
||||
|
||||
// Search users including soft-deleted ones
|
||||
<<<<<<< HEAD
|
||||
// router.get('/users/search/:searchTerm',
|
||||
// adminRequired,
|
||||
// ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
|
||||
@@ -203,34 +167,6 @@ router.get('/users/:userId',
|
||||
// res.status(500).json({ error: 'Internal server error' });
|
||||
// }
|
||||
// });
|
||||
=======
|
||||
router.get('/users/search/:searchTerm',
|
||||
adminRequired,
|
||||
ValidationMiddleware.validateStringLength({ searchTerm: { min: 2, max: 100 } }),
|
||||
async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { searchTerm } = req.params;
|
||||
const includeDeleted = req.query.includeDeleted === 'true';
|
||||
|
||||
logRequest('Admin search users endpoint accessed', req, res, { searchTerm, includeDeleted });
|
||||
|
||||
const users = includeDeleted
|
||||
? await container.userRepository.searchIncludingDeleted(searchTerm)
|
||||
: await container.userRepository.search(searchTerm);
|
||||
|
||||
logRequest('Admin user search completed', req, res, {
|
||||
searchTerm,
|
||||
resultCount: Array.isArray(users) ? users.length : (users.totalCount || 0),
|
||||
includeDeleted
|
||||
});
|
||||
|
||||
res.json(users);
|
||||
} catch (error) {
|
||||
logError('Admin search users endpoint error', error as Error, req, res);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
});
|
||||
>>>>>>> origin/main
|
||||
|
||||
// Update any user (admin only)
|
||||
router.patch('/users/:userId',
|
||||
@@ -422,7 +358,6 @@ router.get('/decks/search/:searchTerm', adminRequired, async (req: Request, res:
|
||||
}
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
//modify deck (admin only)
|
||||
router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -447,8 +382,6 @@ router.patch('/decks/:id', adminRequired, async (req: Request, res: Response) =>
|
||||
}
|
||||
});
|
||||
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
// Hard delete deck (admin only)
|
||||
router.delete('/decks/:id/hard', adminRequired, async (req: Request, res: Response) => {
|
||||
try {
|
||||
|
||||
@@ -5,9 +5,53 @@ import { ErrorResponseService } from '../../Application/Services/ErrorResponseSe
|
||||
import { ValidationMiddleware } from '../../Application/Services/ValidationMiddleware';
|
||||
import { GeneralSearchService } from '../../Application/Search/Generalsearch';
|
||||
import { logRequest, logError, logWarning } from '../../Application/Services/Logger';
|
||||
import { Type, CType } from '../../Domain/Deck/DeckAggregate';
|
||||
|
||||
const deckRouter = Router();
|
||||
|
||||
/**
|
||||
* Helper function to convert string enum values to integer enum values
|
||||
*/
|
||||
function convertEnumValues(data: any): any {
|
||||
const converted = { ...data };
|
||||
|
||||
// Convert Type enum
|
||||
if (converted.type && typeof converted.type === 'string') {
|
||||
switch (converted.type.toUpperCase()) {
|
||||
case 'LUCK':
|
||||
converted.type = Type.LUCK;
|
||||
break;
|
||||
case 'JOKER':
|
||||
converted.type = Type.JOKER;
|
||||
break;
|
||||
case 'QUESTION':
|
||||
converted.type = Type.QUESTION;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid deck type. Must be LUCK, JOKER, or QUESTION');
|
||||
}
|
||||
}
|
||||
|
||||
// Convert CType enum
|
||||
if (converted.ctype && typeof converted.ctype === 'string') {
|
||||
switch (converted.ctype.toUpperCase()) {
|
||||
case 'PUBLIC':
|
||||
converted.ctype = CType.PUBLIC;
|
||||
break;
|
||||
case 'PRIVATE':
|
||||
converted.ctype = CType.PRIVATE;
|
||||
break;
|
||||
case 'ORGANIZATION':
|
||||
converted.ctype = CType.ORGANIZATION;
|
||||
break;
|
||||
default:
|
||||
throw new Error('Invalid deck ctype. Must be PUBLIC, PRIVATE, or ORGANIZATION');
|
||||
}
|
||||
}
|
||||
|
||||
return converted;
|
||||
}
|
||||
|
||||
// Create search service that isn't in the container yet
|
||||
const searchService = new GeneralSearchService(container.userRepository, container.organizationRepository, container.deckRepository);
|
||||
|
||||
@@ -60,18 +104,25 @@ deckRouter.post('/', authRequired, async (req, res) => {
|
||||
try {
|
||||
const userId = (req as any).user.userId;
|
||||
logRequest('Create deck endpoint accessed', req, res, { name: req.body.name, userId });
|
||||
<<<<<<< HEAD
|
||||
req.body.userid = userId; // Set userId in request body
|
||||
=======
|
||||
|
||||
>>>>>>> origin/main
|
||||
const result = await container.createDeckCommandHandler.execute(req.body);
|
||||
// Convert string enum values to integers
|
||||
const command = convertEnumValues({
|
||||
...req.body,
|
||||
userid: userId
|
||||
});
|
||||
|
||||
const result = await container.createDeckCommandHandler.execute(command);
|
||||
|
||||
logRequest('Deck created successfully', req, res, { deckId: result.id, name: req.body.name, userId });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Create deck endpoint error', error as Error, req, res);
|
||||
|
||||
// Handle enum validation errors
|
||||
if (error instanceof Error && error.message.includes('Invalid deck')) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
if (error instanceof Error && (error.message.includes('duplicate') || error.message.includes('unique constraint'))) {
|
||||
return res.status(409).json({ error: 'Deck with this name already exists' });
|
||||
}
|
||||
@@ -144,23 +195,27 @@ deckRouter.get('/:id', authRequired, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
deckRouter.patch('/:id', authRequired, async (req, res) => {
|
||||
=======
|
||||
deckRouter.put('/:id', authRequired, async (req, res) => {
|
||||
>>>>>>> origin/main
|
||||
try {
|
||||
const deckId = req.params.id;
|
||||
const userId = (req as any).user.userId;
|
||||
logRequest('Update deck endpoint accessed', req, res, { deckId, userId, updateFields: Object.keys(req.body) });
|
||||
|
||||
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...req.body });
|
||||
// Convert string enum values to integers
|
||||
const updateData = convertEnumValues(req.body);
|
||||
|
||||
const result = await container.updateDeckCommandHandler.execute({ id: deckId, ...updateData });
|
||||
|
||||
logRequest('Deck updated successfully', req, res, { deckId, userId });
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError('Update deck endpoint error', error as Error, req, res);
|
||||
|
||||
// Handle enum validation errors
|
||||
if (error instanceof Error && error.message.includes('Invalid deck')) {
|
||||
return res.status(400).json({ error: error.message });
|
||||
}
|
||||
|
||||
if (error instanceof Error && error.message.includes('not found')) {
|
||||
return res.status(404).json({ error: 'Deck not found' });
|
||||
}
|
||||
@@ -172,13 +227,10 @@ deckRouter.put('/:id', authRequired, async (req, res) => {
|
||||
if (error instanceof Error && error.message.includes('validation')) {
|
||||
return res.status(400).json({ error: 'Invalid input data', details: error.message });
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
|
||||
if (error instanceof Error && error.message.includes('admin')) {
|
||||
return res.status(403).json({ error: 'Forbidden: ' + error.message });
|
||||
}
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
|
||||
@@ -206,7 +206,26 @@ gameRouter.post('/join', optionalAuth, async (req, res) => {
|
||||
playerName: actualPlayerName
|
||||
});
|
||||
|
||||
res.json(game);
|
||||
// Create game token for WebSocket authentication
|
||||
const gameTokenService = container.gameTokenService;
|
||||
const gameToken = gameTokenService.createGameToken(
|
||||
game.id,
|
||||
game.gamecode,
|
||||
actualPlayerName || 'Anonymous',
|
||||
actualPlayerId
|
||||
);
|
||||
|
||||
// Return clean response with essential data + game token
|
||||
res.json({
|
||||
id: game.id,
|
||||
gamecode: game.gamecode,
|
||||
playerName: actualPlayerName,
|
||||
playerCount: game.players.length,
|
||||
maxPlayers: game.maxplayers,
|
||||
gameType: LoginType[gameToJoin.logintype],
|
||||
isAuthenticated: !!actualPlayerId,
|
||||
gameToken: gameToken
|
||||
});
|
||||
} catch (error) {
|
||||
logError('Join game endpoint error', error as Error, req, res);
|
||||
|
||||
|
||||
@@ -32,11 +32,7 @@ userRouter.post('/login',
|
||||
logAuth('User login successful', result.user.id, { username: result.user.username }, req, res);
|
||||
res.json(result);
|
||||
} else {
|
||||
<<<<<<< HEAD
|
||||
throw new Error(`Login failed: ${result}`);
|
||||
=======
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Invalid username or password');
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -52,12 +48,9 @@ userRouter.post('/login',
|
||||
if (error.message.includes('not verified')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
if (error.message.includes('restriction')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Please verify your email address');
|
||||
}
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
if (error.message.includes('deactivated')) {
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Account has been deactivated');
|
||||
}
|
||||
@@ -94,12 +87,8 @@ userRouter.post('/create',
|
||||
res.status(201).json(result);
|
||||
|
||||
} catch (error) {
|
||||
<<<<<<< HEAD
|
||||
// Don't log here since CreateUserCommandHandler already logs system errors
|
||||
// Only log validation/user input errors at router level
|
||||
=======
|
||||
logError('Create user endpoint error', error as Error, req, res);
|
||||
>>>>>>> origin/main
|
||||
|
||||
if (error instanceof Error) {
|
||||
if (error.message.includes('already exists')) {
|
||||
@@ -108,13 +97,10 @@ userRouter.post('/create',
|
||||
if (error.message.includes('validation')) {
|
||||
return ErrorResponseService.sendBadRequest(res, error.message);
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
// Log unexpected errors that weren't handled by the command handler
|
||||
if (!error.message.includes('Failed to create user')) {
|
||||
logError('Unexpected create user endpoint error', error as Error, req, res);
|
||||
}
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
@@ -187,7 +173,6 @@ userRouter.patch('/profile', authRequired, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
<<<<<<< HEAD
|
||||
//Soft delete user (current user)
|
||||
userRouter.delete('/profile', authRequired, async (req, res) => {
|
||||
try {
|
||||
@@ -214,6 +199,32 @@ userRouter.post('/logout', authRequired, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Refresh token endpoint
|
||||
userRouter.post('/refresh-token', async (req, res) => {
|
||||
try {
|
||||
logRequest('Token refresh endpoint accessed', req, res);
|
||||
|
||||
const jwtService = container.jwtService;
|
||||
const newTokenPair = jwtService.attemptTokenRefresh(req, res);
|
||||
|
||||
if (newTokenPair) {
|
||||
logRequest('Token refresh successful', req, res);
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Tokens refreshed successfully',
|
||||
accessToken: newTokenPair.accessToken,
|
||||
refreshToken: newTokenPair.refreshToken
|
||||
});
|
||||
} else {
|
||||
logWarning('Token refresh failed - invalid or missing refresh token', undefined, req, res);
|
||||
return ErrorResponseService.sendUnauthorized(res, 'Invalid or expired refresh token');
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Refresh token endpoint error', error as Error, req, res);
|
||||
return ErrorResponseService.sendInternalServerError(res);
|
||||
}
|
||||
});
|
||||
|
||||
// Email verification endpoint
|
||||
userRouter.get('/verify-email/:token', async (req, res) => {
|
||||
try {
|
||||
@@ -325,6 +336,4 @@ userRouter.post('/reset-password',
|
||||
}
|
||||
});
|
||||
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
export default userRouter;
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import swaggerJSDoc from 'swagger-jsdoc';
|
||||
<<<<<<< HEAD
|
||||
import path from 'path';
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
|
||||
export const swaggerOptions = {
|
||||
definition: {
|
||||
@@ -22,17 +19,12 @@ export const swaggerOptions = {
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
<<<<<<< HEAD
|
||||
url: 'http://localhost:3001',
|
||||
description: 'Local development server'
|
||||
},
|
||||
{
|
||||
url: 'http://localhost:3000',
|
||||
description: 'Local development server (alt)'
|
||||
=======
|
||||
url: 'http://localhost:3000',
|
||||
description: 'Local development server'
|
||||
>>>>>>> origin/main
|
||||
},
|
||||
{
|
||||
url: 'https://api.serpentrace.com',
|
||||
@@ -74,7 +66,6 @@ export const swaggerOptions = {
|
||||
{
|
||||
name: 'Deck Import/Export',
|
||||
description: 'Import and export deck functionality'
|
||||
<<<<<<< HEAD
|
||||
},
|
||||
{
|
||||
name: 'Games',
|
||||
@@ -99,17 +90,11 @@ export const swaggerOptions = {
|
||||
{
|
||||
name: 'Admin - Contacts',
|
||||
description: 'Admin contact management operations'
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
]
|
||||
},
|
||||
apis: [
|
||||
<<<<<<< HEAD
|
||||
'./src/Api/swagger/swaggerDefinitionsFixed.ts'
|
||||
=======
|
||||
'./src/Api/swagger/swaggerDefinitions.ts'
|
||||
>>>>>>> origin/main
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -21,10 +21,6 @@ export class UserMapper {
|
||||
fname: user.fname,
|
||||
lname: user.lname,
|
||||
code: user.token,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
type: user.type,
|
||||
>>>>>>> origin/main
|
||||
phone: user.phone,
|
||||
state: user.state,
|
||||
};
|
||||
|
||||
@@ -24,10 +24,6 @@ export interface DetailUserDto {
|
||||
fname: string;
|
||||
lname: string;
|
||||
code: string | null;
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
type: string;
|
||||
>>>>>>> origin/main
|
||||
phone: string | null;
|
||||
state: number;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export interface UpdateDeckCommand {
|
||||
id: string;
|
||||
<<<<<<< HEAD
|
||||
userstate?: number;
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
name?: string;
|
||||
type?: number;
|
||||
userid?: string;
|
||||
|
||||
@@ -2,17 +2,13 @@ import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
|
||||
import { UpdateDeckCommand } from './UpdateDeckCommand';
|
||||
import { ShortDeckDto } from '../../DTOs/DeckDto';
|
||||
import { DeckMapper } from '../../DTOs/Mappers/DeckMapper';
|
||||
<<<<<<< HEAD
|
||||
import { DeckAggregate } from '../../../Domain/Deck/DeckAggregate';
|
||||
import { logError } from '../../Services/Logger';
|
||||
=======
|
||||
>>>>>>> origin/main
|
||||
|
||||
export class UpdateDeckCommandHandler {
|
||||
constructor(private readonly deckRepo: IDeckRepository) {}
|
||||
|
||||
async execute(cmd: UpdateDeckCommand): Promise<ShortDeckDto | null> {
|
||||
<<<<<<< HEAD
|
||||
if(cmd.state !== undefined && cmd.userstate!==1) {
|
||||
throw new Error('Only admin users can change deck state');
|
||||
}
|
||||
@@ -50,10 +46,5 @@ export class UpdateDeckCommandHandler {
|
||||
logError(`Error updating deck: ${cmd.id}`, error);
|
||||
throw error;
|
||||
}
|
||||
=======
|
||||
const updated = await this.deckRepo.update(cmd.id, { ...cmd });
|
||||
if (!updated) return null;
|
||||
return DeckMapper.toShortDto(updated);
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,14 @@
|
||||
import { IDeckRepository } from '../../../Domain/IRepository/IDeckRepository';
|
||||
import { GetDeckByIdQuery } from './GetDeckByIdQuery';
|
||||
<<<<<<< HEAD
|
||||
import { DetailDeckDto } from '../../DTOs/DeckDto';
|
||||
=======
|
||||
import { ShortDeckDto } from '../../DTOs/DeckDto';
|
||||
>>>>>>> origin/main
|
||||
import { DeckMapper } from '../../DTOs/Mappers/DeckMapper';
|
||||
|
||||
export class GetDeckByIdQueryHandler {
|
||||
constructor(private readonly deckRepo: IDeckRepository) {}
|
||||
|
||||
<<<<<<< HEAD
|
||||
async execute(query: GetDeckByIdQuery): Promise<DetailDeckDto | null> {
|
||||
const deck = await this.deckRepo.findById(query.id);
|
||||
if (!deck) return null;
|
||||
return DeckMapper.toDetailDto(deck);
|
||||
=======
|
||||
async execute(query: GetDeckByIdQuery): Promise<ShortDeckDto | null> {
|
||||
const deck = await this.deckRepo.findById(query.id);
|
||||
if (!deck) return null;
|
||||
return DeckMapper.toShortDto(deck);
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
}
|
||||
|
||||
@@ -151,6 +151,15 @@ export class JoinGameCommandHandler {
|
||||
isOnline: true
|
||||
};
|
||||
|
||||
// Check if player name is already in use by a different player
|
||||
const existingPlayerWithName = gameData.currentPlayers.find(
|
||||
p => p.playerName === command.playerName && p.playerId !== command.playerId
|
||||
);
|
||||
|
||||
if (existingPlayerWithName) {
|
||||
throw new Error(`Player name "${command.playerName}" is already in use in this game`);
|
||||
}
|
||||
|
||||
// Update players list (remove if exists, then add)
|
||||
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== command.playerId);
|
||||
gameData.currentPlayers.push(newPlayer);
|
||||
@@ -161,9 +170,6 @@ export class JoinGameCommandHandler {
|
||||
// Store updated data in Redis with TTL (24 hours)
|
||||
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
|
||||
|
||||
// Add player to active players set
|
||||
await this.redisService.setAdd(`active_players:${game.id}`, command.playerId);
|
||||
|
||||
logOther('Game data updated in Redis', {
|
||||
gameId: game.id,
|
||||
gameCode: game.gamecode,
|
||||
@@ -204,7 +210,6 @@ export class JoinGameCommandHandler {
|
||||
gameData.currentPlayers = gameData.currentPlayers.filter(p => p.playerId !== playerId);
|
||||
|
||||
await this.redisService.setWithExpiry(redisKey, JSON.stringify(gameData), 24 * 60 * 60);
|
||||
await this.redisService.setRemove(`active_players:${gameId}`, playerId);
|
||||
}
|
||||
} catch (error) {
|
||||
logError('Failed to remove player from Redis', error instanceof Error ? error : new Error(String(error)));
|
||||
|
||||
@@ -64,7 +64,7 @@ export class StartGameCommandHandler {
|
||||
gamecode,
|
||||
maxplayers: command.maxplayers,
|
||||
logintype: command.logintype,
|
||||
createdby: command.userid || null,
|
||||
createdby: command.userid!,
|
||||
orgid: command.orgid || null,
|
||||
gamedecks,
|
||||
players: [],
|
||||
|
||||
@@ -49,7 +49,6 @@ export class GeneralSearchService implements IGeneralSearchService {
|
||||
};
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
// Ensure limit is at least 1 to prevent database issues
|
||||
const effectiveLimit = Math.max(limit || 20, 1);
|
||||
const effectiveOffset = Math.max(offset || 0, 0);
|
||||
@@ -58,12 +57,6 @@ export class GeneralSearchService implements IGeneralSearchService {
|
||||
const { users, totalCount } = await this.userRepo.search(query.trim(), effectiveLimit, effectiveOffset);
|
||||
const results = users.map(user => UserMapper.toShortDto(user));
|
||||
const hasMore = (effectiveOffset + effectiveLimit) < totalCount;
|
||||
=======
|
||||
try {
|
||||
const { users, totalCount } = await this.userRepo.search(query.trim(), limit, offset);
|
||||
const results = users.map(user => UserMapper.toShortDto(user));
|
||||
const hasMore = (offset + limit) < totalCount;
|
||||
>>>>>>> origin/main
|
||||
|
||||
return {
|
||||
results,
|
||||
@@ -116,7 +109,6 @@ export class GeneralSearchService implements IGeneralSearchService {
|
||||
};
|
||||
}
|
||||
|
||||
<<<<<<< HEAD
|
||||
// Ensure limit is at least 1 to prevent database issues
|
||||
const effectiveLimit = Math.max(limit || 20, 1);
|
||||
const effectiveOffset = Math.max(offset || 0, 0);
|
||||
@@ -136,19 +128,6 @@ export class GeneralSearchService implements IGeneralSearchService {
|
||||
} catch (error) {
|
||||
throw new Error('Failed to search decks');
|
||||
}
|
||||
=======
|
||||
const { decks, totalCount } = await this.deckRepo.search(query.trim(), limit, offset);
|
||||
const results = decks.map(deck => DeckMapper.toShortDto(deck));
|
||||
const hasMore = (offset + limit) < totalCount;
|
||||
|
||||
return {
|
||||
results,
|
||||
totalCount,
|
||||
hasMore,
|
||||
searchQuery: query,
|
||||
searchType: 'decks'
|
||||
};
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
|
||||
async searchByType(
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { JWTService } from './JWTService';
|
||||
<<<<<<< HEAD
|
||||
import { RedisService } from './RedisService';
|
||||
import { logAuth, logWarning } from './Logger';
|
||||
|
||||
@@ -80,7 +79,7 @@ export async function authRequired(req: Request, res: Response, next: NextFuncti
|
||||
orgId: payload.orgId
|
||||
}, req);
|
||||
|
||||
const refreshed = jwtService.refreshIfNeeded(payload, res);
|
||||
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
|
||||
if (refreshed) {
|
||||
logAuth('Token refreshed', payload.userId, undefined, req);
|
||||
}
|
||||
@@ -133,7 +132,7 @@ export async function adminRequired(req: Request, res: Response, next: NextFunct
|
||||
orgId: payload.orgId
|
||||
}, req);
|
||||
|
||||
const refreshed = jwtService.refreshIfNeeded(payload, res);
|
||||
const refreshed = jwtService.refreshIfNeeded(payload, res, req);
|
||||
if (refreshed) {
|
||||
logAuth('Admin token refreshed', payload.userId, undefined, req);
|
||||
}
|
||||
@@ -144,60 +143,4 @@ export async function adminRequired(req: Request, res: Response, next: NextFunct
|
||||
logWarning('Admin authentication middleware error', { error: (error as Error).message }, req);
|
||||
return res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
=======
|
||||
import { logAuth, logWarning } from './Logger';
|
||||
|
||||
export const jwtService = new JWTService();
|
||||
|
||||
export function authRequired(req: Request, res: Response, next: NextFunction) {
|
||||
const payload = jwtService.verify(req);
|
||||
if (!payload) {
|
||||
logAuth('Authentication failed - No valid token', undefined, {
|
||||
ip: req.ip,
|
||||
userAgent: req.get ? req.get('User-Agent') : 'unknown',
|
||||
path: req.path
|
||||
}, req);
|
||||
return res.status(401).json({ error: 'Unauthorized' });
|
||||
}
|
||||
|
||||
logAuth('Authentication successful', payload.userId, {
|
||||
authLevel: payload.authLevel,
|
||||
orgId: payload.orgId
|
||||
}, req);
|
||||
|
||||
const refreshed = jwtService.refreshIfNeeded(payload, res);
|
||||
if (refreshed) {
|
||||
logAuth('Token refreshed', payload.userId, undefined, req);
|
||||
}
|
||||
|
||||
(req as any).user = payload;
|
||||
next();
|
||||
}
|
||||
|
||||
export function adminRequired(req: Request, res: Response, next: NextFunction) {
|
||||
const payload = jwtService.verify(req);
|
||||
if (!payload || payload.authLevel !== 1) {
|
||||
logWarning('Admin access denied', {
|
||||
hasPayload: !!payload,
|
||||
authLevel: payload?.authLevel,
|
||||
userId: payload?.userId,
|
||||
ip: req.ip,
|
||||
path: req.path
|
||||
}, req);
|
||||
return res.status(403).json({ error: 'Forbidden' });
|
||||
}
|
||||
|
||||
logAuth('Admin authentication successful', payload.userId, {
|
||||
authLevel: payload.authLevel,
|
||||
orgId: payload.orgId
|
||||
}, req);
|
||||
|
||||
const refreshed = jwtService.refreshIfNeeded(payload, res);
|
||||
if (refreshed) {
|
||||
logAuth('Admin token refreshed', payload.userId, undefined, req);
|
||||
}
|
||||
|
||||
(req as any).user = payload;
|
||||
next();
|
||||
>>>>>>> origin/main
|
||||
}
|
||||
@@ -25,7 +25,7 @@ export class EmailTemplateHelper {
|
||||
}
|
||||
|
||||
public static replaceTemplatePlaceholders(template: string, data: TemplateData): string {
|
||||
return template.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
||||
return template.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, key) => {
|
||||
return data[key] !== undefined ? String(data[key]) : match;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ import {
|
||||
GameActionData,
|
||||
PlayerPosition,
|
||||
GameStateUpdateData,
|
||||
FieldEffectRequest
|
||||
FieldEffectRequest,
|
||||
JoinGameData,
|
||||
LeaveGameData
|
||||
} from './Interfaces/GameInterfaces';
|
||||
import { json } from 'stream/consumers';
|
||||
|
||||
interface AuthenticatedSocket extends Socket {
|
||||
userId?: string;
|
||||
@@ -23,14 +26,6 @@ interface AuthenticatedSocket extends Socket {
|
||||
isAuthenticated?: boolean;
|
||||
}
|
||||
|
||||
interface JoinGameData {
|
||||
gameToken: string; // Required game session token
|
||||
}
|
||||
|
||||
interface LeaveGameData {
|
||||
gameCode: string;
|
||||
}
|
||||
|
||||
interface DiceRollData {
|
||||
gameCode: string;
|
||||
diceValue: number; // Value from frontend (1-6)
|
||||
@@ -91,7 +86,7 @@ export class GameWebSocketService {
|
||||
|
||||
private setupGameEventHandlers(socket: AuthenticatedSocket): void {
|
||||
// Join game room
|
||||
socket.on('game:join', async (data: JoinGameData) => {
|
||||
socket.on('game:join', async (data: any) => {
|
||||
await this.handleJoinGame(socket, data);
|
||||
});
|
||||
|
||||
@@ -141,11 +136,14 @@ export class GameWebSocketService {
|
||||
});
|
||||
}
|
||||
|
||||
private async handleJoinGame(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> {
|
||||
private async handleJoinGame(socket: AuthenticatedSocket, data: any): Promise<void> {
|
||||
try {
|
||||
const { gameToken } = data;
|
||||
// Simple data extraction - let Socket.IO handle the parsing
|
||||
const jsdata = JSON.parse(data);
|
||||
const gameToken = jsdata?.gameToken;
|
||||
|
||||
if (!gameToken) {
|
||||
logError('Game join failed: No game token provided');
|
||||
socket.emit('game:error', { message: 'Game token is required' });
|
||||
return;
|
||||
}
|
||||
@@ -153,6 +151,7 @@ export class GameWebSocketService {
|
||||
// Verify the game token
|
||||
const gameTokenPayload = this.gameTokenService.verifyGameToken(gameToken);
|
||||
if (!gameTokenPayload) {
|
||||
logError('Game join failed: Invalid game token');
|
||||
socket.emit('game:error', { message: 'Invalid or expired game token' });
|
||||
return;
|
||||
}
|
||||
@@ -162,10 +161,19 @@ export class GameWebSocketService {
|
||||
// Validate game still exists
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
if (!game || game.id !== gameId) {
|
||||
logError(`Game join failed: Game not found - Code: ${gameCode}`);
|
||||
socket.emit('game:error', { message: 'Game not found or token invalid' });
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if player name is already in use by checking connected players
|
||||
const connectedPlayers = await this.getConnectedPlayers(gameCode);
|
||||
if (connectedPlayers.includes(playerName)) {
|
||||
logOther(`Game join failed: Player name "${playerName}" already in use in game ${gameCode}`);
|
||||
socket.emit('game:error', { message: `Player name "${playerName}" is already in use in this game` });
|
||||
return;
|
||||
}
|
||||
|
||||
// Set socket properties from game token
|
||||
socket.gameCode = gameCode;
|
||||
socket.playerName = playerName;
|
||||
@@ -185,8 +193,6 @@ export class GameWebSocketService {
|
||||
// Add to pending players list and notify gamemaster
|
||||
await this.addToPendingPlayers(gameCode, playerName);
|
||||
|
||||
logOther(`Player ${playerName} requesting approval to join private game: ${gameRoomName}`);
|
||||
|
||||
// Send pending status to the requesting player
|
||||
socket.emit('game:pending-approval', {
|
||||
gameCode,
|
||||
@@ -210,7 +216,6 @@ export class GameWebSocketService {
|
||||
await socket.join(gameRoomName);
|
||||
await socket.join(playerRoomName);
|
||||
|
||||
logOther(`Player ${playerName} joined game room: ${gameRoomName} (${isAuthenticated ? 'authenticated' : 'public'}) ${isGamemaster ? '[GAMEMASTER]' : ''}`);
|
||||
|
||||
// Send success response to the joining player
|
||||
socket.emit('game:joined', {
|
||||
@@ -222,6 +227,7 @@ export class GameWebSocketService {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
||||
// Notify other players in the game (broadcast)
|
||||
socket.to(gameRoomName).emit('game:player-joined', {
|
||||
playerName: playerName,
|
||||
@@ -230,40 +236,54 @@ export class GameWebSocketService {
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
|
||||
// Send current game state to the joining player
|
||||
const gameState = await this.getGameState(gameCode);
|
||||
socket.emit('game:state', gameState);
|
||||
|
||||
|
||||
// Update Redis with active player connection
|
||||
await this.updatePlayerConnection(gameCode, playerName, true);
|
||||
|
||||
} catch (error) {
|
||||
logError('Error joining game', error as Error);
|
||||
socket.emit('game:error', { message: 'Failed to join game' });
|
||||
socket.emit('game:error', {
|
||||
message: 'Failed to join game',
|
||||
error: error instanceof Error ? error.message : 'Unknown error'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLeaveGame(socket: AuthenticatedSocket, data: LeaveGameData): Promise<void> {
|
||||
try {
|
||||
const { gameCode } = data;
|
||||
const { gameCode } = JSON.parse(data as any);
|
||||
const playerName = socket.playerName;
|
||||
|
||||
// Validate we have the required data
|
||||
if (!playerName) {
|
||||
logError('Cannot leave game: socket has no playerName');
|
||||
socket.emit('game:error', { message: 'Player has no name' });
|
||||
return;
|
||||
}
|
||||
|
||||
const gameRoomName = `game_${gameCode}`;
|
||||
const playerRoomName = `game_${gameCode}:${socket.playerName}`;
|
||||
const playerRoomName = `game_${gameCode}:${playerName}`;
|
||||
|
||||
// Leave both rooms
|
||||
await socket.leave(gameRoomName);
|
||||
await socket.leave(playerRoomName);
|
||||
|
||||
logOther(`Player ${socket.playerName} left game room: ${gameRoomName}`);
|
||||
logOther(`Player ${playerName} left game room: ${gameRoomName}`);
|
||||
|
||||
// Notify other players
|
||||
socket.to(gameRoomName).emit('game:player-left', {
|
||||
playerName: socket.playerName,
|
||||
playerName: playerName,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Update Redis
|
||||
await this.updatePlayerConnection(gameCode, socket.playerName!, false);
|
||||
// Update Redis before clearing socket properties
|
||||
await this.updatePlayerConnection(gameCode, playerName, false);
|
||||
|
||||
// Clear socket properties
|
||||
socket.gameCode = undefined;
|
||||
socket.playerName = undefined;
|
||||
|
||||
@@ -275,7 +295,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleGameAction(socket: AuthenticatedSocket, data: GameActionData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, action, data: actionData } = data;
|
||||
const { gameCode, action, data: actionData } = JSON.parse(data as any);
|
||||
|
||||
if (!socket.gameCode || socket.gameCode !== gameCode) {
|
||||
socket.emit('game:error', { message: 'You must be in the game to perform actions' });
|
||||
@@ -317,7 +337,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleGameChat(socket: AuthenticatedSocket, data: GameChatData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, message } = data;
|
||||
const { gameCode, message } = JSON.parse(data as any);
|
||||
|
||||
if (!socket.gameCode || socket.gameCode !== gameCode) {
|
||||
socket.emit('game:error', { message: 'You must be in the game to chat' });
|
||||
@@ -343,7 +363,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handlePlayerReady(socket: AuthenticatedSocket, data: { gameCode: string; ready: boolean }): Promise<void> {
|
||||
try {
|
||||
const { gameCode, ready } = data;
|
||||
const { gameCode, ready } = JSON.parse(data as any);
|
||||
const gameRoomName = `game_${gameCode}`;
|
||||
|
||||
// Update player ready status in Redis
|
||||
@@ -373,7 +393,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleApprovePlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string }): Promise<void> {
|
||||
try {
|
||||
const { gameCode, playerName } = data;
|
||||
const { gameCode, playerName } = JSON.parse(data as any);
|
||||
|
||||
// Verify that the requesting socket is the gamemaster
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
@@ -434,7 +454,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleRejectPlayer(socket: AuthenticatedSocket, data: { gameCode: string; playerName: string; reason?: string }): Promise<void> {
|
||||
try {
|
||||
const { gameCode, playerName, reason } = data;
|
||||
const { gameCode, playerName, reason } = JSON.parse(data as any);
|
||||
|
||||
// Verify that the requesting socket is the gamemaster
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
@@ -482,7 +502,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleJoinApproved(socket: AuthenticatedSocket, data: JoinGameData): Promise<void> {
|
||||
try {
|
||||
const { gameToken } = data;
|
||||
const { gameToken } = JSON.parse(data as any);
|
||||
|
||||
if (!gameToken) {
|
||||
socket.emit('game:error', { message: 'Game token is required' });
|
||||
@@ -560,7 +580,7 @@ export class GameWebSocketService {
|
||||
|
||||
private async handleDiceRoll(socket: AuthenticatedSocket, data: DiceRollData): Promise<void> {
|
||||
try {
|
||||
const { gameCode, diceValue } = data;
|
||||
const { gameCode, diceValue } = JSON.parse(data as any);
|
||||
|
||||
// Validate input
|
||||
if (!gameCode || !socket.gameCode || socket.gameCode !== gameCode) {
|
||||
@@ -772,14 +792,6 @@ export class GameWebSocketService {
|
||||
// Remove from pending players if they were pending
|
||||
await this.redisService.setRemove(`game_pending:${gameCode}`, playerName);
|
||||
|
||||
// If we have player ID, also clean up ID-based tracking
|
||||
if (playerId) {
|
||||
const game = await this.gameRepository.findByGameCode(gameCode);
|
||||
if (game?.id) {
|
||||
await this.redisService.setRemove(`active_players:${game.id}`, playerId);
|
||||
}
|
||||
}
|
||||
|
||||
logOther(`Cleaned up player data for ${playerName} in game ${gameCode}`);
|
||||
|
||||
} catch (error) {
|
||||
@@ -1276,7 +1288,6 @@ export class GameWebSocketService {
|
||||
if (gameId) {
|
||||
const gameIdKeys = [
|
||||
`game:${gameId}`, // Main game data
|
||||
`active_players:${gameId}`, // Active players set
|
||||
`game_turns:${gameId}` // Turn data by ID
|
||||
];
|
||||
|
||||
|
||||
@@ -7,60 +7,204 @@ export interface TokenPayload {
|
||||
authLevel: 0 | 1;
|
||||
userStatus: UserState;
|
||||
orgId: string;
|
||||
type?: 'access';
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
export interface RefreshTokenPayload {
|
||||
userId: string;
|
||||
type: 'refresh';
|
||||
orgId?: string;
|
||||
tokenId?: string; // For token rotation/revocation
|
||||
iat?: number;
|
||||
exp?: number;
|
||||
}
|
||||
|
||||
export interface TokenPair {
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export class JWTService {
|
||||
private readonly secretKey: string;
|
||||
private readonly refreshSecretKey: string;
|
||||
private readonly tokenExpiry: number;
|
||||
private readonly refreshTokenExpiry: number;
|
||||
private readonly cookieName: string;
|
||||
private readonly refreshCookieName: string;
|
||||
|
||||
constructor() {
|
||||
this.secretKey = process.env.JWT_SECRET || 'your-secret-key';
|
||||
this.refreshSecretKey = process.env.JWT_REFRESH_SECRET || this.secretKey + '_refresh';
|
||||
|
||||
let expiry = 86400;
|
||||
|
||||
// Access token expiry (short-lived)
|
||||
let expiry = 1800; // Default 30 minutes for better security
|
||||
if (process.env.JWT_EXPIRY) {
|
||||
expiry = parseInt(process.env.JWT_EXPIRY);
|
||||
} else if (process.env.JWT_EXPIRATION) {
|
||||
expiry = this.parseDuration(process.env.JWT_EXPIRATION);
|
||||
}
|
||||
|
||||
// Refresh token expiry (long-lived)
|
||||
let refreshExpiry = 604800; // Default 7 days
|
||||
if (process.env.JWT_REFRESH_EXPIRATION) {
|
||||
refreshExpiry = this.parseDuration(process.env.JWT_REFRESH_EXPIRATION);
|
||||
}
|
||||
|
||||
this.tokenExpiry = expiry;
|
||||
this.refreshTokenExpiry = refreshExpiry;
|
||||
this.cookieName = 'auth_token';
|
||||
this.refreshCookieName = 'refresh_token';
|
||||
|
||||
if (process.env.NODE_ENV === 'production' && (!process.env.JWT_SECRET || process.env.JWT_SECRET === 'your-secret-key')) {
|
||||
throw new Error('JWT_SECRET environment variable must be set in production');
|
||||
}
|
||||
}
|
||||
|
||||
create(payload: TokenPayload, res: Response): string {
|
||||
/**
|
||||
* Create a pair of access and refresh tokens
|
||||
*/
|
||||
public createTokenPair(payload: Omit<TokenPayload, 'type' | 'iat' | 'exp'>): TokenPair {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const payloadWithTimestamps: TokenPayload = {
|
||||
// Create access token
|
||||
const accessTokenPayload: TokenPayload = {
|
||||
...payload,
|
||||
type: 'access',
|
||||
iat: now,
|
||||
exp: now + this.tokenExpiry
|
||||
};
|
||||
const accessToken = jwt.sign(accessTokenPayload, this.secretKey);
|
||||
|
||||
// Don't use expiresIn option since we're manually setting exp in payload
|
||||
const options: SignOptions = {};
|
||||
const token = jwt.sign(payloadWithTimestamps, this.secretKey, options);
|
||||
// Create refresh token
|
||||
const refreshTokenPayload: RefreshTokenPayload = {
|
||||
userId: payload.userId,
|
||||
type: 'refresh',
|
||||
orgId: payload.orgId,
|
||||
iat: now,
|
||||
exp: now + this.refreshTokenExpiry
|
||||
};
|
||||
const refreshToken = jwt.sign(refreshTokenPayload, this.refreshSecretKey);
|
||||
|
||||
res.cookie(this.cookieName, token, {
|
||||
return { accessToken, refreshToken };
|
||||
}
|
||||
|
||||
/**
|
||||
* Create access and refresh tokens and set cookies (for cookie-based auth)
|
||||
*/
|
||||
create(payload: Omit<TokenPayload, 'type' | 'iat' | 'exp'>, res: Response): TokenPair {
|
||||
const tokenPair = this.createTokenPair(payload);
|
||||
this.setTokenCookies(res, tokenPair);
|
||||
return tokenPair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the request is using Bearer token authentication
|
||||
*/
|
||||
private isUsingBearerAuth(req: Request): boolean {
|
||||
// No cookie but has Authorization header
|
||||
return !req.cookies?.[this.cookieName] &&
|
||||
!!req.headers.authorization &&
|
||||
req.headers.authorization.startsWith('Bearer ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a refresh token
|
||||
*/
|
||||
public verifyRefreshToken(token: string): RefreshTokenPayload | null {
|
||||
try {
|
||||
const decoded = jwt.verify(token, this.refreshSecretKey) as RefreshTokenPayload;
|
||||
if (decoded.type !== 'refresh') {
|
||||
return null;
|
||||
}
|
||||
return decoded;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to refresh tokens using refresh token from cookies or headers
|
||||
*/
|
||||
public attemptTokenRefresh(req: Request, res: Response): TokenPair | null {
|
||||
try {
|
||||
// Try to get refresh token from cookie first
|
||||
let refreshToken = req.cookies[this.refreshCookieName];
|
||||
|
||||
// If no cookie, try X-Refresh-Token header
|
||||
if (!refreshToken) {
|
||||
refreshToken = req.headers['x-refresh-token'] as string;
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const refreshPayload = this.verifyRefreshToken(refreshToken);
|
||||
if (!refreshPayload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create new token pair
|
||||
const newTokenPair = this.createTokenPair({
|
||||
userId: refreshPayload.userId,
|
||||
authLevel: 0, // Default auth level, should be fetched from user data
|
||||
userStatus: UserState.VERIFIED_REGULAR, // Default status, should be fetched from user data
|
||||
orgId: refreshPayload.orgId || ''
|
||||
});
|
||||
|
||||
// Set new tokens based on authentication method
|
||||
if (req.cookies[this.cookieName] || req.cookies[this.refreshCookieName]) {
|
||||
// Cookie-based auth: set new cookies
|
||||
this.setTokenCookies(res, newTokenPair);
|
||||
} else {
|
||||
// Header-based auth: send tokens in response headers
|
||||
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
|
||||
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
|
||||
res.setHeader('X-Token-Refreshed', 'true');
|
||||
}
|
||||
|
||||
return newTokenPair;
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set token cookies for cookie-based authentication
|
||||
*/
|
||||
private setTokenCookies(res: Response, tokenPair: TokenPair): void {
|
||||
// Set access token cookie
|
||||
res.cookie(this.cookieName, tokenPair.accessToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: this.tokenExpiry * 1000, // Convert to milliseconds
|
||||
maxAge: this.tokenExpiry * 1000,
|
||||
});
|
||||
|
||||
return token;
|
||||
// Set refresh token cookie
|
||||
res.cookie(this.refreshCookieName, tokenPair.refreshToken, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
maxAge: this.refreshTokenExpiry * 1000,
|
||||
});
|
||||
}
|
||||
|
||||
verify(req: Request): TokenPayload | null {
|
||||
try {
|
||||
const token = req.cookies[this.cookieName];
|
||||
// First try to get token from cookie
|
||||
let token = req.cookies[this.cookieName];
|
||||
|
||||
// If no cookie token, try Authorization header
|
||||
if (!token) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||
token = authHeader.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) return null;
|
||||
|
||||
const decoded = jwt.verify(token, this.secretKey) as TokenPayload;
|
||||
@@ -70,6 +214,32 @@ export class JWTService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logout user by clearing tokens
|
||||
*/
|
||||
public logout(req: Request, res: Response): void {
|
||||
// Clear cookies if they exist
|
||||
if (req.cookies[this.cookieName]) {
|
||||
res.clearCookie(this.cookieName, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict'
|
||||
});
|
||||
}
|
||||
|
||||
if (req.cookies[this.refreshCookieName]) {
|
||||
res.clearCookie(this.refreshCookieName, {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict'
|
||||
});
|
||||
}
|
||||
|
||||
// For bearer token auth, set headers to indicate logout
|
||||
res.setHeader('X-Auth-Logout', 'true');
|
||||
res.setHeader('X-Clear-Tokens', 'true');
|
||||
}
|
||||
|
||||
// Check if token needs refresh (within 25% of expiry time)
|
||||
shouldRefreshToken(payload: TokenPayload): boolean {
|
||||
if (!payload.exp || !payload.iat) return false;
|
||||
@@ -83,16 +253,39 @@ export class JWTService {
|
||||
}
|
||||
|
||||
// Conditionally refresh token only if needed
|
||||
refreshIfNeeded(payload: TokenPayload, res: Response): boolean {
|
||||
refreshIfNeeded(payload: TokenPayload, res: Response, req?: Request): boolean {
|
||||
if (this.shouldRefreshToken(payload)) {
|
||||
// Create new token with fresh timestamps, but same user data
|
||||
const freshPayload: Omit<TokenPayload, 'iat' | 'exp'> = {
|
||||
if (req) {
|
||||
// Try to use the new refresh token system
|
||||
const newTokenPair = this.attemptTokenRefresh(req, res);
|
||||
if (newTokenPair) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: create new token pair
|
||||
const freshPayload: Omit<TokenPayload, 'iat' | 'exp' | 'type'> = {
|
||||
userId: payload.userId,
|
||||
authLevel: payload.authLevel,
|
||||
userStatus: payload.userStatus,
|
||||
orgId: payload.orgId
|
||||
};
|
||||
this.create(freshPayload, res);
|
||||
|
||||
// Check if using Bearer authentication
|
||||
if (req && this.isUsingBearerAuth(req)) {
|
||||
// For Bearer auth, create token pair and add to headers
|
||||
const newTokenPair = this.createTokenPair(freshPayload);
|
||||
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
|
||||
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
|
||||
res.setHeader('X-Token-Refreshed', 'true');
|
||||
} else {
|
||||
// For cookie auth, create token pair and set cookies
|
||||
const newTokenPair = this.create(freshPayload, res);
|
||||
res.setHeader('X-New-Access-Token', newTokenPair.accessToken);
|
||||
res.setHeader('X-New-Refresh-Token', newTokenPair.refreshToken);
|
||||
res.setHeader('X-Token-Refreshed', 'true');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { Response } from 'express';
|
||||
export interface LoginResponse {
|
||||
user: ShortUserDto;
|
||||
token: string;
|
||||
refreshToken: string;
|
||||
requiresOrgReauth?: boolean;
|
||||
orgLoginUrl?: string;
|
||||
organizationName?: string;
|
||||
@@ -111,7 +112,23 @@ export class LoginCommandHandler {
|
||||
try {
|
||||
// Use the real response object if provided, otherwise use mock
|
||||
const responseObj = res || mockRes;
|
||||
const token = this.jwtService.create(tokenPayload, responseObj);
|
||||
|
||||
// Check if client prefers Bearer token authentication
|
||||
const prefersBearerAuth = res && (
|
||||
res.req?.headers['authorization'] !== undefined ||
|
||||
res.req?.headers['x-auth-method'] === 'bearer' ||
|
||||
res.req?.headers['accept']?.includes('application/json')
|
||||
);
|
||||
|
||||
let tokenPair: any;
|
||||
|
||||
if (prefersBearerAuth && res) {
|
||||
// Create token pair for Bearer authentication (no cookies)
|
||||
tokenPair = this.jwtService.createTokenPair(tokenPayload);
|
||||
} else {
|
||||
// Cookie-based authentication (sets cookies automatically)
|
||||
tokenPair = this.jwtService.create(tokenPayload, responseObj);
|
||||
}
|
||||
|
||||
// Check if user belongs to an organization and needs reauthentication
|
||||
let requiresOrgReauth = false;
|
||||
@@ -154,7 +171,8 @@ export class LoginCommandHandler {
|
||||
|
||||
const response: LoginResponse = {
|
||||
user: UserMapper.toShortDto(user),
|
||||
token
|
||||
token: tokenPair.accessToken,
|
||||
refreshToken: tokenPair.refreshToken
|
||||
};
|
||||
|
||||
if (requiresOrgReauth) {
|
||||
|
||||
@@ -17,45 +17,63 @@ export class LogoutCommandHandler {
|
||||
try {
|
||||
logAuth('Logout process started', userId);
|
||||
|
||||
// 1. Get token from request to blacklist it
|
||||
let tokenToBlacklist: string | null = null;
|
||||
// 1. Get tokens from request to blacklist them
|
||||
let accessTokenToBlacklist: string | null = null;
|
||||
let refreshTokenToBlacklist: string | null = null;
|
||||
|
||||
if (req) {
|
||||
// Extract token from cookie
|
||||
tokenToBlacklist = req.cookies['auth_token'];
|
||||
|
||||
// Also check Authorization header as fallback
|
||||
if (!tokenToBlacklist && req.headers.authorization) {
|
||||
// Extract access token from cookie or Authorization header
|
||||
accessTokenToBlacklist = req.cookies['auth_token'];
|
||||
if (!accessTokenToBlacklist && req.headers.authorization) {
|
||||
const authHeader = req.headers.authorization;
|
||||
if (authHeader.startsWith('Bearer ')) {
|
||||
tokenToBlacklist = authHeader.substring(7);
|
||||
accessTokenToBlacklist = authHeader.substring(7);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract refresh token from cookie or header
|
||||
refreshTokenToBlacklist = req.cookies['refresh_token'];
|
||||
if (!refreshTokenToBlacklist) {
|
||||
refreshTokenToBlacklist = req.headers['x-refresh-token'] as string;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Blacklist the current JWT token in Redis (if available)
|
||||
if (tokenToBlacklist && req) {
|
||||
// 2. Blacklist both access and refresh tokens in Redis
|
||||
if (accessTokenToBlacklist && req) {
|
||||
try {
|
||||
// Store token in blacklist with expiration matching token expiry
|
||||
const decoded = this.jwtService.verify(req);
|
||||
if (decoded && decoded.exp) {
|
||||
const ttl = decoded.exp - Math.floor(Date.now() / 1000);
|
||||
if (ttl > 0) {
|
||||
await this.redisService.setWithExpiry(`blacklist:${tokenToBlacklist}`, 'true', ttl);
|
||||
logAuth('JWT token blacklisted', userId, { tokenExpiry: ttl });
|
||||
await this.redisService.setWithExpiry(`blacklist:${accessTokenToBlacklist}`, 'true', ttl);
|
||||
logAuth('Access token blacklisted', userId, { tokenExpiry: ttl });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('Failed to blacklist token', { userId, error: (error as Error).message });
|
||||
logWarning('Failed to blacklist access token', { userId, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Clear authentication cookie
|
||||
res.clearCookie('auth_token', {
|
||||
httpOnly: true,
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
sameSite: 'strict',
|
||||
path: '/'
|
||||
});
|
||||
// Blacklist refresh token if present
|
||||
if (refreshTokenToBlacklist) {
|
||||
try {
|
||||
const refreshDecoded = this.jwtService.verifyRefreshToken(refreshTokenToBlacklist);
|
||||
if (refreshDecoded && refreshDecoded.exp) {
|
||||
const ttl = refreshDecoded.exp - Math.floor(Date.now() / 1000);
|
||||
if (ttl > 0) {
|
||||
await this.redisService.setWithExpiry(`blacklist:${refreshTokenToBlacklist}`, 'true', ttl);
|
||||
logAuth('Refresh token blacklisted', userId, { tokenExpiry: ttl });
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logWarning('Failed to blacklist refresh token', { userId, error: (error as Error).message });
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Use JWT service to clear cookies and set logout headers
|
||||
if (req) {
|
||||
this.jwtService.logout(req, res);
|
||||
}
|
||||
|
||||
// 4. Remove user from active sessions in Redis
|
||||
try {
|
||||
|
||||
@@ -50,16 +50,19 @@ export class GameAggregate {
|
||||
@Column({ type: 'int', default: LoginType.PUBLIC })
|
||||
logintype!: LoginType;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
createdby!: string | null;
|
||||
@Column({ type: 'int', default: 50 })
|
||||
boardsize!: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
@Column({ type: 'uuid', nullable: false, name: 'createdBy' })
|
||||
createdby!: string;
|
||||
|
||||
@Column({ type: 'uuid', nullable: true, name: 'organizationid' })
|
||||
orgid!: string | null;
|
||||
|
||||
@Column({ type: 'json' })
|
||||
@Column({ type: 'jsonb', default: () => "'[]'", name: 'decks' })
|
||||
gamedecks!: GameDeck[];
|
||||
|
||||
@Column({ type: 'json', default: () => "'[]'" })
|
||||
@Column({ type: 'uuid', array: true, default: () => "'{}'", name: 'playerids' })
|
||||
players!: string[];
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
@@ -68,22 +71,22 @@ export class GameAggregate {
|
||||
@Column({ type: 'boolean', default: false })
|
||||
finished!: boolean;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
@Column({ type: 'uuid', nullable: true, name: 'winnerid' })
|
||||
winner!: string | null;
|
||||
|
||||
@Column({ type: 'int', default: GameState.WAITING })
|
||||
state!: GameState;
|
||||
|
||||
@CreateDateColumn({ name: 'create_date' })
|
||||
@CreateDateColumn({ name: 'createDate' })
|
||||
createdate!: Date;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true, name: 'start_date' })
|
||||
startdate!: Date | null;
|
||||
|
||||
@Column({ type: 'timestamp', nullable: true, name: 'end_date' })
|
||||
@Column({ type: 'timestamp', nullable: true, name: 'finishDate' })
|
||||
enddate!: Date | null;
|
||||
|
||||
@UpdateDateColumn({ name: 'update_date' })
|
||||
@UpdateDateColumn({ name: 'updateDate' })
|
||||
updatedate!: Date;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Test1755691733404 implements MigrationInterface {
|
||||
name = 'Test1755691733404'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "Users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "orgid" uuid, "username" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "fname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL, "code" character varying(50), "type" character varying(50) NOT NULL, "phone" character varying(20), "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "Orglogindate" TIMESTAMP, CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE ("username"), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE ("email"), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Organizations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "contactfname" character varying(100) NOT NULL, "contactlname" character varying(100) NOT NULL, "contactphone" character varying(20) NOT NULL, "contactemail" character varying(255) NOT NULL, "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "url" character varying(500), "userinorg" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Decks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" integer NOT NULL, "user_id" uuid NOT NULL, "creation_date" TIMESTAMP NOT NULL DEFAULT now(), "cards" json NOT NULL, "played_number" integer NOT NULL DEFAULT '0', "ctype" integer NOT NULL DEFAULT '0', "update_date" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "organization_id" uuid, CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Chats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "users" uuid array NOT NULL, "messages" json NOT NULL, "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "Decks" ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Decks" DROP CONSTRAINT "FK_06ee28f90d68543a03b14aebe13"`);
|
||||
await queryRunner.query(`DROP TABLE "Chats"`);
|
||||
await queryRunner.query(`DROP TABLE "Decks"`);
|
||||
await queryRunner.query(`DROP TABLE "Organizations"`);
|
||||
await queryRunner.query(`DROP TABLE "Users"`);
|
||||
}
|
||||
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddEmailVerificationFields1755706019351 implements MigrationInterface {
|
||||
name = 'AddEmailVerificationFields1755706019351'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Users" DROP COLUMN "code"`);
|
||||
await queryRunner.query(`ALTER TABLE "Users" ADD "token" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "Users" ADD "TokenExpires" TIMESTAMP`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Users" DROP COLUMN "TokenExpires"`);
|
||||
await queryRunner.query(`ALTER TABLE "Users" DROP COLUMN "token"`);
|
||||
await queryRunner.query(`ALTER TABLE "Users" ADD "code" character varying(50)`);
|
||||
}
|
||||
|
||||
}
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddChatMessagingSystem1755817306222 implements MigrationInterface {
|
||||
name = 'AddChatMessagingSystem1755817306222'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "ChatArchives" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatId" uuid NOT NULL, "archivedMessages" json NOT NULL, "archivedAt" TIMESTAMP NOT NULL, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "chatType" character varying(50) NOT NULL, "chatName" character varying(255), "gameId" uuid, "participants" uuid array NOT NULL, CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "type" character varying(50) NOT NULL DEFAULT 'direct'`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "name" character varying(255)`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "gameId" uuid`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "createdBy" uuid`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "lastActivity" TIMESTAMP`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "createDate" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ADD "archiveDate" TIMESTAMP`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ALTER COLUMN "messages" SET DEFAULT '[]'`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Chats" ALTER COLUMN "messages" DROP DEFAULT`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "archiveDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "createDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "lastActivity"`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "createdBy"`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "gameId"`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "name"`);
|
||||
await queryRunner.query(`ALTER TABLE "Chats" DROP COLUMN "type"`);
|
||||
await queryRunner.query(`DROP TABLE "ChatArchives"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class CreateContactTable1755855028839 implements MigrationInterface {
|
||||
name = 'CreateContactTable1755855028839'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "Contacts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "userid" uuid, "type" integer NOT NULL, "txt" text NOT NULL, "state" integer NOT NULL DEFAULT '0', "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "adminResponse" text, "responseDate" TIMESTAMP, "respondedBy" uuid, CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY ("id"))`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE "Contacts"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1757939815984 implements MigrationInterface {
|
||||
name = 'Full1757939815984'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`CREATE TABLE "Chats" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "type" character varying(50) NOT NULL DEFAULT 'direct', "name" character varying(255), "gameId" uuid, "createdBy" uuid, "users" uuid array NOT NULL, "messages" json NOT NULL DEFAULT '[]', "lastActivity" TIMESTAMP, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "archiveDate" TIMESTAMP, CONSTRAINT "PK_64c36c2b8d86a0d5de4cf64de8d" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "orgid" uuid, "username" character varying(100) NOT NULL, "password" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "fname" character varying(100) NOT NULL, "lname" character varying(100) NOT NULL, "token" character varying(255), "TokenExpires" TIMESTAMP, "phone" character varying(20), "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "Orglogindate" TIMESTAMP, CONSTRAINT "UQ_ffc81a3b97dcbf8e320d5106c0d" UNIQUE ("username"), CONSTRAINT "UQ_3c3ab3f49a87e6ddb607f3c4945" UNIQUE ("email"), CONSTRAINT "PK_16d4f7d636df336db11d87413e3" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Contacts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "email" character varying(255) NOT NULL, "userid" uuid, "type" integer NOT NULL, "txt" text NOT NULL, "state" integer NOT NULL DEFAULT '0', "createDate" TIMESTAMP NOT NULL DEFAULT now(), "updateDate" TIMESTAMP NOT NULL DEFAULT now(), "adminResponse" text, "responseDate" TIMESTAMP, "respondedBy" uuid, CONSTRAINT "PK_68782cec65c8eef577c62958273" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "ChatArchives" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "chatId" uuid NOT NULL, "archivedMessages" json NOT NULL, "archivedAt" TIMESTAMP NOT NULL, "createDate" TIMESTAMP NOT NULL DEFAULT now(), "chatType" character varying(50) NOT NULL, "chatName" character varying(255), "gameId" uuid, "participants" uuid array NOT NULL, CONSTRAINT "PK_fe62979fc2061d7afe278d3f14e" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Games" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "gamecode" character varying(10) NOT NULL, "maxplayers" integer NOT NULL, "logintype" integer NOT NULL DEFAULT '0', "createdby" character varying(255), "orgid" character varying(255), "gamedecks" json NOT NULL, "players" json NOT NULL DEFAULT '[]', "started" boolean NOT NULL DEFAULT false, "finished" boolean NOT NULL DEFAULT false, "winner" character varying(255), "state" integer NOT NULL DEFAULT '0', "create_date" TIMESTAMP NOT NULL DEFAULT now(), "start_date" TIMESTAMP, "end_date" TIMESTAMP, "update_date" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "UQ_9d52c646079cbe6f242a85c5c41" UNIQUE ("gamecode"), CONSTRAINT "PK_1950492f583d31609c5e9fbbe12" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Organizations" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "contactfname" character varying(100) NOT NULL, "contactlname" character varying(100) NOT NULL, "contactphone" character varying(20) NOT NULL, "contactemail" character varying(255) NOT NULL, "state" integer NOT NULL DEFAULT '0', "regdate" TIMESTAMP NOT NULL DEFAULT now(), "updatedate" TIMESTAMP NOT NULL DEFAULT now(), "url" character varying(500), "userinorg" integer NOT NULL DEFAULT '0', "maxOrganizationalDecks" integer, CONSTRAINT "PK_e0690a31419f6666194423526f2" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`CREATE TABLE "Decks" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying(255) NOT NULL, "type" integer NOT NULL, "user_id" uuid NOT NULL, "creation_date" TIMESTAMP NOT NULL DEFAULT now(), "cards" json NOT NULL, "played_number" integer NOT NULL DEFAULT '0', "ctype" integer NOT NULL DEFAULT '0', "update_date" TIMESTAMP NOT NULL DEFAULT now(), "state" integer NOT NULL DEFAULT '0', "organization_id" uuid, CONSTRAINT "PK_001f26cb3ec39c1f25269943473" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "Decks" ADD CONSTRAINT "FK_06ee28f90d68543a03b14aebe13" FOREIGN KEY ("organization_id") REFERENCES "Organizations"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Decks" DROP CONSTRAINT "FK_06ee28f90d68543a03b14aebe13"`);
|
||||
await queryRunner.query(`DROP TABLE "Decks"`);
|
||||
await queryRunner.query(`DROP TABLE "Organizations"`);
|
||||
await queryRunner.query(`DROP TABLE "Games"`);
|
||||
await queryRunner.query(`DROP TABLE "ChatArchives"`);
|
||||
await queryRunner.query(`DROP TABLE "Contacts"`);
|
||||
await queryRunner.query(`DROP TABLE "Users"`);
|
||||
await queryRunner.query(`DROP TABLE "Chats"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1758463929834 implements MigrationInterface {
|
||||
name = 'Full1758463929834'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "winner"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "create_date"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "end_date"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "update_date"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "boardsize" integer NOT NULL DEFAULT '50'`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "winnerid" uuid`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "createDate" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "finishDate" TIMESTAMP`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "updateDate" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "updateDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "finishDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "createDate"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "winnerid"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" DROP COLUMN "boardsize"`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "update_date" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "end_date" TIMESTAMP`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "create_date" TIMESTAMP NOT NULL DEFAULT now()`);
|
||||
await queryRunner.query(`ALTER TABLE "Games" ADD "winner" character varying(255)`);
|
||||
}
|
||||
|
||||
}
|
||||
-28
@@ -1,28 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
export class AddMaxOrganizationalDecksToOrganization1692712800000 implements MigrationInterface {
|
||||
name = 'AddMaxOrganizationalDecksToOrganization1692712800000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Add maxOrganizationalDecks column to Organizations table
|
||||
await queryRunner.addColumn('Organizations', new TableColumn({
|
||||
name: 'maxOrganizationalDecks',
|
||||
type: 'int',
|
||||
isNullable: true, // No default - set by admin
|
||||
comment: 'Maximum number of organizational decks a premium user can create in this organization'
|
||||
}));
|
||||
|
||||
// Add performance indexes for deck filtering queries
|
||||
await queryRunner.query(`CREATE INDEX "IDX_DECK_USER_STATE_CTYPE" ON "Decks" ("user_id", "state", "ctype")`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_DECK_ORG_CTYPE_STATE" ON "Decks" ("organization_id", "ctype", "state")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Drop indexes
|
||||
await queryRunner.query(`DROP INDEX "IDX_DECK_ORG_CTYPE_STATE"`);
|
||||
await queryRunner.query(`DROP INDEX "IDX_DECK_USER_STATE_CTYPE"`);
|
||||
|
||||
// Remove maxOrganizationalDecks column
|
||||
await queryRunner.dropColumn('Organizations', 'maxOrganizationalDecks');
|
||||
}
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddEmailVerificationFields1755706017175 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
-11
@@ -1,11 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class FixEmailVerificationFields1755706055220 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Full1757939815062 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class Test1755691732089 implements MigrationInterface {
|
||||
export class Full1758463928499 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
}
|
||||
+50
-13
@@ -1,17 +1,54 @@
|
||||
# Development Environment Variables
|
||||
POSTGRES_PASSWORD=postgres
|
||||
JWT_SECRET=dev_jwt_secret_change_in_production_please_use_a_long_random_string
|
||||
JWT_EXPIRATION=24h
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
# ==============================================
|
||||
# SerpentRace Backend Environment Configuration
|
||||
# ==============================================
|
||||
# Copy this file to .env and fill in your values
|
||||
|
||||
# APPLICATION CONFIGURATION
|
||||
NODE_ENV=development
|
||||
PORT=3000
|
||||
APP_BASE_URL=http://localhost:3000
|
||||
|
||||
# DATABASE CONFIGURATION (PostgreSQL)
|
||||
DB_HOST=postgres
|
||||
DB_PORT=5432
|
||||
DB_NAME=serpentrace
|
||||
DB_USERNAME=postgres
|
||||
DB_PASSWORD=postgres
|
||||
|
||||
# REDIS CONFIGURATION
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
# MINIO CONFIGURATION
|
||||
MINIO_ENDPOINT=minio
|
||||
MINIO_PORT=9000
|
||||
MINIO_ACCESS_KEY=serpentrace
|
||||
MINIO_SECRET_KEY=serpentrace123!
|
||||
MINIO_USE_SSL=false
|
||||
MINIO_BUCKET_NAME=serpentrace-logs
|
||||
|
||||
# Optional: Email configuration for development
|
||||
EMAIL_HOST=
|
||||
EMAIL_PORT=
|
||||
EMAIL_USER=
|
||||
EMAIL_PASS=
|
||||
EMAIL_FROM=
|
||||
# JWT CONFIGURATION
|
||||
JWT_SECRET=your_super_secret_jwt_key_change_in_production
|
||||
JWT_EXPIRY=86400
|
||||
JWT_EXPIRATION=24h
|
||||
JWT_REFRESH_EXPIRATION=7d
|
||||
GAME_TOKEN_EXPIRY=86400
|
||||
|
||||
# Optional: Other development settings
|
||||
NODE_ENV=development
|
||||
# EMAIL SERVICE CONFIGURATION
|
||||
EMAIL_HOST=mail.serpentrace.hu
|
||||
EMAIL_PORT=465
|
||||
EMAIL_SECURE=true
|
||||
EMAIL_USER=noreply@serpentrace.hu
|
||||
EMAIL_PASS=ZUx720ece&Cin&F{
|
||||
EMAIL_FROM=noreply@serpentrace.com
|
||||
|
||||
# CHAT SYSTEM CONFIGURATION
|
||||
CHAT_INACTIVITY_TIMEOUT_MINUTES=30
|
||||
CHAT_MAX_MESSAGES_PER_USER=100
|
||||
CHAT_MESSAGE_CLEANUP_WEEKS=4
|
||||
|
||||
# GAME CONFIGURATION
|
||||
MAX_SPECIAL_FIELDS_PERCENTAGE=67
|
||||
MAX_GENERATION_TIME_SECONDS=20
|
||||
GENERATION_ERROR_TOLERANCE=15
|
||||
|
||||
@@ -60,7 +60,7 @@ services:
|
||||
- "5173:5173"
|
||||
environment:
|
||||
- NODE_ENV=development
|
||||
- VITE_API_URL=http://localhost:3000
|
||||
- API_URL=http://localhost:3000
|
||||
volumes:
|
||||
- ../SerpentRace_Frontend:/app
|
||||
- /app/node_modules
|
||||
|
||||
@@ -6,6 +6,8 @@ services:
|
||||
dockerfile: ../SerpentRace_Docker/Dockerfile_backend.dev
|
||||
container_name: serpentrace-backend-dev
|
||||
restart: unless-stopped
|
||||
env_file:
|
||||
- .env.dev
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
@@ -19,9 +21,6 @@ services:
|
||||
- REDIS_URL=redis://redis:6379
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
- JWT_SECRET=dev_jwt_secret_change_in_production
|
||||
- JWT_EXPIRATION=24h
|
||||
- JWT_REFRESH_EXPIRATION=7d
|
||||
- MINIO_ENDPOINT=minio
|
||||
- MINIO_PORT=9000
|
||||
- MINIO_ACCESS_KEY=serpentrace
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
{
|
||||
"name": "SerpentRace",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"node_modules/framer-motion": {
|
||||
"version": "12.23.0",
|
||||
"resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.0.tgz",
|
||||
"integrity": "sha512-xf6NxTGAyf7zR4r2KlnhFmsRfKIbjqeBupEDBAaEtVIBJX96sAon00kMlsKButSIRwPSHjbRrAPnYdJJ9kyhbA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-dom": "^12.22.0",
|
||||
"motion-utils": "^12.19.0",
|
||||
"tslib": "^2.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@emotion/is-prop-valid": "*",
|
||||
"react": "^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@emotion/is-prop-valid": {
|
||||
"optional": true
|
||||
},
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/motion-dom": {
|
||||
"version": "12.22.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.22.0.tgz",
|
||||
"integrity": "sha512-ooH7+/BPw9gOsL9VtPhEJHE2m4ltnhMlcGMhEqA0YGNhKof7jdaszvsyThXI6LVIKshJUZ9/CP6HNqQhJfV7kw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"motion-utils": "^12.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/motion-utils": {
|
||||
"version": "12.19.0",
|
||||
"resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.19.0.tgz",
|
||||
"integrity": "sha512-BuFTHINYmV07pdWs6lj6aI63vr2N4dg0vR+td0rtrdpWOhBzIkEklZyLcvKBoEtwSqx8Jg06vUB5RS0xDiUybw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Framer B.V.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-137
@@ -1,137 +0,0 @@
|
||||
<p align="center">
|
||||
<img width="42" height="42" alt="Motion logo" src="https://github.com/user-attachments/assets/00d6d1c3-72c4-4c2f-a664-69da13182ffc" />
|
||||
</p>
|
||||
<h1 align="center">Motion for React</h1>
|
||||
<h3 align="center">
|
||||
An open source, production-ready animation library
|
||||
</h3>
|
||||
<p align="center">Previously Framer Motion. Also available for <a href="https://github.com/motiondivision/motion/tree/main/packages/motion">JavaScript</a> and <a href="https://github.com/motiondivision/motion-vue">Vue</a></p>
|
||||
|
||||
<br>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.npmjs.com/package/framer-motion" target="_blank">
|
||||
<img src="https://img.shields.io/npm/v/framer-motion.svg?style=flat-square" />
|
||||
</a>
|
||||
<a href="https://www.npmjs.com/package/framer-motion" target="_blank">
|
||||
<img src="https://img.shields.io/npm/dm/framer-motion.svg?style=flat-square" />
|
||||
</a>
|
||||
<a href="https://twitter.com/mattgperry" target="_blank">
|
||||
<img src="https://img.shields.io/twitter/follow/mattgperry.svg?style=social&label=Follow" />
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
|
||||
Motion for React is an open source, production-ready library that’s designed for all creative developers.
|
||||
|
||||
It's the only animation library with a hybrid engine, combining the power of JavaScript animations with the performance of native browser APIs.
|
||||
|
||||
It looks like this:
|
||||
|
||||
```jsx
|
||||
<motion.div animate={{ x: 0 }} />
|
||||
```
|
||||
|
||||
It does all this:
|
||||
|
||||
- [Springs](https://motion.dev/docs/react-transitions#spring)
|
||||
- [Keyframes](https://motion.dev/docs/react-animation#keyframes)
|
||||
- [Layout animations](https://motion.dev/docs/react-layout-animations)
|
||||
- [Shared layout animations](https://motion.dev/docs/react-layout-animations#shared-layout-animations)
|
||||
- [Gestures (drag/tap/hover)](https://motion.dev/docs/react-gestures)
|
||||
- [Scroll animations](https://motion.dev/docs/react-scroll-animations)
|
||||
- [SVG paths](https://motion.dev/docs/react-animation#svg-line-drawing)
|
||||
- [Exit animations](https://motion.dev/docs/react-animation#exit-animations)
|
||||
- [Server-side rendering](https://motion.dev/docs/react-motion-component#server-side-rendering)
|
||||
- [Independent transforms](https://motion.dev/docs/react-motion-component#style)
|
||||
- [Orchestrate animations across components](https://motion.dev/docs/react-animation#orchestration)
|
||||
- [CSS variables](https://motion.dev/docs/react-animation#css-variables)
|
||||
|
||||
...and a whole lot more.
|
||||
|
||||
## Get started
|
||||
|
||||
### 🐇 Quick start
|
||||
|
||||
```bash
|
||||
npm install motion
|
||||
```
|
||||
|
||||
```jsx
|
||||
import { motion } from "motion/react"
|
||||
|
||||
function Component() {
|
||||
return <motion.div animate={{ x: 100 }} />
|
||||
}
|
||||
```
|
||||
|
||||
Get started with [Motion for React](https://motion.dev/docs/react-quick-start).
|
||||
|
||||
## 🎨 Studio
|
||||
|
||||

|
||||
|
||||
Motion Studio is a versatile suite of developer tools allowing you to:
|
||||
|
||||
- Visually edit CSS and Motion easing curves in VS Code
|
||||
- Generate CSS springs with LLMs
|
||||
- Load Motion docs into your LLM
|
||||
|
||||
Get started with [Motion Studio](https://motion.dev/docs/tools-quick-start).
|
||||
|
||||
## 🎓 Examples
|
||||
|
||||
[Motion Examples](https://examples.motion.dev/react) offers 100s of free and Motion+ examples for beginners and advanced users alike. Easy copy/paste code to kickstart your next project.
|
||||
|
||||
## ⚡️ Motion+
|
||||
|
||||
[Motion+](https://motion.dev/plus) is a one-time fee, lifetime membership that unlocks over 100 premium examples, early access, powerful Studio tools, a private Discord, and exclusive APIs like:
|
||||
|
||||
- Cursor
|
||||
- Ticker
|
||||
- AnimateNumber
|
||||
- splitText
|
||||
|
||||
[Get Motion+](https://motion.dev/plus)
|
||||
|
||||
### 💎 Contribute
|
||||
|
||||
- Want to contribute to Motion? Our [contributing guide](https://github.com/motiondivision/motion/blob/master/CONTRIBUTING.md) has you covered.
|
||||
|
||||
### 👩🏻⚖️ License
|
||||
|
||||
- Motion for React is MIT licensed.
|
||||
|
||||
## ✨ Sponsors
|
||||
|
||||
Motion is sustainable thanks to the kind support of its sponsors.
|
||||
|
||||
### Partners
|
||||
|
||||
#### Framer
|
||||
|
||||
Motion powers Framer animations, the web builder for creative pros. Design and ship your dream site. Zero code, maximum speed.
|
||||
|
||||
<a href="https://www.framer.com?utm_source=motion-readme">
|
||||
<img alt="Framer" src="https://github.com/user-attachments/assets/0404c7a1-c29d-4785-89ae-aae315f3c759" width="300px" height="200px">
|
||||
</a>
|
||||
|
||||
### Platinum
|
||||
|
||||
<a href="https://tailwindcss.com"><img alt="Tailwind" src="https://github.com/user-attachments/assets/c0496f09-b8ee-4bc4-85ab-83a071bbbdec" width="300px" height="200px"></a> <a href="https://emilkowal.ski"><img alt="Emil Kowalski" src="https://github.com/user-attachments/assets/29f56b1a-37fb-4695-a6a6-151f6c24864f" width="300px" height="200px"></a> <a href="https://linear.app"><img alt="Linear" src="https://github.com/user-attachments/assets/a93710bb-d8ed-40e3-b0fb-1c5b3e2b16bb" width="300px" height="200px"></a>
|
||||
|
||||
### Gold
|
||||
|
||||
<a href="https://vercel.com"><img alt="Vercel" src="https://github.com/user-attachments/assets/23cb1e37-fa67-49ad-8f77-7f4b8eaba325" width="225px" height="150px"></a> <a href="https://liveblocks.io"><img alt="Liveblocks" src="https://github.com/user-attachments/assets/31436a47-951e-4eab-9a68-bdd54ccf9444" width="225px" height="150px"></a> <a href="https://lu.ma"><img alt="Luma" src="https://github.com/user-attachments/assets/4fae0c9d-de0f-4042-9cd6-e07885d028a9" width="225px" height="150px"></a>
|
||||
|
||||
### Silver
|
||||
|
||||
<a href="https://www.frontend.fyi/?utm_source=motion"><img alt="Frontend.fyi" src="https://github.com/user-attachments/assets/07d23aa5-69db-44a0-849d-90177e6fc817" width="150px" height="100px"></a> <a href="https://firecrawl.dev"><img alt="Firecrawl" src="https://github.com/user-attachments/assets/cba90e54-1329-4353-8fba-85beef4d2ee9" width="150px" height="100px"></a> <a href="https://puzzmo.com"><img alt="Puzzmo" src="https://github.com/user-attachments/assets/aa2d5586-e5e2-43b9-8446-db456e4b0758" width="150px" height="100px"></a> <a href="https://buildui.com"><img alt="Build UI" src="https://github.com/user-attachments/assets/024bfcd5-50e8-4b3d-a115-d5c6d6030d1c" width="150px" height="100px"></a>
|
||||
|
||||
### Personal
|
||||
|
||||
- [OlegWock](https://sinja.io)
|
||||
- [Lambert Weller](https://github.com/l-mbert)
|
||||
- [Jake LeBoeuf](https://jklb.wf)
|
||||
- [Han Lee](https://github.com/hahnlee)
|
||||
-1
@@ -1 +0,0 @@
|
||||
This directory is a fallback for `exports["./client"]` in the root `framer-motion` `package.json`.
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"private": true,
|
||||
"types": "../dist/types/client.d.ts",
|
||||
"main": "../dist/cjs/client.js",
|
||||
"module": "../dist/es/client.mjs"
|
||||
}
|
||||
-365
@@ -1,365 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var create = require('./create-C-c1JfhA.js');
|
||||
require('motion-dom');
|
||||
require('motion-utils');
|
||||
require('react/jsx-runtime');
|
||||
require('react');
|
||||
|
||||
/**
|
||||
* HTML components
|
||||
*/
|
||||
const MotionA = /*@__PURE__*/ create.createMotionComponent("a");
|
||||
const MotionAbbr = /*@__PURE__*/ create.createMotionComponent("abbr");
|
||||
const MotionAddress = /*@__PURE__*/ create.createMotionComponent("address");
|
||||
const MotionArea = /*@__PURE__*/ create.createMotionComponent("area");
|
||||
const MotionArticle = /*@__PURE__*/ create.createMotionComponent("article");
|
||||
const MotionAside = /*@__PURE__*/ create.createMotionComponent("aside");
|
||||
const MotionAudio = /*@__PURE__*/ create.createMotionComponent("audio");
|
||||
const MotionB = /*@__PURE__*/ create.createMotionComponent("b");
|
||||
const MotionBase = /*@__PURE__*/ create.createMotionComponent("base");
|
||||
const MotionBdi = /*@__PURE__*/ create.createMotionComponent("bdi");
|
||||
const MotionBdo = /*@__PURE__*/ create.createMotionComponent("bdo");
|
||||
const MotionBig = /*@__PURE__*/ create.createMotionComponent("big");
|
||||
const MotionBlockquote =
|
||||
/*@__PURE__*/ create.createMotionComponent("blockquote");
|
||||
const MotionBody = /*@__PURE__*/ create.createMotionComponent("body");
|
||||
const MotionButton = /*@__PURE__*/ create.createMotionComponent("button");
|
||||
const MotionCanvas = /*@__PURE__*/ create.createMotionComponent("canvas");
|
||||
const MotionCaption = /*@__PURE__*/ create.createMotionComponent("caption");
|
||||
const MotionCite = /*@__PURE__*/ create.createMotionComponent("cite");
|
||||
const MotionCode = /*@__PURE__*/ create.createMotionComponent("code");
|
||||
const MotionCol = /*@__PURE__*/ create.createMotionComponent("col");
|
||||
const MotionColgroup = /*@__PURE__*/ create.createMotionComponent("colgroup");
|
||||
const MotionData = /*@__PURE__*/ create.createMotionComponent("data");
|
||||
const MotionDatalist = /*@__PURE__*/ create.createMotionComponent("datalist");
|
||||
const MotionDd = /*@__PURE__*/ create.createMotionComponent("dd");
|
||||
const MotionDel = /*@__PURE__*/ create.createMotionComponent("del");
|
||||
const MotionDetails = /*@__PURE__*/ create.createMotionComponent("details");
|
||||
const MotionDfn = /*@__PURE__*/ create.createMotionComponent("dfn");
|
||||
const MotionDialog = /*@__PURE__*/ create.createMotionComponent("dialog");
|
||||
const MotionDiv = /*@__PURE__*/ create.createMotionComponent("div");
|
||||
const MotionDl = /*@__PURE__*/ create.createMotionComponent("dl");
|
||||
const MotionDt = /*@__PURE__*/ create.createMotionComponent("dt");
|
||||
const MotionEm = /*@__PURE__*/ create.createMotionComponent("em");
|
||||
const MotionEmbed = /*@__PURE__*/ create.createMotionComponent("embed");
|
||||
const MotionFieldset = /*@__PURE__*/ create.createMotionComponent("fieldset");
|
||||
const MotionFigcaption =
|
||||
/*@__PURE__*/ create.createMotionComponent("figcaption");
|
||||
const MotionFigure = /*@__PURE__*/ create.createMotionComponent("figure");
|
||||
const MotionFooter = /*@__PURE__*/ create.createMotionComponent("footer");
|
||||
const MotionForm = /*@__PURE__*/ create.createMotionComponent("form");
|
||||
const MotionH1 = /*@__PURE__*/ create.createMotionComponent("h1");
|
||||
const MotionH2 = /*@__PURE__*/ create.createMotionComponent("h2");
|
||||
const MotionH3 = /*@__PURE__*/ create.createMotionComponent("h3");
|
||||
const MotionH4 = /*@__PURE__*/ create.createMotionComponent("h4");
|
||||
const MotionH5 = /*@__PURE__*/ create.createMotionComponent("h5");
|
||||
const MotionH6 = /*@__PURE__*/ create.createMotionComponent("h6");
|
||||
const MotionHead = /*@__PURE__*/ create.createMotionComponent("head");
|
||||
const MotionHeader = /*@__PURE__*/ create.createMotionComponent("header");
|
||||
const MotionHgroup = /*@__PURE__*/ create.createMotionComponent("hgroup");
|
||||
const MotionHr = /*@__PURE__*/ create.createMotionComponent("hr");
|
||||
const MotionHtml = /*@__PURE__*/ create.createMotionComponent("html");
|
||||
const MotionI = /*@__PURE__*/ create.createMotionComponent("i");
|
||||
const MotionIframe = /*@__PURE__*/ create.createMotionComponent("iframe");
|
||||
const MotionImg = /*@__PURE__*/ create.createMotionComponent("img");
|
||||
const MotionInput = /*@__PURE__*/ create.createMotionComponent("input");
|
||||
const MotionIns = /*@__PURE__*/ create.createMotionComponent("ins");
|
||||
const MotionKbd = /*@__PURE__*/ create.createMotionComponent("kbd");
|
||||
const MotionKeygen = /*@__PURE__*/ create.createMotionComponent("keygen");
|
||||
const MotionLabel = /*@__PURE__*/ create.createMotionComponent("label");
|
||||
const MotionLegend = /*@__PURE__*/ create.createMotionComponent("legend");
|
||||
const MotionLi = /*@__PURE__*/ create.createMotionComponent("li");
|
||||
const MotionLink = /*@__PURE__*/ create.createMotionComponent("link");
|
||||
const MotionMain = /*@__PURE__*/ create.createMotionComponent("main");
|
||||
const MotionMap = /*@__PURE__*/ create.createMotionComponent("map");
|
||||
const MotionMark = /*@__PURE__*/ create.createMotionComponent("mark");
|
||||
const MotionMenu = /*@__PURE__*/ create.createMotionComponent("menu");
|
||||
const MotionMenuitem = /*@__PURE__*/ create.createMotionComponent("menuitem");
|
||||
const MotionMeter = /*@__PURE__*/ create.createMotionComponent("meter");
|
||||
const MotionNav = /*@__PURE__*/ create.createMotionComponent("nav");
|
||||
const MotionObject = /*@__PURE__*/ create.createMotionComponent("object");
|
||||
const MotionOl = /*@__PURE__*/ create.createMotionComponent("ol");
|
||||
const MotionOptgroup = /*@__PURE__*/ create.createMotionComponent("optgroup");
|
||||
const MotionOption = /*@__PURE__*/ create.createMotionComponent("option");
|
||||
const MotionOutput = /*@__PURE__*/ create.createMotionComponent("output");
|
||||
const MotionP = /*@__PURE__*/ create.createMotionComponent("p");
|
||||
const MotionParam = /*@__PURE__*/ create.createMotionComponent("param");
|
||||
const MotionPicture = /*@__PURE__*/ create.createMotionComponent("picture");
|
||||
const MotionPre = /*@__PURE__*/ create.createMotionComponent("pre");
|
||||
const MotionProgress = /*@__PURE__*/ create.createMotionComponent("progress");
|
||||
const MotionQ = /*@__PURE__*/ create.createMotionComponent("q");
|
||||
const MotionRp = /*@__PURE__*/ create.createMotionComponent("rp");
|
||||
const MotionRt = /*@__PURE__*/ create.createMotionComponent("rt");
|
||||
const MotionRuby = /*@__PURE__*/ create.createMotionComponent("ruby");
|
||||
const MotionS = /*@__PURE__*/ create.createMotionComponent("s");
|
||||
const MotionSamp = /*@__PURE__*/ create.createMotionComponent("samp");
|
||||
const MotionScript = /*@__PURE__*/ create.createMotionComponent("script");
|
||||
const MotionSection = /*@__PURE__*/ create.createMotionComponent("section");
|
||||
const MotionSelect = /*@__PURE__*/ create.createMotionComponent("select");
|
||||
const MotionSmall = /*@__PURE__*/ create.createMotionComponent("small");
|
||||
const MotionSource = /*@__PURE__*/ create.createMotionComponent("source");
|
||||
const MotionSpan = /*@__PURE__*/ create.createMotionComponent("span");
|
||||
const MotionStrong = /*@__PURE__*/ create.createMotionComponent("strong");
|
||||
const MotionStyle = /*@__PURE__*/ create.createMotionComponent("style");
|
||||
const MotionSub = /*@__PURE__*/ create.createMotionComponent("sub");
|
||||
const MotionSummary = /*@__PURE__*/ create.createMotionComponent("summary");
|
||||
const MotionSup = /*@__PURE__*/ create.createMotionComponent("sup");
|
||||
const MotionTable = /*@__PURE__*/ create.createMotionComponent("table");
|
||||
const MotionTbody = /*@__PURE__*/ create.createMotionComponent("tbody");
|
||||
const MotionTd = /*@__PURE__*/ create.createMotionComponent("td");
|
||||
const MotionTextarea = /*@__PURE__*/ create.createMotionComponent("textarea");
|
||||
const MotionTfoot = /*@__PURE__*/ create.createMotionComponent("tfoot");
|
||||
const MotionTh = /*@__PURE__*/ create.createMotionComponent("th");
|
||||
const MotionThead = /*@__PURE__*/ create.createMotionComponent("thead");
|
||||
const MotionTime = /*@__PURE__*/ create.createMotionComponent("time");
|
||||
const MotionTitle = /*@__PURE__*/ create.createMotionComponent("title");
|
||||
const MotionTr = /*@__PURE__*/ create.createMotionComponent("tr");
|
||||
const MotionTrack = /*@__PURE__*/ create.createMotionComponent("track");
|
||||
const MotionU = /*@__PURE__*/ create.createMotionComponent("u");
|
||||
const MotionUl = /*@__PURE__*/ create.createMotionComponent("ul");
|
||||
const MotionVideo = /*@__PURE__*/ create.createMotionComponent("video");
|
||||
const MotionWbr = /*@__PURE__*/ create.createMotionComponent("wbr");
|
||||
const MotionWebview = /*@__PURE__*/ create.createMotionComponent("webview");
|
||||
/**
|
||||
* SVG components
|
||||
*/
|
||||
const MotionAnimate = /*@__PURE__*/ create.createMotionComponent("animate");
|
||||
const MotionCircle = /*@__PURE__*/ create.createMotionComponent("circle");
|
||||
const MotionDefs = /*@__PURE__*/ create.createMotionComponent("defs");
|
||||
const MotionDesc = /*@__PURE__*/ create.createMotionComponent("desc");
|
||||
const MotionEllipse = /*@__PURE__*/ create.createMotionComponent("ellipse");
|
||||
const MotionG = /*@__PURE__*/ create.createMotionComponent("g");
|
||||
const MotionImage = /*@__PURE__*/ create.createMotionComponent("image");
|
||||
const MotionLine = /*@__PURE__*/ create.createMotionComponent("line");
|
||||
const MotionFilter = /*@__PURE__*/ create.createMotionComponent("filter");
|
||||
const MotionMarker = /*@__PURE__*/ create.createMotionComponent("marker");
|
||||
const MotionMask = /*@__PURE__*/ create.createMotionComponent("mask");
|
||||
const MotionMetadata = /*@__PURE__*/ create.createMotionComponent("metadata");
|
||||
const MotionPath = /*@__PURE__*/ create.createMotionComponent("path");
|
||||
const MotionPattern = /*@__PURE__*/ create.createMotionComponent("pattern");
|
||||
const MotionPolygon = /*@__PURE__*/ create.createMotionComponent("polygon");
|
||||
const MotionPolyline = /*@__PURE__*/ create.createMotionComponent("polyline");
|
||||
const MotionRect = /*@__PURE__*/ create.createMotionComponent("rect");
|
||||
const MotionStop = /*@__PURE__*/ create.createMotionComponent("stop");
|
||||
const MotionSvg = /*@__PURE__*/ create.createMotionComponent("svg");
|
||||
const MotionSymbol = /*@__PURE__*/ create.createMotionComponent("symbol");
|
||||
const MotionText = /*@__PURE__*/ create.createMotionComponent("text");
|
||||
const MotionTspan = /*@__PURE__*/ create.createMotionComponent("tspan");
|
||||
const MotionUse = /*@__PURE__*/ create.createMotionComponent("use");
|
||||
const MotionView = /*@__PURE__*/ create.createMotionComponent("view");
|
||||
const MotionClipPath = /*@__PURE__*/ create.createMotionComponent("clipPath");
|
||||
const MotionFeBlend = /*@__PURE__*/ create.createMotionComponent("feBlend");
|
||||
const MotionFeColorMatrix =
|
||||
/*@__PURE__*/ create.createMotionComponent("feColorMatrix");
|
||||
const MotionFeComponentTransfer = /*@__PURE__*/ create.createMotionComponent("feComponentTransfer");
|
||||
const MotionFeComposite =
|
||||
/*@__PURE__*/ create.createMotionComponent("feComposite");
|
||||
const MotionFeConvolveMatrix =
|
||||
/*@__PURE__*/ create.createMotionComponent("feConvolveMatrix");
|
||||
const MotionFeDiffuseLighting =
|
||||
/*@__PURE__*/ create.createMotionComponent("feDiffuseLighting");
|
||||
const MotionFeDisplacementMap =
|
||||
/*@__PURE__*/ create.createMotionComponent("feDisplacementMap");
|
||||
const MotionFeDistantLight =
|
||||
/*@__PURE__*/ create.createMotionComponent("feDistantLight");
|
||||
const MotionFeDropShadow =
|
||||
/*@__PURE__*/ create.createMotionComponent("feDropShadow");
|
||||
const MotionFeFlood = /*@__PURE__*/ create.createMotionComponent("feFlood");
|
||||
const MotionFeFuncA = /*@__PURE__*/ create.createMotionComponent("feFuncA");
|
||||
const MotionFeFuncB = /*@__PURE__*/ create.createMotionComponent("feFuncB");
|
||||
const MotionFeFuncG = /*@__PURE__*/ create.createMotionComponent("feFuncG");
|
||||
const MotionFeFuncR = /*@__PURE__*/ create.createMotionComponent("feFuncR");
|
||||
const MotionFeGaussianBlur =
|
||||
/*@__PURE__*/ create.createMotionComponent("feGaussianBlur");
|
||||
const MotionFeImage = /*@__PURE__*/ create.createMotionComponent("feImage");
|
||||
const MotionFeMerge = /*@__PURE__*/ create.createMotionComponent("feMerge");
|
||||
const MotionFeMergeNode =
|
||||
/*@__PURE__*/ create.createMotionComponent("feMergeNode");
|
||||
const MotionFeMorphology =
|
||||
/*@__PURE__*/ create.createMotionComponent("feMorphology");
|
||||
const MotionFeOffset = /*@__PURE__*/ create.createMotionComponent("feOffset");
|
||||
const MotionFePointLight =
|
||||
/*@__PURE__*/ create.createMotionComponent("fePointLight");
|
||||
const MotionFeSpecularLighting =
|
||||
/*@__PURE__*/ create.createMotionComponent("feSpecularLighting");
|
||||
const MotionFeSpotLight =
|
||||
/*@__PURE__*/ create.createMotionComponent("feSpotLight");
|
||||
const MotionFeTile = /*@__PURE__*/ create.createMotionComponent("feTile");
|
||||
const MotionFeTurbulence =
|
||||
/*@__PURE__*/ create.createMotionComponent("feTurbulence");
|
||||
const MotionForeignObject =
|
||||
/*@__PURE__*/ create.createMotionComponent("foreignObject");
|
||||
const MotionLinearGradient =
|
||||
/*@__PURE__*/ create.createMotionComponent("linearGradient");
|
||||
const MotionRadialGradient =
|
||||
/*@__PURE__*/ create.createMotionComponent("radialGradient");
|
||||
const MotionTextPath = /*@__PURE__*/ create.createMotionComponent("textPath");
|
||||
|
||||
exports.create = create.createMotionComponent;
|
||||
exports.a = MotionA;
|
||||
exports.abbr = MotionAbbr;
|
||||
exports.address = MotionAddress;
|
||||
exports.animate = MotionAnimate;
|
||||
exports.area = MotionArea;
|
||||
exports.article = MotionArticle;
|
||||
exports.aside = MotionAside;
|
||||
exports.audio = MotionAudio;
|
||||
exports.b = MotionB;
|
||||
exports.base = MotionBase;
|
||||
exports.bdi = MotionBdi;
|
||||
exports.bdo = MotionBdo;
|
||||
exports.big = MotionBig;
|
||||
exports.blockquote = MotionBlockquote;
|
||||
exports.body = MotionBody;
|
||||
exports.button = MotionButton;
|
||||
exports.canvas = MotionCanvas;
|
||||
exports.caption = MotionCaption;
|
||||
exports.circle = MotionCircle;
|
||||
exports.cite = MotionCite;
|
||||
exports.clipPath = MotionClipPath;
|
||||
exports.code = MotionCode;
|
||||
exports.col = MotionCol;
|
||||
exports.colgroup = MotionColgroup;
|
||||
exports.data = MotionData;
|
||||
exports.datalist = MotionDatalist;
|
||||
exports.dd = MotionDd;
|
||||
exports.defs = MotionDefs;
|
||||
exports.del = MotionDel;
|
||||
exports.desc = MotionDesc;
|
||||
exports.details = MotionDetails;
|
||||
exports.dfn = MotionDfn;
|
||||
exports.dialog = MotionDialog;
|
||||
exports.div = MotionDiv;
|
||||
exports.dl = MotionDl;
|
||||
exports.dt = MotionDt;
|
||||
exports.ellipse = MotionEllipse;
|
||||
exports.em = MotionEm;
|
||||
exports.embed = MotionEmbed;
|
||||
exports.feBlend = MotionFeBlend;
|
||||
exports.feColorMatrix = MotionFeColorMatrix;
|
||||
exports.feComponentTransfer = MotionFeComponentTransfer;
|
||||
exports.feComposite = MotionFeComposite;
|
||||
exports.feConvolveMatrix = MotionFeConvolveMatrix;
|
||||
exports.feDiffuseLighting = MotionFeDiffuseLighting;
|
||||
exports.feDisplacementMap = MotionFeDisplacementMap;
|
||||
exports.feDistantLight = MotionFeDistantLight;
|
||||
exports.feDropShadow = MotionFeDropShadow;
|
||||
exports.feFlood = MotionFeFlood;
|
||||
exports.feFuncA = MotionFeFuncA;
|
||||
exports.feFuncB = MotionFeFuncB;
|
||||
exports.feFuncG = MotionFeFuncG;
|
||||
exports.feFuncR = MotionFeFuncR;
|
||||
exports.feGaussianBlur = MotionFeGaussianBlur;
|
||||
exports.feImage = MotionFeImage;
|
||||
exports.feMerge = MotionFeMerge;
|
||||
exports.feMergeNode = MotionFeMergeNode;
|
||||
exports.feMorphology = MotionFeMorphology;
|
||||
exports.feOffset = MotionFeOffset;
|
||||
exports.fePointLight = MotionFePointLight;
|
||||
exports.feSpecularLighting = MotionFeSpecularLighting;
|
||||
exports.feSpotLight = MotionFeSpotLight;
|
||||
exports.feTile = MotionFeTile;
|
||||
exports.feTurbulence = MotionFeTurbulence;
|
||||
exports.fieldset = MotionFieldset;
|
||||
exports.figcaption = MotionFigcaption;
|
||||
exports.figure = MotionFigure;
|
||||
exports.filter = MotionFilter;
|
||||
exports.footer = MotionFooter;
|
||||
exports.foreignObject = MotionForeignObject;
|
||||
exports.form = MotionForm;
|
||||
exports.g = MotionG;
|
||||
exports.h1 = MotionH1;
|
||||
exports.h2 = MotionH2;
|
||||
exports.h3 = MotionH3;
|
||||
exports.h4 = MotionH4;
|
||||
exports.h5 = MotionH5;
|
||||
exports.h6 = MotionH6;
|
||||
exports.head = MotionHead;
|
||||
exports.header = MotionHeader;
|
||||
exports.hgroup = MotionHgroup;
|
||||
exports.hr = MotionHr;
|
||||
exports.html = MotionHtml;
|
||||
exports.i = MotionI;
|
||||
exports.iframe = MotionIframe;
|
||||
exports.image = MotionImage;
|
||||
exports.img = MotionImg;
|
||||
exports.input = MotionInput;
|
||||
exports.ins = MotionIns;
|
||||
exports.kbd = MotionKbd;
|
||||
exports.keygen = MotionKeygen;
|
||||
exports.label = MotionLabel;
|
||||
exports.legend = MotionLegend;
|
||||
exports.li = MotionLi;
|
||||
exports.line = MotionLine;
|
||||
exports.linearGradient = MotionLinearGradient;
|
||||
exports.link = MotionLink;
|
||||
exports.main = MotionMain;
|
||||
exports.map = MotionMap;
|
||||
exports.mark = MotionMark;
|
||||
exports.marker = MotionMarker;
|
||||
exports.mask = MotionMask;
|
||||
exports.menu = MotionMenu;
|
||||
exports.menuitem = MotionMenuitem;
|
||||
exports.metadata = MotionMetadata;
|
||||
exports.meter = MotionMeter;
|
||||
exports.nav = MotionNav;
|
||||
exports.object = MotionObject;
|
||||
exports.ol = MotionOl;
|
||||
exports.optgroup = MotionOptgroup;
|
||||
exports.option = MotionOption;
|
||||
exports.output = MotionOutput;
|
||||
exports.p = MotionP;
|
||||
exports.param = MotionParam;
|
||||
exports.path = MotionPath;
|
||||
exports.pattern = MotionPattern;
|
||||
exports.picture = MotionPicture;
|
||||
exports.polygon = MotionPolygon;
|
||||
exports.polyline = MotionPolyline;
|
||||
exports.pre = MotionPre;
|
||||
exports.progress = MotionProgress;
|
||||
exports.q = MotionQ;
|
||||
exports.radialGradient = MotionRadialGradient;
|
||||
exports.rect = MotionRect;
|
||||
exports.rp = MotionRp;
|
||||
exports.rt = MotionRt;
|
||||
exports.ruby = MotionRuby;
|
||||
exports.s = MotionS;
|
||||
exports.samp = MotionSamp;
|
||||
exports.script = MotionScript;
|
||||
exports.section = MotionSection;
|
||||
exports.select = MotionSelect;
|
||||
exports.small = MotionSmall;
|
||||
exports.source = MotionSource;
|
||||
exports.span = MotionSpan;
|
||||
exports.stop = MotionStop;
|
||||
exports.strong = MotionStrong;
|
||||
exports.style = MotionStyle;
|
||||
exports.sub = MotionSub;
|
||||
exports.summary = MotionSummary;
|
||||
exports.sup = MotionSup;
|
||||
exports.svg = MotionSvg;
|
||||
exports.symbol = MotionSymbol;
|
||||
exports.table = MotionTable;
|
||||
exports.tbody = MotionTbody;
|
||||
exports.td = MotionTd;
|
||||
exports.text = MotionText;
|
||||
exports.textPath = MotionTextPath;
|
||||
exports.textarea = MotionTextarea;
|
||||
exports.tfoot = MotionTfoot;
|
||||
exports.th = MotionTh;
|
||||
exports.thead = MotionThead;
|
||||
exports.time = MotionTime;
|
||||
exports.title = MotionTitle;
|
||||
exports.tr = MotionTr;
|
||||
exports.track = MotionTrack;
|
||||
exports.tspan = MotionTspan;
|
||||
exports.u = MotionU;
|
||||
exports.ul = MotionUl;
|
||||
exports.use = MotionUse;
|
||||
exports.video = MotionVideo;
|
||||
exports.view = MotionView;
|
||||
exports.wbr = MotionWbr;
|
||||
exports.webview = MotionWebview;
|
||||
-6267
File diff suppressed because it is too large
Load Diff
-12
@@ -1,12 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var motionDom = require('motion-dom');
|
||||
|
||||
|
||||
|
||||
Object.defineProperty(exports, "recordStats", {
|
||||
enumerable: true,
|
||||
get: function () { return motionDom.recordStats; }
|
||||
});
|
||||
-461
@@ -1,461 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var motionDom = require('motion-dom');
|
||||
var motionUtils = require('motion-utils');
|
||||
|
||||
function isDOMKeyframes(keyframes) {
|
||||
return typeof keyframes === "object" && !Array.isArray(keyframes);
|
||||
}
|
||||
|
||||
function resolveSubjects(subject, keyframes, scope, selectorCache) {
|
||||
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
|
||||
return motionDom.resolveElements(subject, scope, selectorCache);
|
||||
}
|
||||
else if (subject instanceof NodeList) {
|
||||
return Array.from(subject);
|
||||
}
|
||||
else if (Array.isArray(subject)) {
|
||||
return subject;
|
||||
}
|
||||
else {
|
||||
return [subject];
|
||||
}
|
||||
}
|
||||
|
||||
function calculateRepeatDuration(duration, repeat, _repeatDelay) {
|
||||
return duration * (repeat + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a absolute or relative time definition and current/prev time state of the sequence,
|
||||
* calculate an absolute time for the next keyframes.
|
||||
*/
|
||||
function calcNextTime(current, next, prev, labels) {
|
||||
if (typeof next === "number") {
|
||||
return next;
|
||||
}
|
||||
else if (next.startsWith("-") || next.startsWith("+")) {
|
||||
return Math.max(0, current + parseFloat(next));
|
||||
}
|
||||
else if (next === "<") {
|
||||
return prev;
|
||||
}
|
||||
else if (next.startsWith("<")) {
|
||||
return Math.max(0, prev + parseFloat(next.slice(1)));
|
||||
}
|
||||
else {
|
||||
return labels.get(next) ?? current;
|
||||
}
|
||||
}
|
||||
|
||||
function eraseKeyframes(sequence, startTime, endTime) {
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const keyframe = sequence[i];
|
||||
if (keyframe.at > startTime && keyframe.at < endTime) {
|
||||
motionUtils.removeItem(sequence, keyframe);
|
||||
// If we remove this item we have to push the pointer back one
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
|
||||
/**
|
||||
* Erase every existing value between currentTime and targetTime,
|
||||
* this will essentially splice this timeline into any currently
|
||||
* defined ones.
|
||||
*/
|
||||
eraseKeyframes(sequence, startTime, endTime);
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
sequence.push({
|
||||
value: keyframes[i],
|
||||
at: motionDom.mixNumber(startTime, endTime, offset[i]),
|
||||
easing: motionUtils.getEasingForSegment(easing, i),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Take an array of times that represent repeated keyframes. For instance
|
||||
* if we have original times of [0, 0.5, 1] then our repeated times will
|
||||
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
|
||||
* down to a 0-1 scale.
|
||||
*/
|
||||
function normalizeTimes(times, repeat) {
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
times[i] = times[i] / (repeat + 1);
|
||||
}
|
||||
}
|
||||
|
||||
function compareByTime(a, b) {
|
||||
if (a.at === b.at) {
|
||||
if (a.value === null)
|
||||
return 1;
|
||||
if (b.value === null)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return a.at - b.at;
|
||||
}
|
||||
}
|
||||
|
||||
const defaultSegmentEasing = "easeInOut";
|
||||
const MAX_REPEAT = 20;
|
||||
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
|
||||
const defaultDuration = defaultTransition.duration || 0.3;
|
||||
const animationDefinitions = new Map();
|
||||
const sequences = new Map();
|
||||
const elementCache = {};
|
||||
const timeLabels = new Map();
|
||||
let prevTime = 0;
|
||||
let currentTime = 0;
|
||||
let totalDuration = 0;
|
||||
/**
|
||||
* Build the timeline by mapping over the sequence array and converting
|
||||
* the definitions into keyframes and offsets with absolute time values.
|
||||
* These will later get converted into relative offsets in a second pass.
|
||||
*/
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const segment = sequence[i];
|
||||
/**
|
||||
* If this is a timeline label, mark it and skip the rest of this iteration.
|
||||
*/
|
||||
if (typeof segment === "string") {
|
||||
timeLabels.set(segment, currentTime);
|
||||
continue;
|
||||
}
|
||||
else if (!Array.isArray(segment)) {
|
||||
timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
|
||||
continue;
|
||||
}
|
||||
let [subject, keyframes, transition = {}] = segment;
|
||||
/**
|
||||
* If a relative or absolute time value has been specified we need to resolve
|
||||
* it in relation to the currentTime.
|
||||
*/
|
||||
if (transition.at !== undefined) {
|
||||
currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
|
||||
}
|
||||
/**
|
||||
* Keep track of the maximum duration in this definition. This will be
|
||||
* applied to currentTime once the definition has been parsed.
|
||||
*/
|
||||
let maxDuration = 0;
|
||||
const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
|
||||
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
|
||||
const { delay = 0, times = motionDom.defaultOffset(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
|
||||
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
|
||||
/**
|
||||
* Resolve stagger() if defined.
|
||||
*/
|
||||
const calculatedDelay = typeof delay === "function"
|
||||
? delay(elementIndex, numSubjects)
|
||||
: delay;
|
||||
/**
|
||||
* If this animation should and can use a spring, generate a spring easing function.
|
||||
*/
|
||||
const numKeyframes = valueKeyframesAsList.length;
|
||||
const createGenerator = motionDom.isGenerator(type)
|
||||
? type
|
||||
: generators?.[type || "keyframes"];
|
||||
if (numKeyframes <= 2 && createGenerator) {
|
||||
/**
|
||||
* As we're creating an easing function from a spring,
|
||||
* ideally we want to generate it using the real distance
|
||||
* between the two keyframes. However this isn't always
|
||||
* possible - in these situations we use 0-100.
|
||||
*/
|
||||
let absoluteDelta = 100;
|
||||
if (numKeyframes === 2 &&
|
||||
isNumberKeyframesArray(valueKeyframesAsList)) {
|
||||
const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
|
||||
absoluteDelta = Math.abs(delta);
|
||||
}
|
||||
const springTransition = { ...remainingTransition };
|
||||
if (duration !== undefined) {
|
||||
springTransition.duration = motionUtils.secondsToMilliseconds(duration);
|
||||
}
|
||||
const springEasing = motionDom.createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
|
||||
ease = springEasing.ease;
|
||||
duration = springEasing.duration;
|
||||
}
|
||||
duration ?? (duration = defaultDuration);
|
||||
const startTime = currentTime + calculatedDelay;
|
||||
/**
|
||||
* If there's only one time offset of 0, fill in a second with length 1
|
||||
*/
|
||||
if (times.length === 1 && times[0] === 0) {
|
||||
times[1] = 1;
|
||||
}
|
||||
/**
|
||||
* Fill out if offset if fewer offsets than keyframes
|
||||
*/
|
||||
const remainder = times.length - valueKeyframesAsList.length;
|
||||
remainder > 0 && motionDom.fillOffset(times, remainder);
|
||||
/**
|
||||
* If only one value has been set, ie [1], push a null to the start of
|
||||
* the keyframe array. This will let us mark a keyframe at this point
|
||||
* that will later be hydrated with the previous value.
|
||||
*/
|
||||
valueKeyframesAsList.length === 1 &&
|
||||
valueKeyframesAsList.unshift(null);
|
||||
/**
|
||||
* Handle repeat options
|
||||
*/
|
||||
if (repeat) {
|
||||
motionUtils.invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
|
||||
duration = calculateRepeatDuration(duration, repeat);
|
||||
const originalKeyframes = [...valueKeyframesAsList];
|
||||
const originalTimes = [...times];
|
||||
ease = Array.isArray(ease) ? [...ease] : [ease];
|
||||
const originalEase = [...ease];
|
||||
for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
|
||||
valueKeyframesAsList.push(...originalKeyframes);
|
||||
for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
|
||||
times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
|
||||
ease.push(keyframeIndex === 0
|
||||
? "linear"
|
||||
: motionUtils.getEasingForSegment(originalEase, keyframeIndex - 1));
|
||||
}
|
||||
}
|
||||
normalizeTimes(times, repeat);
|
||||
}
|
||||
const targetTime = startTime + duration;
|
||||
/**
|
||||
* Add keyframes, mapping offsets to absolute time.
|
||||
*/
|
||||
addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
|
||||
maxDuration = Math.max(calculatedDelay + duration, maxDuration);
|
||||
totalDuration = Math.max(targetTime, totalDuration);
|
||||
};
|
||||
if (motionDom.isMotionValue(subject)) {
|
||||
const subjectSequence = getSubjectSequence(subject, sequences);
|
||||
resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
|
||||
}
|
||||
else {
|
||||
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
|
||||
const numSubjects = subjects.length;
|
||||
/**
|
||||
* For every element in this segment, process the defined values.
|
||||
*/
|
||||
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
|
||||
/**
|
||||
* Cast necessary, but we know these are of this type
|
||||
*/
|
||||
keyframes = keyframes;
|
||||
transition = transition;
|
||||
const thisSubject = subjects[subjectIndex];
|
||||
const subjectSequence = getSubjectSequence(thisSubject, sequences);
|
||||
for (const key in keyframes) {
|
||||
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
prevTime = currentTime;
|
||||
currentTime += maxDuration;
|
||||
}
|
||||
/**
|
||||
* For every element and value combination create a new animation.
|
||||
*/
|
||||
sequences.forEach((valueSequences, element) => {
|
||||
for (const key in valueSequences) {
|
||||
const valueSequence = valueSequences[key];
|
||||
/**
|
||||
* Arrange all the keyframes in ascending time order.
|
||||
*/
|
||||
valueSequence.sort(compareByTime);
|
||||
const keyframes = [];
|
||||
const valueOffset = [];
|
||||
const valueEasing = [];
|
||||
/**
|
||||
* For each keyframe, translate absolute times into
|
||||
* relative offsets based on the total duration of the timeline.
|
||||
*/
|
||||
for (let i = 0; i < valueSequence.length; i++) {
|
||||
const { at, value, easing } = valueSequence[i];
|
||||
keyframes.push(value);
|
||||
valueOffset.push(motionUtils.progress(0, totalDuration, at));
|
||||
valueEasing.push(easing || "easeOut");
|
||||
}
|
||||
/**
|
||||
* If the first keyframe doesn't land on offset: 0
|
||||
* provide one by duplicating the initial keyframe. This ensures
|
||||
* it snaps to the first keyframe when the animation starts.
|
||||
*/
|
||||
if (valueOffset[0] !== 0) {
|
||||
valueOffset.unshift(0);
|
||||
keyframes.unshift(keyframes[0]);
|
||||
valueEasing.unshift(defaultSegmentEasing);
|
||||
}
|
||||
/**
|
||||
* If the last keyframe doesn't land on offset: 1
|
||||
* provide one with a null wildcard value. This will ensure it
|
||||
* stays static until the end of the animation.
|
||||
*/
|
||||
if (valueOffset[valueOffset.length - 1] !== 1) {
|
||||
valueOffset.push(1);
|
||||
keyframes.push(null);
|
||||
}
|
||||
if (!animationDefinitions.has(element)) {
|
||||
animationDefinitions.set(element, {
|
||||
keyframes: {},
|
||||
transition: {},
|
||||
});
|
||||
}
|
||||
const definition = animationDefinitions.get(element);
|
||||
definition.keyframes[key] = keyframes;
|
||||
definition.transition[key] = {
|
||||
...defaultTransition,
|
||||
duration: totalDuration,
|
||||
ease: valueEasing,
|
||||
times: valueOffset,
|
||||
...sequenceTransition,
|
||||
};
|
||||
}
|
||||
});
|
||||
return animationDefinitions;
|
||||
}
|
||||
function getSubjectSequence(subject, sequences) {
|
||||
!sequences.has(subject) && sequences.set(subject, {});
|
||||
return sequences.get(subject);
|
||||
}
|
||||
function getValueSequence(name, sequences) {
|
||||
if (!sequences[name])
|
||||
sequences[name] = [];
|
||||
return sequences[name];
|
||||
}
|
||||
function keyframesAsList(keyframes) {
|
||||
return Array.isArray(keyframes) ? keyframes : [keyframes];
|
||||
}
|
||||
function getValueTransition(transition, key) {
|
||||
return transition && transition[key]
|
||||
? {
|
||||
...transition,
|
||||
...transition[key],
|
||||
}
|
||||
: { ...transition };
|
||||
}
|
||||
const isNumber = (keyframe) => typeof keyframe === "number";
|
||||
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
|
||||
|
||||
function animateElements(elementOrSelector, keyframes, options, scope) {
|
||||
const elements = motionDom.resolveElements(elementOrSelector, scope);
|
||||
const numElements = elements.length;
|
||||
motionUtils.invariant(Boolean(numElements), "No valid element provided.");
|
||||
/**
|
||||
* WAAPI doesn't support interrupting animations.
|
||||
*
|
||||
* Therefore, starting animations requires a three-step process:
|
||||
* 1. Stop existing animations (write styles to DOM)
|
||||
* 2. Resolve keyframes (read styles from DOM)
|
||||
* 3. Create new animations (write styles to DOM)
|
||||
*
|
||||
* The hybrid `animate()` function uses AsyncAnimation to resolve
|
||||
* keyframes before creating new animations, which removes style
|
||||
* thrashing. Here, we have much stricter filesize constraints.
|
||||
* Therefore we do this in a synchronous way that ensures that
|
||||
* at least within `animate()` calls there is no style thrashing.
|
||||
*
|
||||
* In the motion-native-animate-mini-interrupt benchmark this
|
||||
* was 80% faster than a single loop.
|
||||
*/
|
||||
const animationDefinitions = [];
|
||||
/**
|
||||
* Step 1: Build options and stop existing animations (write)
|
||||
*/
|
||||
for (let i = 0; i < numElements; i++) {
|
||||
const element = elements[i];
|
||||
const elementTransition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if (typeof elementTransition.delay === "function") {
|
||||
elementTransition.delay = elementTransition.delay(i, numElements);
|
||||
}
|
||||
for (const valueName in keyframes) {
|
||||
let valueKeyframes = keyframes[valueName];
|
||||
if (!Array.isArray(valueKeyframes)) {
|
||||
valueKeyframes = [valueKeyframes];
|
||||
}
|
||||
const valueOptions = {
|
||||
...motionDom.getValueTransition(elementTransition, valueName),
|
||||
};
|
||||
valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration));
|
||||
valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay));
|
||||
/**
|
||||
* If there's an existing animation playing on this element then stop it
|
||||
* before creating a new one.
|
||||
*/
|
||||
const map = motionDom.getAnimationMap(element);
|
||||
const key = motionDom.animationMapKey(valueName, valueOptions.pseudoElement || "");
|
||||
const currentAnimation = map.get(key);
|
||||
currentAnimation && currentAnimation.stop();
|
||||
animationDefinitions.push({
|
||||
map,
|
||||
key,
|
||||
unresolvedKeyframes: valueKeyframes,
|
||||
options: {
|
||||
...valueOptions,
|
||||
element,
|
||||
name: valueName,
|
||||
allowFlatten: !elementTransition.type && !elementTransition.ease,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Step 2: Resolve keyframes (read)
|
||||
*/
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i];
|
||||
const { element, name, pseudoElement } = animationOptions;
|
||||
if (!pseudoElement && unresolvedKeyframes[0] === null) {
|
||||
unresolvedKeyframes[0] = motionDom.getComputedStyle(element, name);
|
||||
}
|
||||
motionDom.fillWildcards(unresolvedKeyframes);
|
||||
motionDom.applyPxDefaults(unresolvedKeyframes, name);
|
||||
/**
|
||||
* If we only have one keyframe, explicitly read the initial keyframe
|
||||
* from the computed style. This is to ensure consistency with WAAPI behaviour
|
||||
* for restarting animations, for instance .play() after finish, when it
|
||||
* has one vs two keyframes.
|
||||
*/
|
||||
if (!pseudoElement && unresolvedKeyframes.length < 2) {
|
||||
unresolvedKeyframes.unshift(motionDom.getComputedStyle(element, name));
|
||||
}
|
||||
animationOptions.keyframes = unresolvedKeyframes;
|
||||
}
|
||||
/**
|
||||
* Step 3: Create new animations (write)
|
||||
*/
|
||||
const animations = [];
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { map, key, options: animationOptions } = animationDefinitions[i];
|
||||
const animation = new motionDom.NativeAnimation(animationOptions);
|
||||
map.set(key, animation);
|
||||
animation.finished.finally(() => map.delete(key));
|
||||
animations.push(animation);
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
function animateSequence(definition, options) {
|
||||
const animations = [];
|
||||
createAnimationsFromSequence(definition, options).forEach(({ keyframes, transition }, element) => {
|
||||
animations.push(...animateElements(element, keyframes, transition));
|
||||
});
|
||||
return new motionDom.GroupAnimationWithThen(animations);
|
||||
}
|
||||
|
||||
const createScopedWaapiAnimate = (scope) => {
|
||||
function scopedAnimate(elementOrSelector, keyframes, options) {
|
||||
return new motionDom.GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
|
||||
}
|
||||
return scopedAnimate;
|
||||
};
|
||||
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
|
||||
|
||||
exports.animate = animateMini;
|
||||
exports.animateSequence = animateSequence;
|
||||
-2471
File diff suppressed because it is too large
Load Diff
-2939
File diff suppressed because it is too large
Load Diff
-1444
File diff suppressed because it is too large
Load Diff
-148
@@ -1,148 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
Object.defineProperty(exports, '__esModule', { value: true });
|
||||
|
||||
var react = require('react');
|
||||
var motionDom = require('motion-dom');
|
||||
var motionUtils = require('motion-utils');
|
||||
|
||||
/**
|
||||
* Creates a constant value over the lifecycle of a component.
|
||||
*
|
||||
* Even if `useMemo` is provided an empty array as its final argument, it doesn't offer
|
||||
* a guarantee that it won't re-run for performance reasons later on. By using `useConstant`
|
||||
* you can ensure that initialisers don't execute twice or more.
|
||||
*/
|
||||
function useConstant(init) {
|
||||
const ref = react.useRef(null);
|
||||
if (ref.current === null) {
|
||||
ref.current = init();
|
||||
}
|
||||
return ref.current;
|
||||
}
|
||||
|
||||
function useUnmountEffect(callback) {
|
||||
return react.useEffect(() => () => callback(), []);
|
||||
}
|
||||
|
||||
function animateElements(elementOrSelector, keyframes, options, scope) {
|
||||
const elements = motionDom.resolveElements(elementOrSelector, scope);
|
||||
const numElements = elements.length;
|
||||
motionUtils.invariant(Boolean(numElements), "No valid element provided.");
|
||||
/**
|
||||
* WAAPI doesn't support interrupting animations.
|
||||
*
|
||||
* Therefore, starting animations requires a three-step process:
|
||||
* 1. Stop existing animations (write styles to DOM)
|
||||
* 2. Resolve keyframes (read styles from DOM)
|
||||
* 3. Create new animations (write styles to DOM)
|
||||
*
|
||||
* The hybrid `animate()` function uses AsyncAnimation to resolve
|
||||
* keyframes before creating new animations, which removes style
|
||||
* thrashing. Here, we have much stricter filesize constraints.
|
||||
* Therefore we do this in a synchronous way that ensures that
|
||||
* at least within `animate()` calls there is no style thrashing.
|
||||
*
|
||||
* In the motion-native-animate-mini-interrupt benchmark this
|
||||
* was 80% faster than a single loop.
|
||||
*/
|
||||
const animationDefinitions = [];
|
||||
/**
|
||||
* Step 1: Build options and stop existing animations (write)
|
||||
*/
|
||||
for (let i = 0; i < numElements; i++) {
|
||||
const element = elements[i];
|
||||
const elementTransition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if (typeof elementTransition.delay === "function") {
|
||||
elementTransition.delay = elementTransition.delay(i, numElements);
|
||||
}
|
||||
for (const valueName in keyframes) {
|
||||
let valueKeyframes = keyframes[valueName];
|
||||
if (!Array.isArray(valueKeyframes)) {
|
||||
valueKeyframes = [valueKeyframes];
|
||||
}
|
||||
const valueOptions = {
|
||||
...motionDom.getValueTransition(elementTransition, valueName),
|
||||
};
|
||||
valueOptions.duration && (valueOptions.duration = motionUtils.secondsToMilliseconds(valueOptions.duration));
|
||||
valueOptions.delay && (valueOptions.delay = motionUtils.secondsToMilliseconds(valueOptions.delay));
|
||||
/**
|
||||
* If there's an existing animation playing on this element then stop it
|
||||
* before creating a new one.
|
||||
*/
|
||||
const map = motionDom.getAnimationMap(element);
|
||||
const key = motionDom.animationMapKey(valueName, valueOptions.pseudoElement || "");
|
||||
const currentAnimation = map.get(key);
|
||||
currentAnimation && currentAnimation.stop();
|
||||
animationDefinitions.push({
|
||||
map,
|
||||
key,
|
||||
unresolvedKeyframes: valueKeyframes,
|
||||
options: {
|
||||
...valueOptions,
|
||||
element,
|
||||
name: valueName,
|
||||
allowFlatten: !elementTransition.type && !elementTransition.ease,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Step 2: Resolve keyframes (read)
|
||||
*/
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i];
|
||||
const { element, name, pseudoElement } = animationOptions;
|
||||
if (!pseudoElement && unresolvedKeyframes[0] === null) {
|
||||
unresolvedKeyframes[0] = motionDom.getComputedStyle(element, name);
|
||||
}
|
||||
motionDom.fillWildcards(unresolvedKeyframes);
|
||||
motionDom.applyPxDefaults(unresolvedKeyframes, name);
|
||||
/**
|
||||
* If we only have one keyframe, explicitly read the initial keyframe
|
||||
* from the computed style. This is to ensure consistency with WAAPI behaviour
|
||||
* for restarting animations, for instance .play() after finish, when it
|
||||
* has one vs two keyframes.
|
||||
*/
|
||||
if (!pseudoElement && unresolvedKeyframes.length < 2) {
|
||||
unresolvedKeyframes.unshift(motionDom.getComputedStyle(element, name));
|
||||
}
|
||||
animationOptions.keyframes = unresolvedKeyframes;
|
||||
}
|
||||
/**
|
||||
* Step 3: Create new animations (write)
|
||||
*/
|
||||
const animations = [];
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { map, key, options: animationOptions } = animationDefinitions[i];
|
||||
const animation = new motionDom.NativeAnimation(animationOptions);
|
||||
map.set(key, animation);
|
||||
animation.finished.finally(() => map.delete(key));
|
||||
animations.push(animation);
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
const createScopedWaapiAnimate = (scope) => {
|
||||
function scopedAnimate(elementOrSelector, keyframes, options) {
|
||||
return new motionDom.GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
|
||||
}
|
||||
return scopedAnimate;
|
||||
};
|
||||
|
||||
function useAnimateMini() {
|
||||
const scope = useConstant(() => ({
|
||||
current: null, // Will be hydrated by React
|
||||
animations: [],
|
||||
}));
|
||||
const animate = useConstant(() => createScopedWaapiAnimate(scope));
|
||||
useUnmountEffect(() => {
|
||||
scope.animations.forEach((animation) => animation.stop());
|
||||
});
|
||||
return [scope, animate];
|
||||
}
|
||||
|
||||
exports.useAnimate = useAnimateMini;
|
||||
-2
@@ -1,2 +0,0 @@
|
||||
export * from 'motion-dom';
|
||||
export { recordStats } from 'motion-dom';
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import { AnimationPlaybackOptions, Transition, MotionValue, UnresolvedValueKeyframe, ElementOrSelector, DOMKeyframesDefinition, AnimationOptions, GroupAnimationWithThen, AnimationPlaybackControlsWithThen } from 'motion-dom';
|
||||
|
||||
type ObjectTarget<O> = {
|
||||
[K in keyof O]?: O[K] | UnresolvedValueKeyframe[];
|
||||
};
|
||||
type SequenceTime = number | "<" | `+${number}` | `-${number}` | `${string}`;
|
||||
type SequenceLabel = string;
|
||||
interface SequenceLabelWithTime {
|
||||
name: SequenceLabel;
|
||||
at: SequenceTime;
|
||||
}
|
||||
interface At {
|
||||
at?: SequenceTime;
|
||||
}
|
||||
type MotionValueSegment = [
|
||||
MotionValue,
|
||||
UnresolvedValueKeyframe | UnresolvedValueKeyframe[]
|
||||
];
|
||||
type MotionValueSegmentWithTransition = [
|
||||
MotionValue,
|
||||
UnresolvedValueKeyframe | UnresolvedValueKeyframe[],
|
||||
Transition & At
|
||||
];
|
||||
type DOMSegment = [ElementOrSelector, DOMKeyframesDefinition];
|
||||
type DOMSegmentWithTransition = [
|
||||
ElementOrSelector,
|
||||
DOMKeyframesDefinition,
|
||||
AnimationOptions & At
|
||||
];
|
||||
type ObjectSegment<O extends {} = {}> = [O, ObjectTarget<O>];
|
||||
type ObjectSegmentWithTransition<O extends {} = {}> = [
|
||||
O,
|
||||
ObjectTarget<O>,
|
||||
AnimationOptions & At
|
||||
];
|
||||
type Segment = ObjectSegment | ObjectSegmentWithTransition | SequenceLabel | SequenceLabelWithTime | MotionValueSegment | MotionValueSegmentWithTransition | DOMSegment | DOMSegmentWithTransition;
|
||||
type AnimationSequence = Segment[];
|
||||
interface SequenceOptions extends AnimationPlaybackOptions {
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
defaultTransition?: Transition;
|
||||
}
|
||||
|
||||
declare function animateSequence(definition: AnimationSequence, options?: SequenceOptions): GroupAnimationWithThen;
|
||||
|
||||
declare const animateMini: (elementOrSelector: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions) => AnimationPlaybackControlsWithThen;
|
||||
|
||||
export { animateMini as animate, animateSequence };
|
||||
-1
File diff suppressed because one or more lines are too long
-151
@@ -1,151 +0,0 @@
|
||||
import { UnresolvedValueKeyframe, MotionValue, Transition, ElementOrSelector, DOMKeyframesDefinition, AnimationOptions, AnimationPlaybackOptions, AnyResolvedKeyframe, AnimationScope, AnimationPlaybackControlsWithThen, ValueAnimationTransition, AnimationPlaybackControls } from 'motion-dom';
|
||||
export * from 'motion-dom';
|
||||
import { Easing, EasingFunction, Point } from 'motion-utils';
|
||||
export * from 'motion-utils';
|
||||
|
||||
type ObjectTarget<O> = {
|
||||
[K in keyof O]?: O[K] | UnresolvedValueKeyframe[];
|
||||
};
|
||||
type SequenceTime = number | "<" | `+${number}` | `-${number}` | `${string}`;
|
||||
type SequenceLabel = string;
|
||||
interface SequenceLabelWithTime {
|
||||
name: SequenceLabel;
|
||||
at: SequenceTime;
|
||||
}
|
||||
interface At {
|
||||
at?: SequenceTime;
|
||||
}
|
||||
type MotionValueSegment = [
|
||||
MotionValue,
|
||||
UnresolvedValueKeyframe | UnresolvedValueKeyframe[]
|
||||
];
|
||||
type MotionValueSegmentWithTransition = [
|
||||
MotionValue,
|
||||
UnresolvedValueKeyframe | UnresolvedValueKeyframe[],
|
||||
Transition & At
|
||||
];
|
||||
type DOMSegment = [ElementOrSelector, DOMKeyframesDefinition];
|
||||
type DOMSegmentWithTransition = [
|
||||
ElementOrSelector,
|
||||
DOMKeyframesDefinition,
|
||||
AnimationOptions & At
|
||||
];
|
||||
type ObjectSegment<O extends {} = {}> = [O, ObjectTarget<O>];
|
||||
type ObjectSegmentWithTransition<O extends {} = {}> = [
|
||||
O,
|
||||
ObjectTarget<O>,
|
||||
AnimationOptions & At
|
||||
];
|
||||
type Segment = ObjectSegment | ObjectSegmentWithTransition | SequenceLabel | SequenceLabelWithTime | MotionValueSegment | MotionValueSegmentWithTransition | DOMSegment | DOMSegmentWithTransition;
|
||||
type AnimationSequence = Segment[];
|
||||
interface SequenceOptions extends AnimationPlaybackOptions {
|
||||
delay?: number;
|
||||
duration?: number;
|
||||
defaultTransition?: Transition;
|
||||
}
|
||||
interface AbsoluteKeyframe {
|
||||
value: AnyResolvedKeyframe | null;
|
||||
at: number;
|
||||
easing?: Easing;
|
||||
}
|
||||
type ValueSequence = AbsoluteKeyframe[];
|
||||
interface SequenceMap {
|
||||
[key: string]: ValueSequence;
|
||||
}
|
||||
type ResolvedAnimationDefinition = {
|
||||
keyframes: {
|
||||
[key: string]: UnresolvedValueKeyframe[];
|
||||
};
|
||||
transition: {
|
||||
[key: string]: Transition;
|
||||
};
|
||||
};
|
||||
type ResolvedAnimationDefinitions = Map<Element | MotionValue, ResolvedAnimationDefinition>;
|
||||
|
||||
/**
|
||||
* Creates an animation function that is optionally scoped
|
||||
* to a specific element.
|
||||
*/
|
||||
declare function createScopedAnimate(scope?: AnimationScope): {
|
||||
(sequence: AnimationSequence, options?: SequenceOptions): AnimationPlaybackControlsWithThen;
|
||||
(value: string | MotionValue<string>, keyframes: string | UnresolvedValueKeyframe<string>[], options?: ValueAnimationTransition<string>): AnimationPlaybackControlsWithThen;
|
||||
(value: number | MotionValue<number>, keyframes: number | UnresolvedValueKeyframe<number>[], options?: ValueAnimationTransition<number>): AnimationPlaybackControlsWithThen;
|
||||
<V extends string | number>(value: V | MotionValue<V>, keyframes: V | UnresolvedValueKeyframe<V>[], options?: ValueAnimationTransition<V>): AnimationPlaybackControlsWithThen;
|
||||
(element: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions): AnimationPlaybackControlsWithThen;
|
||||
<O extends {}>(object: O | O[], keyframes: ObjectTarget<O>, options?: AnimationOptions): AnimationPlaybackControlsWithThen;
|
||||
};
|
||||
declare const animate: {
|
||||
(sequence: AnimationSequence, options?: SequenceOptions): AnimationPlaybackControlsWithThen;
|
||||
(value: string | MotionValue<string>, keyframes: string | UnresolvedValueKeyframe<string>[], options?: ValueAnimationTransition<string>): AnimationPlaybackControlsWithThen;
|
||||
(value: number | MotionValue<number>, keyframes: number | UnresolvedValueKeyframe<number>[], options?: ValueAnimationTransition<number>): AnimationPlaybackControlsWithThen;
|
||||
<V extends string | number>(value: V | MotionValue<V>, keyframes: V | UnresolvedValueKeyframe<V>[], options?: ValueAnimationTransition<V>): AnimationPlaybackControlsWithThen;
|
||||
(element: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions): AnimationPlaybackControlsWithThen;
|
||||
<O extends {}>(object: O | O[], keyframes: ObjectTarget<O>, options?: AnimationOptions): AnimationPlaybackControlsWithThen;
|
||||
};
|
||||
|
||||
declare const animateMini: (elementOrSelector: ElementOrSelector, keyframes: DOMKeyframesDefinition, options?: AnimationOptions) => AnimationPlaybackControlsWithThen;
|
||||
|
||||
interface ScrollOptions {
|
||||
source?: HTMLElement;
|
||||
container?: Element;
|
||||
target?: Element;
|
||||
axis?: "x" | "y";
|
||||
offset?: ScrollOffset;
|
||||
}
|
||||
type OnScrollProgress = (progress: number) => void;
|
||||
type OnScrollWithInfo = (progress: number, info: ScrollInfo) => void;
|
||||
type OnScroll = OnScrollProgress | OnScrollWithInfo;
|
||||
interface AxisScrollInfo {
|
||||
current: number;
|
||||
offset: number[];
|
||||
progress: number;
|
||||
scrollLength: number;
|
||||
velocity: number;
|
||||
targetOffset: number;
|
||||
targetLength: number;
|
||||
containerLength: number;
|
||||
interpolatorOffsets?: number[];
|
||||
interpolate?: EasingFunction;
|
||||
}
|
||||
interface ScrollInfo {
|
||||
time: number;
|
||||
x: AxisScrollInfo;
|
||||
y: AxisScrollInfo;
|
||||
}
|
||||
type OnScrollInfo = (info: ScrollInfo) => void;
|
||||
type SupportedEdgeUnit = "px" | "vw" | "vh" | "%";
|
||||
type EdgeUnit = `${number}${SupportedEdgeUnit}`;
|
||||
type NamedEdges = "start" | "end" | "center";
|
||||
type EdgeString = NamedEdges | EdgeUnit | `${number}`;
|
||||
type Edge = EdgeString | number;
|
||||
type ProgressIntersection = [number, number];
|
||||
type Intersection = `${Edge} ${Edge}`;
|
||||
type ScrollOffset = Array<Edge | Intersection | ProgressIntersection>;
|
||||
interface ScrollInfoOptions {
|
||||
container?: Element;
|
||||
target?: Element;
|
||||
axis?: "x" | "y";
|
||||
offset?: ScrollOffset;
|
||||
}
|
||||
|
||||
declare function scroll(onScroll: OnScroll | AnimationPlaybackControls, { axis, container, ...options }?: ScrollOptions): VoidFunction;
|
||||
|
||||
declare function scrollInfo(onScroll: OnScrollInfo, { container, ...options }?: ScrollInfoOptions): VoidFunction;
|
||||
|
||||
type ViewChangeHandler = (entry: IntersectionObserverEntry) => void;
|
||||
type MarginValue = `${number}${"px" | "%"}`;
|
||||
type MarginType = MarginValue | `${MarginValue} ${MarginValue}` | `${MarginValue} ${MarginValue} ${MarginValue}` | `${MarginValue} ${MarginValue} ${MarginValue} ${MarginValue}`;
|
||||
interface InViewOptions {
|
||||
root?: Element | Document;
|
||||
margin?: MarginType;
|
||||
amount?: "some" | "all" | number;
|
||||
}
|
||||
declare function inView(elementOrSelector: ElementOrSelector, onStart: (element: Element, entry: IntersectionObserverEntry) => void | ViewChangeHandler, { root, margin: rootMargin, amount }?: InViewOptions): VoidFunction;
|
||||
|
||||
type DelayedFunction = (overshoot: number) => void;
|
||||
declare function delayInSeconds(callback: DelayedFunction, timeout: number): () => void;
|
||||
|
||||
declare const distance: (a: number, b: number) => number;
|
||||
declare function distance2D(a: Point, b: Point): number;
|
||||
|
||||
export { type AbsoluteKeyframe, type AnimationSequence, type At, type DOMSegment, type DOMSegmentWithTransition, type DelayedFunction, type MotionValueSegment, type MotionValueSegmentWithTransition, type ObjectSegment, type ObjectSegmentWithTransition, type ObjectTarget, type ResolvedAnimationDefinition, type ResolvedAnimationDefinitions, type Segment, type SequenceLabel, type SequenceLabelWithTime, type SequenceMap, type SequenceOptions, type SequenceTime, type ValueSequence, animate, animateMini, createScopedAnimate, delayInSeconds as delay, distance, distance2D, inView, scroll, scrollInfo };
|
||||
-1
File diff suppressed because one or more lines are too long
-38
@@ -1,38 +0,0 @@
|
||||
import { GroupAnimationWithThen } from 'motion-dom';
|
||||
import { removeItem } from 'motion-utils';
|
||||
import { animateSequence } from './sequence.mjs';
|
||||
import { animateSubject } from './subject.mjs';
|
||||
|
||||
function isSequence(value) {
|
||||
return Array.isArray(value) && value.some(Array.isArray);
|
||||
}
|
||||
/**
|
||||
* Creates an animation function that is optionally scoped
|
||||
* to a specific element.
|
||||
*/
|
||||
function createScopedAnimate(scope) {
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
function scopedAnimate(subjectOrSequence, optionsOrKeyframes, options) {
|
||||
let animations = [];
|
||||
if (isSequence(subjectOrSequence)) {
|
||||
animations = animateSequence(subjectOrSequence, optionsOrKeyframes, scope);
|
||||
}
|
||||
else {
|
||||
animations = animateSubject(subjectOrSequence, optionsOrKeyframes, options, scope);
|
||||
}
|
||||
const animation = new GroupAnimationWithThen(animations);
|
||||
if (scope) {
|
||||
scope.animations.push(animation);
|
||||
animation.finished.then(() => {
|
||||
removeItem(scope.animations, animation);
|
||||
});
|
||||
}
|
||||
return animation;
|
||||
}
|
||||
return scopedAnimate;
|
||||
}
|
||||
const animate = createScopedAnimate();
|
||||
|
||||
export { animate, createScopedAnimate };
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { resolveElements } from 'motion-dom';
|
||||
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
|
||||
|
||||
function resolveSubjects(subject, keyframes, scope, selectorCache) {
|
||||
if (typeof subject === "string" && isDOMKeyframes(keyframes)) {
|
||||
return resolveElements(subject, scope, selectorCache);
|
||||
}
|
||||
else if (subject instanceof NodeList) {
|
||||
return Array.from(subject);
|
||||
}
|
||||
else if (Array.isArray(subject)) {
|
||||
return subject;
|
||||
}
|
||||
else {
|
||||
return [subject];
|
||||
}
|
||||
}
|
||||
|
||||
export { resolveSubjects };
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
import { spring } from 'motion-dom';
|
||||
import { createAnimationsFromSequence } from '../sequence/create.mjs';
|
||||
import { animateSubject } from './subject.mjs';
|
||||
|
||||
function animateSequence(sequence, options, scope) {
|
||||
const animations = [];
|
||||
const animationDefinitions = createAnimationsFromSequence(sequence, options, scope, { spring });
|
||||
animationDefinitions.forEach(({ keyframes, transition }, subject) => {
|
||||
animations.push(...animateSubject(subject, keyframes, transition));
|
||||
});
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateSequence };
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
import { isMotionValue, motionValue } from 'motion-dom';
|
||||
import { animateMotionValue } from '../interfaces/motion-value.mjs';
|
||||
|
||||
function animateSingleValue(value, keyframes, options) {
|
||||
const motionValue$1 = isMotionValue(value) ? value : motionValue(value);
|
||||
motionValue$1.start(animateMotionValue("", motionValue$1, keyframes, options));
|
||||
return motionValue$1.animation;
|
||||
}
|
||||
|
||||
export { animateSingleValue };
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
import { isMotionValue } from 'motion-dom';
|
||||
import { invariant } from 'motion-utils';
|
||||
import { visualElementStore } from '../../render/store.mjs';
|
||||
import { animateTarget } from '../interfaces/visual-element-target.mjs';
|
||||
import { createDOMVisualElement, createObjectVisualElement } from '../utils/create-visual-element.mjs';
|
||||
import { isDOMKeyframes } from '../utils/is-dom-keyframes.mjs';
|
||||
import { resolveSubjects } from './resolve-subjects.mjs';
|
||||
import { animateSingleValue } from './single-value.mjs';
|
||||
|
||||
function isSingleValue(subject, keyframes) {
|
||||
return (isMotionValue(subject) ||
|
||||
typeof subject === "number" ||
|
||||
(typeof subject === "string" && !isDOMKeyframes(keyframes)));
|
||||
}
|
||||
/**
|
||||
* Implementation
|
||||
*/
|
||||
function animateSubject(subject, keyframes, options, scope) {
|
||||
const animations = [];
|
||||
if (isSingleValue(subject, keyframes)) {
|
||||
animations.push(animateSingleValue(subject, isDOMKeyframes(keyframes)
|
||||
? keyframes.default || keyframes
|
||||
: keyframes, options ? options.default || options : options));
|
||||
}
|
||||
else {
|
||||
const subjects = resolveSubjects(subject, keyframes, scope);
|
||||
const numSubjects = subjects.length;
|
||||
invariant(Boolean(numSubjects), "No valid elements provided.");
|
||||
for (let i = 0; i < numSubjects; i++) {
|
||||
const thisSubject = subjects[i];
|
||||
const createVisualElement = thisSubject instanceof Element
|
||||
? createDOMVisualElement
|
||||
: createObjectVisualElement;
|
||||
if (!visualElementStore.has(thisSubject)) {
|
||||
createVisualElement(thisSubject);
|
||||
}
|
||||
const visualElement = visualElementStore.get(thisSubject);
|
||||
const transition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if ("delay" in transition &&
|
||||
typeof transition.delay === "function") {
|
||||
transition.delay = transition.delay(i, numSubjects);
|
||||
}
|
||||
animations.push(...animateTarget(visualElement, { ...keyframes, transition }, {}));
|
||||
}
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateSubject };
|
||||
Generated
Vendored
-105
@@ -1,105 +0,0 @@
|
||||
import { resolveElements, getValueTransition, getAnimationMap, animationMapKey, getComputedStyle, fillWildcards, applyPxDefaults, NativeAnimation } from 'motion-dom';
|
||||
import { invariant, secondsToMilliseconds } from 'motion-utils';
|
||||
|
||||
function animateElements(elementOrSelector, keyframes, options, scope) {
|
||||
const elements = resolveElements(elementOrSelector, scope);
|
||||
const numElements = elements.length;
|
||||
invariant(Boolean(numElements), "No valid element provided.");
|
||||
/**
|
||||
* WAAPI doesn't support interrupting animations.
|
||||
*
|
||||
* Therefore, starting animations requires a three-step process:
|
||||
* 1. Stop existing animations (write styles to DOM)
|
||||
* 2. Resolve keyframes (read styles from DOM)
|
||||
* 3. Create new animations (write styles to DOM)
|
||||
*
|
||||
* The hybrid `animate()` function uses AsyncAnimation to resolve
|
||||
* keyframes before creating new animations, which removes style
|
||||
* thrashing. Here, we have much stricter filesize constraints.
|
||||
* Therefore we do this in a synchronous way that ensures that
|
||||
* at least within `animate()` calls there is no style thrashing.
|
||||
*
|
||||
* In the motion-native-animate-mini-interrupt benchmark this
|
||||
* was 80% faster than a single loop.
|
||||
*/
|
||||
const animationDefinitions = [];
|
||||
/**
|
||||
* Step 1: Build options and stop existing animations (write)
|
||||
*/
|
||||
for (let i = 0; i < numElements; i++) {
|
||||
const element = elements[i];
|
||||
const elementTransition = { ...options };
|
||||
/**
|
||||
* Resolve stagger function if provided.
|
||||
*/
|
||||
if (typeof elementTransition.delay === "function") {
|
||||
elementTransition.delay = elementTransition.delay(i, numElements);
|
||||
}
|
||||
for (const valueName in keyframes) {
|
||||
let valueKeyframes = keyframes[valueName];
|
||||
if (!Array.isArray(valueKeyframes)) {
|
||||
valueKeyframes = [valueKeyframes];
|
||||
}
|
||||
const valueOptions = {
|
||||
...getValueTransition(elementTransition, valueName),
|
||||
};
|
||||
valueOptions.duration && (valueOptions.duration = secondsToMilliseconds(valueOptions.duration));
|
||||
valueOptions.delay && (valueOptions.delay = secondsToMilliseconds(valueOptions.delay));
|
||||
/**
|
||||
* If there's an existing animation playing on this element then stop it
|
||||
* before creating a new one.
|
||||
*/
|
||||
const map = getAnimationMap(element);
|
||||
const key = animationMapKey(valueName, valueOptions.pseudoElement || "");
|
||||
const currentAnimation = map.get(key);
|
||||
currentAnimation && currentAnimation.stop();
|
||||
animationDefinitions.push({
|
||||
map,
|
||||
key,
|
||||
unresolvedKeyframes: valueKeyframes,
|
||||
options: {
|
||||
...valueOptions,
|
||||
element,
|
||||
name: valueName,
|
||||
allowFlatten: !elementTransition.type && !elementTransition.ease,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Step 2: Resolve keyframes (read)
|
||||
*/
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { unresolvedKeyframes, options: animationOptions } = animationDefinitions[i];
|
||||
const { element, name, pseudoElement } = animationOptions;
|
||||
if (!pseudoElement && unresolvedKeyframes[0] === null) {
|
||||
unresolvedKeyframes[0] = getComputedStyle(element, name);
|
||||
}
|
||||
fillWildcards(unresolvedKeyframes);
|
||||
applyPxDefaults(unresolvedKeyframes, name);
|
||||
/**
|
||||
* If we only have one keyframe, explicitly read the initial keyframe
|
||||
* from the computed style. This is to ensure consistency with WAAPI behaviour
|
||||
* for restarting animations, for instance .play() after finish, when it
|
||||
* has one vs two keyframes.
|
||||
*/
|
||||
if (!pseudoElement && unresolvedKeyframes.length < 2) {
|
||||
unresolvedKeyframes.unshift(getComputedStyle(element, name));
|
||||
}
|
||||
animationOptions.keyframes = unresolvedKeyframes;
|
||||
}
|
||||
/**
|
||||
* Step 3: Create new animations (write)
|
||||
*/
|
||||
const animations = [];
|
||||
for (let i = 0; i < animationDefinitions.length; i++) {
|
||||
const { map, key, options: animationOptions } = animationDefinitions[i];
|
||||
const animation = new NativeAnimation(animationOptions);
|
||||
map.set(key, animation);
|
||||
animation.finished.finally(() => map.delete(key));
|
||||
animations.push(animation);
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateElements };
|
||||
Generated
Vendored
-13
@@ -1,13 +0,0 @@
|
||||
import { GroupAnimationWithThen } from 'motion-dom';
|
||||
import { createAnimationsFromSequence } from '../../sequence/create.mjs';
|
||||
import { animateElements } from './animate-elements.mjs';
|
||||
|
||||
function animateSequence(definition, options) {
|
||||
const animations = [];
|
||||
createAnimationsFromSequence(definition, options).forEach(({ keyframes, transition }, element) => {
|
||||
animations.push(...animateElements(element, keyframes, transition));
|
||||
});
|
||||
return new GroupAnimationWithThen(animations);
|
||||
}
|
||||
|
||||
export { animateSequence };
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
import { GroupAnimationWithThen } from 'motion-dom';
|
||||
import { animateElements } from './animate-elements.mjs';
|
||||
|
||||
const createScopedWaapiAnimate = (scope) => {
|
||||
function scopedAnimate(elementOrSelector, keyframes, options) {
|
||||
return new GroupAnimationWithThen(animateElements(elementOrSelector, keyframes, options, scope));
|
||||
}
|
||||
return scopedAnimate;
|
||||
};
|
||||
const animateMini = /*@__PURE__*/ createScopedWaapiAnimate();
|
||||
|
||||
export { animateMini, createScopedWaapiAnimate };
|
||||
Generated
Vendored
-12
@@ -1,12 +0,0 @@
|
||||
const isNotNull = (value) => value !== null;
|
||||
function getFinalKeyframe(keyframes, { repeat, repeatType = "loop" }, finalKeyframe) {
|
||||
const resolvedKeyframes = keyframes.filter(isNotNull);
|
||||
const index = repeat && repeatType !== "loop" && repeat % 2 === 1
|
||||
? 0
|
||||
: resolvedKeyframes.length - 1;
|
||||
return !index || finalKeyframe === undefined
|
||||
? resolvedKeyframes[index]
|
||||
: finalKeyframe;
|
||||
}
|
||||
|
||||
export { getFinalKeyframe };
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import { invariant } from 'motion-utils';
|
||||
import { setTarget } from '../../render/utils/setters.mjs';
|
||||
import { animateVisualElement } from '../interfaces/visual-element.mjs';
|
||||
|
||||
function stopAnimation(visualElement) {
|
||||
visualElement.values.forEach((value) => value.stop());
|
||||
}
|
||||
function setVariants(visualElement, variantLabels) {
|
||||
const reversedLabels = [...variantLabels].reverse();
|
||||
reversedLabels.forEach((key) => {
|
||||
const variant = visualElement.getVariant(key);
|
||||
variant && setTarget(visualElement, variant);
|
||||
if (visualElement.variantChildren) {
|
||||
visualElement.variantChildren.forEach((child) => {
|
||||
setVariants(child, variantLabels);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
function setValues(visualElement, definition) {
|
||||
if (Array.isArray(definition)) {
|
||||
return setVariants(visualElement, definition);
|
||||
}
|
||||
else if (typeof definition === "string") {
|
||||
return setVariants(visualElement, [definition]);
|
||||
}
|
||||
else {
|
||||
setTarget(visualElement, definition);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
function animationControls() {
|
||||
/**
|
||||
* Track whether the host component has mounted.
|
||||
*/
|
||||
let hasMounted = false;
|
||||
/**
|
||||
* A collection of linked component animation controls.
|
||||
*/
|
||||
const subscribers = new Set();
|
||||
const controls = {
|
||||
subscribe(visualElement) {
|
||||
subscribers.add(visualElement);
|
||||
return () => void subscribers.delete(visualElement);
|
||||
},
|
||||
start(definition, transitionOverride) {
|
||||
invariant(hasMounted, "controls.start() should only be called after a component has mounted. Consider calling within a useEffect hook.");
|
||||
const animations = [];
|
||||
subscribers.forEach((visualElement) => {
|
||||
animations.push(animateVisualElement(visualElement, definition, {
|
||||
transitionOverride,
|
||||
}));
|
||||
});
|
||||
return Promise.all(animations);
|
||||
},
|
||||
set(definition) {
|
||||
invariant(hasMounted, "controls.set() should only be called after a component has mounted. Consider calling within a useEffect hook.");
|
||||
return subscribers.forEach((visualElement) => {
|
||||
setValues(visualElement, definition);
|
||||
});
|
||||
},
|
||||
stop() {
|
||||
subscribers.forEach((visualElement) => {
|
||||
stopAnimation(visualElement);
|
||||
});
|
||||
},
|
||||
mount() {
|
||||
hasMounted = true;
|
||||
return () => {
|
||||
hasMounted = false;
|
||||
controls.stop();
|
||||
};
|
||||
},
|
||||
};
|
||||
return controls;
|
||||
}
|
||||
|
||||
export { animationControls, setValues };
|
||||
-17
@@ -1,17 +0,0 @@
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
|
||||
import { createScopedWaapiAnimate } from '../animators/waapi/animate-style.mjs';
|
||||
|
||||
function useAnimateMini() {
|
||||
const scope = useConstant(() => ({
|
||||
current: null, // Will be hydrated by React
|
||||
animations: [],
|
||||
}));
|
||||
const animate = useConstant(() => createScopedWaapiAnimate(scope));
|
||||
useUnmountEffect(() => {
|
||||
scope.animations.forEach((animation) => animation.stop());
|
||||
});
|
||||
return [scope, animate];
|
||||
}
|
||||
|
||||
export { useAnimateMini };
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useUnmountEffect } from '../../utils/use-unmount-effect.mjs';
|
||||
import { createScopedAnimate } from '../animate/index.mjs';
|
||||
|
||||
function useAnimate() {
|
||||
const scope = useConstant(() => ({
|
||||
current: null, // Will be hydrated by React
|
||||
animations: [],
|
||||
}));
|
||||
const animate = useConstant(() => createScopedAnimate(scope));
|
||||
useUnmountEffect(() => {
|
||||
scope.animations.forEach((animation) => animation.stop());
|
||||
scope.animations.length = 0;
|
||||
});
|
||||
return [scope, animate];
|
||||
}
|
||||
|
||||
export { useAnimate };
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
import { useState, useLayoutEffect } from 'react';
|
||||
import { makeUseVisualState } from '../../motion/utils/use-visual-state.mjs';
|
||||
import { createBox } from '../../projection/geometry/models.mjs';
|
||||
import { VisualElement } from '../../render/VisualElement.mjs';
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { animateVisualElement } from '../interfaces/visual-element.mjs';
|
||||
|
||||
const createObject = () => ({});
|
||||
class StateVisualElement extends VisualElement {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
this.measureInstanceViewportBox = createBox;
|
||||
}
|
||||
build() { }
|
||||
resetTransform() { }
|
||||
restoreTransform() { }
|
||||
removeValueFromRenderState() { }
|
||||
renderInstance() { }
|
||||
scrapeMotionValuesFromProps() {
|
||||
return createObject();
|
||||
}
|
||||
getBaseTargetFromProps() {
|
||||
return undefined;
|
||||
}
|
||||
readValueFromInstance(_state, key, options) {
|
||||
return options.initialState[key] || 0;
|
||||
}
|
||||
sortInstanceNodePosition() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
const useVisualState = makeUseVisualState({
|
||||
scrapeMotionValuesFromProps: createObject,
|
||||
createRenderState: createObject,
|
||||
});
|
||||
/**
|
||||
* This is not an officially supported API and may be removed
|
||||
* on any version.
|
||||
*/
|
||||
function useAnimatedState(initialState) {
|
||||
const [animationState, setAnimationState] = useState(initialState);
|
||||
const visualState = useVisualState({}, false);
|
||||
const element = useConstant(() => {
|
||||
return new StateVisualElement({
|
||||
props: {
|
||||
onUpdate: (v) => {
|
||||
setAnimationState({ ...v });
|
||||
},
|
||||
},
|
||||
visualState,
|
||||
presenceContext: null,
|
||||
}, { initialState });
|
||||
});
|
||||
useLayoutEffect(() => {
|
||||
element.mount({});
|
||||
return () => element.unmount();
|
||||
}, [element]);
|
||||
const startAnimation = useConstant(() => (animationDefinition) => {
|
||||
return animateVisualElement(element, animationDefinition);
|
||||
});
|
||||
return [animationState, startAnimation];
|
||||
}
|
||||
|
||||
export { useAnimatedState };
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
|
||||
import { animationControls } from './animation-controls.mjs';
|
||||
|
||||
/**
|
||||
* Creates `LegacyAnimationControls`, which can be used to manually start, stop
|
||||
* and sequence animations on one or more components.
|
||||
*
|
||||
* The returned `LegacyAnimationControls` should be passed to the `animate` property
|
||||
* of the components you want to animate.
|
||||
*
|
||||
* These components can then be animated with the `start` method.
|
||||
*
|
||||
* ```jsx
|
||||
* import * as React from 'react'
|
||||
* import { motion, useAnimation } from 'framer-motion'
|
||||
*
|
||||
* export function MyComponent(props) {
|
||||
* const controls = useAnimation()
|
||||
*
|
||||
* controls.start({
|
||||
* x: 100,
|
||||
* transition: { duration: 0.5 },
|
||||
* })
|
||||
*
|
||||
* return <motion.div animate={controls} />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @returns Animation controller with `start` and `stop` methods
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function useAnimationControls() {
|
||||
const controls = useConstant(animationControls);
|
||||
useIsomorphicLayoutEffect(controls.mount, []);
|
||||
return controls;
|
||||
}
|
||||
const useAnimation = useAnimationControls;
|
||||
|
||||
export { useAnimation, useAnimationControls };
|
||||
-98
@@ -1,98 +0,0 @@
|
||||
import { getValueTransition, frame, JSAnimation, AsyncMotionValueAnimation } from 'motion-dom';
|
||||
import { secondsToMilliseconds, MotionGlobalConfig } from 'motion-utils';
|
||||
import { getFinalKeyframe } from '../animators/waapi/utils/get-final-keyframe.mjs';
|
||||
import { getDefaultTransition } from '../utils/default-transitions.mjs';
|
||||
import { isTransitionDefined } from '../utils/is-transition-defined.mjs';
|
||||
|
||||
const animateMotionValue = (name, value, target, transition = {}, element, isHandoff) => (onComplete) => {
|
||||
const valueTransition = getValueTransition(transition, name) || {};
|
||||
/**
|
||||
* Most transition values are currently completely overwritten by value-specific
|
||||
* transitions. In the future it'd be nicer to blend these transitions. But for now
|
||||
* delay actually does inherit from the root transition if not value-specific.
|
||||
*/
|
||||
const delay = valueTransition.delay || transition.delay || 0;
|
||||
/**
|
||||
* Elapsed isn't a public transition option but can be passed through from
|
||||
* optimized appear effects in milliseconds.
|
||||
*/
|
||||
let { elapsed = 0 } = transition;
|
||||
elapsed = elapsed - secondsToMilliseconds(delay);
|
||||
const options = {
|
||||
keyframes: Array.isArray(target) ? target : [null, target],
|
||||
ease: "easeOut",
|
||||
velocity: value.getVelocity(),
|
||||
...valueTransition,
|
||||
delay: -elapsed,
|
||||
onUpdate: (v) => {
|
||||
value.set(v);
|
||||
valueTransition.onUpdate && valueTransition.onUpdate(v);
|
||||
},
|
||||
onComplete: () => {
|
||||
onComplete();
|
||||
valueTransition.onComplete && valueTransition.onComplete();
|
||||
},
|
||||
name,
|
||||
motionValue: value,
|
||||
element: isHandoff ? undefined : element,
|
||||
};
|
||||
/**
|
||||
* If there's no transition defined for this value, we can generate
|
||||
* unique transition settings for this value.
|
||||
*/
|
||||
if (!isTransitionDefined(valueTransition)) {
|
||||
Object.assign(options, getDefaultTransition(name, options));
|
||||
}
|
||||
/**
|
||||
* Both WAAPI and our internal animation functions use durations
|
||||
* as defined by milliseconds, while our external API defines them
|
||||
* as seconds.
|
||||
*/
|
||||
options.duration && (options.duration = secondsToMilliseconds(options.duration));
|
||||
options.repeatDelay && (options.repeatDelay = secondsToMilliseconds(options.repeatDelay));
|
||||
/**
|
||||
* Support deprecated way to set initial value. Prefer keyframe syntax.
|
||||
*/
|
||||
if (options.from !== undefined) {
|
||||
options.keyframes[0] = options.from;
|
||||
}
|
||||
let shouldSkip = false;
|
||||
if (options.type === false ||
|
||||
(options.duration === 0 && !options.repeatDelay)) {
|
||||
options.duration = 0;
|
||||
if (options.delay === 0) {
|
||||
shouldSkip = true;
|
||||
}
|
||||
}
|
||||
if (MotionGlobalConfig.instantAnimations ||
|
||||
MotionGlobalConfig.skipAnimations) {
|
||||
shouldSkip = true;
|
||||
options.duration = 0;
|
||||
options.delay = 0;
|
||||
}
|
||||
/**
|
||||
* If the transition type or easing has been explicitly set by the user
|
||||
* then we don't want to allow flattening the animation.
|
||||
*/
|
||||
options.allowFlatten = !valueTransition.type && !valueTransition.ease;
|
||||
/**
|
||||
* If we can or must skip creating the animation, and apply only
|
||||
* the final keyframe, do so. We also check once keyframes are resolved but
|
||||
* this early check prevents the need to create an animation at all.
|
||||
*/
|
||||
if (shouldSkip && !isHandoff && value.get() !== undefined) {
|
||||
const finalKeyframe = getFinalKeyframe(options.keyframes, valueTransition);
|
||||
if (finalKeyframe !== undefined) {
|
||||
frame.update(() => {
|
||||
options.onUpdate(finalKeyframe);
|
||||
options.onComplete();
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
return valueTransition.isSync
|
||||
? new JSAnimation(options)
|
||||
: new AsyncMotionValueAnimation(options);
|
||||
};
|
||||
|
||||
export { animateMotionValue };
|
||||
Generated
Vendored
-83
@@ -1,83 +0,0 @@
|
||||
import { getValueTransition, frame, positionalKeys } from 'motion-dom';
|
||||
import { setTarget } from '../../render/utils/setters.mjs';
|
||||
import { addValueToWillChange } from '../../value/use-will-change/add-will-change.mjs';
|
||||
import { getOptimisedAppearId } from '../optimized-appear/get-appear-id.mjs';
|
||||
import { animateMotionValue } from './motion-value.mjs';
|
||||
|
||||
/**
|
||||
* Decide whether we should block this animation. Previously, we achieved this
|
||||
* just by checking whether the key was listed in protectedKeys, but this
|
||||
* posed problems if an animation was triggered by afterChildren and protectedKeys
|
||||
* had been set to true in the meantime.
|
||||
*/
|
||||
function shouldBlockAnimation({ protectedKeys, needsAnimating }, key) {
|
||||
const shouldBlock = protectedKeys.hasOwnProperty(key) && needsAnimating[key] !== true;
|
||||
needsAnimating[key] = false;
|
||||
return shouldBlock;
|
||||
}
|
||||
function animateTarget(visualElement, targetAndTransition, { delay = 0, transitionOverride, type } = {}) {
|
||||
let { transition = visualElement.getDefaultTransition(), transitionEnd, ...target } = targetAndTransition;
|
||||
if (transitionOverride)
|
||||
transition = transitionOverride;
|
||||
const animations = [];
|
||||
const animationTypeState = type &&
|
||||
visualElement.animationState &&
|
||||
visualElement.animationState.getState()[type];
|
||||
for (const key in target) {
|
||||
const value = visualElement.getValue(key, visualElement.latestValues[key] ?? null);
|
||||
const valueTarget = target[key];
|
||||
if (valueTarget === undefined ||
|
||||
(animationTypeState &&
|
||||
shouldBlockAnimation(animationTypeState, key))) {
|
||||
continue;
|
||||
}
|
||||
const valueTransition = {
|
||||
delay,
|
||||
...getValueTransition(transition || {}, key),
|
||||
};
|
||||
/**
|
||||
* If the value is already at the defined target, skip the animation.
|
||||
*/
|
||||
const currentValue = value.get();
|
||||
if (currentValue !== undefined &&
|
||||
!value.isAnimating &&
|
||||
!Array.isArray(valueTarget) &&
|
||||
valueTarget === currentValue &&
|
||||
!valueTransition.velocity) {
|
||||
continue;
|
||||
}
|
||||
/**
|
||||
* If this is the first time a value is being animated, check
|
||||
* to see if we're handling off from an existing animation.
|
||||
*/
|
||||
let isHandoff = false;
|
||||
if (window.MotionHandoffAnimation) {
|
||||
const appearId = getOptimisedAppearId(visualElement);
|
||||
if (appearId) {
|
||||
const startTime = window.MotionHandoffAnimation(appearId, key, frame);
|
||||
if (startTime !== null) {
|
||||
valueTransition.startTime = startTime;
|
||||
isHandoff = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
addValueToWillChange(visualElement, key);
|
||||
value.start(animateMotionValue(key, value, valueTarget, visualElement.shouldReduceMotion && positionalKeys.has(key)
|
||||
? { type: false }
|
||||
: valueTransition, visualElement, isHandoff));
|
||||
const animation = value.animation;
|
||||
if (animation) {
|
||||
animations.push(animation);
|
||||
}
|
||||
}
|
||||
if (transitionEnd) {
|
||||
Promise.all(animations).then(() => {
|
||||
frame.update(() => {
|
||||
transitionEnd && setTarget(visualElement, transitionEnd);
|
||||
});
|
||||
});
|
||||
}
|
||||
return animations;
|
||||
}
|
||||
|
||||
export { animateTarget };
|
||||
Generated
Vendored
-72
@@ -1,72 +0,0 @@
|
||||
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
|
||||
import { animateTarget } from './visual-element-target.mjs';
|
||||
|
||||
function animateVariant(visualElement, variant, options = {}) {
|
||||
const resolved = resolveVariant(visualElement, variant, options.type === "exit"
|
||||
? visualElement.presenceContext?.custom
|
||||
: undefined);
|
||||
let { transition = visualElement.getDefaultTransition() || {} } = resolved || {};
|
||||
if (options.transitionOverride) {
|
||||
transition = options.transitionOverride;
|
||||
}
|
||||
/**
|
||||
* If we have a variant, create a callback that runs it as an animation.
|
||||
* Otherwise, we resolve a Promise immediately for a composable no-op.
|
||||
*/
|
||||
const getAnimation = resolved
|
||||
? () => Promise.all(animateTarget(visualElement, resolved, options))
|
||||
: () => Promise.resolve();
|
||||
/**
|
||||
* If we have children, create a callback that runs all their animations.
|
||||
* Otherwise, we resolve a Promise immediately for a composable no-op.
|
||||
*/
|
||||
const getChildAnimations = visualElement.variantChildren && visualElement.variantChildren.size
|
||||
? (forwardDelay = 0) => {
|
||||
const { delayChildren = 0, staggerChildren, staggerDirection, } = transition;
|
||||
return animateChildren(visualElement, variant, forwardDelay, delayChildren, staggerChildren, staggerDirection, options);
|
||||
}
|
||||
: () => Promise.resolve();
|
||||
/**
|
||||
* If the transition explicitly defines a "when" option, we need to resolve either
|
||||
* this animation or all children animations before playing the other.
|
||||
*/
|
||||
const { when } = transition;
|
||||
if (when) {
|
||||
const [first, last] = when === "beforeChildren"
|
||||
? [getAnimation, getChildAnimations]
|
||||
: [getChildAnimations, getAnimation];
|
||||
return first().then(() => last());
|
||||
}
|
||||
else {
|
||||
return Promise.all([getAnimation(), getChildAnimations(options.delay)]);
|
||||
}
|
||||
}
|
||||
function animateChildren(visualElement, variant, delay = 0, delayChildren = 0, staggerChildren = 0, staggerDirection = 1, options) {
|
||||
const animations = [];
|
||||
const numChildren = visualElement.variantChildren.size;
|
||||
const maxStaggerDuration = (numChildren - 1) * staggerChildren;
|
||||
const delayIsFunction = typeof delayChildren === "function";
|
||||
const generateStaggerDuration = delayIsFunction
|
||||
? (i) => delayChildren(i, numChildren)
|
||||
: // Support deprecated staggerChildren
|
||||
staggerDirection === 1
|
||||
? (i = 0) => i * staggerChildren
|
||||
: (i = 0) => maxStaggerDuration - i * staggerChildren;
|
||||
Array.from(visualElement.variantChildren)
|
||||
.sort(sortByTreeOrder)
|
||||
.forEach((child, i) => {
|
||||
child.notify("AnimationStart", variant);
|
||||
animations.push(animateVariant(child, variant, {
|
||||
...options,
|
||||
delay: delay +
|
||||
(delayIsFunction ? 0 : delayChildren) +
|
||||
generateStaggerDuration(i),
|
||||
}).then(() => child.notify("AnimationComplete", variant)));
|
||||
});
|
||||
return Promise.all(animations);
|
||||
}
|
||||
function sortByTreeOrder(a, b) {
|
||||
return a.sortNodePosition(b);
|
||||
}
|
||||
|
||||
export { animateVariant, sortByTreeOrder };
|
||||
-26
@@ -1,26 +0,0 @@
|
||||
import { resolveVariant } from '../../render/utils/resolve-dynamic-variants.mjs';
|
||||
import { animateTarget } from './visual-element-target.mjs';
|
||||
import { animateVariant } from './visual-element-variant.mjs';
|
||||
|
||||
function animateVisualElement(visualElement, definition, options = {}) {
|
||||
visualElement.notify("AnimationStart", definition);
|
||||
let animation;
|
||||
if (Array.isArray(definition)) {
|
||||
const animations = definition.map((variant) => animateVariant(visualElement, variant, options));
|
||||
animation = Promise.all(animations);
|
||||
}
|
||||
else if (typeof definition === "string") {
|
||||
animation = animateVariant(visualElement, definition, options);
|
||||
}
|
||||
else {
|
||||
const resolvedDefinition = typeof definition === "function"
|
||||
? resolveVariant(visualElement, definition, options.custom)
|
||||
: definition;
|
||||
animation = Promise.all(animateTarget(visualElement, resolvedDefinition, options));
|
||||
}
|
||||
return animation.then(() => {
|
||||
visualElement.notify("AnimationComplete", definition);
|
||||
});
|
||||
}
|
||||
|
||||
export { animateVisualElement };
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
import { camelToDash } from '../../render/dom/utils/camel-to-dash.mjs';
|
||||
|
||||
const optimizedAppearDataId = "framerAppearId";
|
||||
const optimizedAppearDataAttribute = "data-" + camelToDash(optimizedAppearDataId);
|
||||
|
||||
export { optimizedAppearDataAttribute, optimizedAppearDataId };
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { optimizedAppearDataAttribute } from './data-id.mjs';
|
||||
|
||||
function getOptimisedAppearId(visualElement) {
|
||||
return visualElement.props[optimizedAppearDataAttribute];
|
||||
}
|
||||
|
||||
export { getOptimisedAppearId };
|
||||
-38
@@ -1,38 +0,0 @@
|
||||
import { appearAnimationStore } from './store.mjs';
|
||||
import { appearStoreId } from './store-id.mjs';
|
||||
|
||||
function handoffOptimizedAppearAnimation(elementId, valueName, frame) {
|
||||
const storeId = appearStoreId(elementId, valueName);
|
||||
const optimisedAnimation = appearAnimationStore.get(storeId);
|
||||
if (!optimisedAnimation) {
|
||||
return null;
|
||||
}
|
||||
const { animation, startTime } = optimisedAnimation;
|
||||
function cancelAnimation() {
|
||||
window.MotionCancelOptimisedAnimation?.(elementId, valueName, frame);
|
||||
}
|
||||
/**
|
||||
* We can cancel the animation once it's finished now that we've synced
|
||||
* with Motion.
|
||||
*
|
||||
* Prefer onfinish over finished as onfinish is backwards compatible with
|
||||
* older browsers.
|
||||
*/
|
||||
animation.onfinish = cancelAnimation;
|
||||
if (startTime === null || window.MotionHandoffIsComplete?.(elementId)) {
|
||||
/**
|
||||
* If the startTime is null, this animation is the Paint Ready detection animation
|
||||
* and we can cancel it immediately without handoff.
|
||||
*
|
||||
* Or if we've already handed off the animation then we're now interrupting it.
|
||||
* In which case we need to cancel it.
|
||||
*/
|
||||
cancelAnimation();
|
||||
return null;
|
||||
}
|
||||
else {
|
||||
return startTime;
|
||||
}
|
||||
}
|
||||
|
||||
export { handoffOptimizedAppearAnimation };
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
import { startWaapiAnimation } from 'motion-dom';
|
||||
import { noop } from 'motion-utils';
|
||||
import { optimizedAppearDataId } from './data-id.mjs';
|
||||
import { getOptimisedAppearId } from './get-appear-id.mjs';
|
||||
import { handoffOptimizedAppearAnimation } from './handoff.mjs';
|
||||
import { appearAnimationStore, appearComplete } from './store.mjs';
|
||||
import { appearStoreId } from './store-id.mjs';
|
||||
|
||||
/**
|
||||
* A single time to use across all animations to manually set startTime
|
||||
* and ensure they're all in sync.
|
||||
*/
|
||||
let startFrameTime;
|
||||
/**
|
||||
* A dummy animation to detect when Chrome is ready to start
|
||||
* painting the page and hold off from triggering the real animation
|
||||
* until then. We only need one animation to detect paint ready.
|
||||
*
|
||||
* https://bugs.chromium.org/p/chromium/issues/detail?id=1406850
|
||||
*/
|
||||
let readyAnimation;
|
||||
/**
|
||||
* Keep track of animations that were suspended vs cancelled so we
|
||||
* can easily resume them when we're done measuring layout.
|
||||
*/
|
||||
const suspendedAnimations = new Set();
|
||||
function resumeSuspendedAnimations() {
|
||||
suspendedAnimations.forEach((data) => {
|
||||
data.animation.play();
|
||||
data.animation.startTime = data.startTime;
|
||||
});
|
||||
suspendedAnimations.clear();
|
||||
}
|
||||
function startOptimizedAppearAnimation(element, name, keyframes, options, onReady) {
|
||||
// Prevent optimised appear animations if Motion has already started animating.
|
||||
if (window.MotionIsMounted) {
|
||||
return;
|
||||
}
|
||||
const id = element.dataset[optimizedAppearDataId];
|
||||
if (!id)
|
||||
return;
|
||||
window.MotionHandoffAnimation = handoffOptimizedAppearAnimation;
|
||||
const storeId = appearStoreId(id, name);
|
||||
if (!readyAnimation) {
|
||||
readyAnimation = startWaapiAnimation(element, name, [keyframes[0], keyframes[0]],
|
||||
/**
|
||||
* 10 secs is basically just a super-safe duration to give Chrome
|
||||
* long enough to get the animation ready.
|
||||
*/
|
||||
{ duration: 10000, ease: "linear" });
|
||||
appearAnimationStore.set(storeId, {
|
||||
animation: readyAnimation,
|
||||
startTime: null,
|
||||
});
|
||||
/**
|
||||
* If there's no readyAnimation then there's been no instantiation
|
||||
* of handoff animations.
|
||||
*/
|
||||
window.MotionHandoffAnimation = handoffOptimizedAppearAnimation;
|
||||
window.MotionHasOptimisedAnimation = (elementId, valueName) => {
|
||||
if (!elementId)
|
||||
return false;
|
||||
/**
|
||||
* Keep a map of elementIds that have started animating. We check
|
||||
* via ID instead of Element because of hydration errors and
|
||||
* pre-hydration checks. We also actively record IDs as they start
|
||||
* animating rather than simply checking for data-appear-id as
|
||||
* this attrbute might be present but not lead to an animation, for
|
||||
* instance if the element's appear animation is on a different
|
||||
* breakpoint.
|
||||
*/
|
||||
if (!valueName) {
|
||||
return appearComplete.has(elementId);
|
||||
}
|
||||
const animationId = appearStoreId(elementId, valueName);
|
||||
return Boolean(appearAnimationStore.get(animationId));
|
||||
};
|
||||
window.MotionHandoffMarkAsComplete = (elementId) => {
|
||||
if (appearComplete.has(elementId)) {
|
||||
appearComplete.set(elementId, true);
|
||||
}
|
||||
};
|
||||
window.MotionHandoffIsComplete = (elementId) => {
|
||||
return appearComplete.get(elementId) === true;
|
||||
};
|
||||
/**
|
||||
* We only need to cancel transform animations as
|
||||
* they're the ones that will interfere with the
|
||||
* layout animation measurements.
|
||||
*/
|
||||
window.MotionCancelOptimisedAnimation = (elementId, valueName, frame, canResume) => {
|
||||
const animationId = appearStoreId(elementId, valueName);
|
||||
const data = appearAnimationStore.get(animationId);
|
||||
if (!data)
|
||||
return;
|
||||
if (frame && canResume === undefined) {
|
||||
/**
|
||||
* Wait until the end of the subsequent frame to cancel the animation
|
||||
* to ensure we don't remove the animation before the main thread has
|
||||
* had a chance to resolve keyframes and render.
|
||||
*/
|
||||
frame.postRender(() => {
|
||||
frame.postRender(() => {
|
||||
data.animation.cancel();
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
data.animation.cancel();
|
||||
}
|
||||
if (frame && canResume) {
|
||||
suspendedAnimations.add(data);
|
||||
frame.render(resumeSuspendedAnimations);
|
||||
}
|
||||
else {
|
||||
appearAnimationStore.delete(animationId);
|
||||
/**
|
||||
* If there are no more animations left, we can remove the cancel function.
|
||||
* This will let us know when we can stop checking for conflicting layout animations.
|
||||
*/
|
||||
if (!appearAnimationStore.size) {
|
||||
window.MotionCancelOptimisedAnimation = undefined;
|
||||
}
|
||||
}
|
||||
};
|
||||
window.MotionCheckAppearSync = (visualElement, valueName, value) => {
|
||||
const appearId = getOptimisedAppearId(visualElement);
|
||||
if (!appearId)
|
||||
return;
|
||||
const valueIsOptimised = window.MotionHasOptimisedAnimation?.(appearId, valueName);
|
||||
const externalAnimationValue = visualElement.props.values?.[valueName];
|
||||
if (!valueIsOptimised || !externalAnimationValue)
|
||||
return;
|
||||
const removeSyncCheck = value.on("change", (latestValue) => {
|
||||
if (externalAnimationValue.get() !== latestValue) {
|
||||
window.MotionCancelOptimisedAnimation?.(appearId, valueName);
|
||||
removeSyncCheck();
|
||||
}
|
||||
});
|
||||
return removeSyncCheck;
|
||||
};
|
||||
}
|
||||
const startAnimation = () => {
|
||||
readyAnimation.cancel();
|
||||
const appearAnimation = startWaapiAnimation(element, name, keyframes, options);
|
||||
/**
|
||||
* Record the time of the first started animation. We call performance.now() once
|
||||
* here and once in handoff to ensure we're getting
|
||||
* close to a frame-locked time. This keeps all animations in sync.
|
||||
*/
|
||||
if (startFrameTime === undefined) {
|
||||
startFrameTime = performance.now();
|
||||
}
|
||||
appearAnimation.startTime = startFrameTime;
|
||||
appearAnimationStore.set(storeId, {
|
||||
animation: appearAnimation,
|
||||
startTime: startFrameTime,
|
||||
});
|
||||
if (onReady)
|
||||
onReady(appearAnimation);
|
||||
};
|
||||
appearComplete.set(id, false);
|
||||
if (readyAnimation.ready) {
|
||||
readyAnimation.ready.then(startAnimation).catch(noop);
|
||||
}
|
||||
else {
|
||||
startAnimation();
|
||||
}
|
||||
}
|
||||
|
||||
export { startOptimizedAppearAnimation };
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { transformProps } from 'motion-dom';
|
||||
|
||||
const appearStoreId = (elementId, valueName) => {
|
||||
const key = transformProps.has(valueName) ? "transform" : valueName;
|
||||
return `${elementId}: ${key}`;
|
||||
};
|
||||
|
||||
export { appearStoreId };
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
const appearAnimationStore = new Map();
|
||||
const appearComplete = new Map();
|
||||
|
||||
export { appearAnimationStore, appearComplete };
|
||||
-249
@@ -1,249 +0,0 @@
|
||||
import { isMotionValue, defaultOffset, isGenerator, createGeneratorEasing, fillOffset } from 'motion-dom';
|
||||
import { progress, secondsToMilliseconds, invariant, getEasingForSegment } from 'motion-utils';
|
||||
import { resolveSubjects } from '../animate/resolve-subjects.mjs';
|
||||
import { calculateRepeatDuration } from './utils/calc-repeat-duration.mjs';
|
||||
import { calcNextTime } from './utils/calc-time.mjs';
|
||||
import { addKeyframes } from './utils/edit.mjs';
|
||||
import { normalizeTimes } from './utils/normalize-times.mjs';
|
||||
import { compareByTime } from './utils/sort.mjs';
|
||||
|
||||
const defaultSegmentEasing = "easeInOut";
|
||||
const MAX_REPEAT = 20;
|
||||
function createAnimationsFromSequence(sequence, { defaultTransition = {}, ...sequenceTransition } = {}, scope, generators) {
|
||||
const defaultDuration = defaultTransition.duration || 0.3;
|
||||
const animationDefinitions = new Map();
|
||||
const sequences = new Map();
|
||||
const elementCache = {};
|
||||
const timeLabels = new Map();
|
||||
let prevTime = 0;
|
||||
let currentTime = 0;
|
||||
let totalDuration = 0;
|
||||
/**
|
||||
* Build the timeline by mapping over the sequence array and converting
|
||||
* the definitions into keyframes and offsets with absolute time values.
|
||||
* These will later get converted into relative offsets in a second pass.
|
||||
*/
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const segment = sequence[i];
|
||||
/**
|
||||
* If this is a timeline label, mark it and skip the rest of this iteration.
|
||||
*/
|
||||
if (typeof segment === "string") {
|
||||
timeLabels.set(segment, currentTime);
|
||||
continue;
|
||||
}
|
||||
else if (!Array.isArray(segment)) {
|
||||
timeLabels.set(segment.name, calcNextTime(currentTime, segment.at, prevTime, timeLabels));
|
||||
continue;
|
||||
}
|
||||
let [subject, keyframes, transition = {}] = segment;
|
||||
/**
|
||||
* If a relative or absolute time value has been specified we need to resolve
|
||||
* it in relation to the currentTime.
|
||||
*/
|
||||
if (transition.at !== undefined) {
|
||||
currentTime = calcNextTime(currentTime, transition.at, prevTime, timeLabels);
|
||||
}
|
||||
/**
|
||||
* Keep track of the maximum duration in this definition. This will be
|
||||
* applied to currentTime once the definition has been parsed.
|
||||
*/
|
||||
let maxDuration = 0;
|
||||
const resolveValueSequence = (valueKeyframes, valueTransition, valueSequence, elementIndex = 0, numSubjects = 0) => {
|
||||
const valueKeyframesAsList = keyframesAsList(valueKeyframes);
|
||||
const { delay = 0, times = defaultOffset(valueKeyframesAsList), type = "keyframes", repeat, repeatType, repeatDelay = 0, ...remainingTransition } = valueTransition;
|
||||
let { ease = defaultTransition.ease || "easeOut", duration } = valueTransition;
|
||||
/**
|
||||
* Resolve stagger() if defined.
|
||||
*/
|
||||
const calculatedDelay = typeof delay === "function"
|
||||
? delay(elementIndex, numSubjects)
|
||||
: delay;
|
||||
/**
|
||||
* If this animation should and can use a spring, generate a spring easing function.
|
||||
*/
|
||||
const numKeyframes = valueKeyframesAsList.length;
|
||||
const createGenerator = isGenerator(type)
|
||||
? type
|
||||
: generators?.[type || "keyframes"];
|
||||
if (numKeyframes <= 2 && createGenerator) {
|
||||
/**
|
||||
* As we're creating an easing function from a spring,
|
||||
* ideally we want to generate it using the real distance
|
||||
* between the two keyframes. However this isn't always
|
||||
* possible - in these situations we use 0-100.
|
||||
*/
|
||||
let absoluteDelta = 100;
|
||||
if (numKeyframes === 2 &&
|
||||
isNumberKeyframesArray(valueKeyframesAsList)) {
|
||||
const delta = valueKeyframesAsList[1] - valueKeyframesAsList[0];
|
||||
absoluteDelta = Math.abs(delta);
|
||||
}
|
||||
const springTransition = { ...remainingTransition };
|
||||
if (duration !== undefined) {
|
||||
springTransition.duration = secondsToMilliseconds(duration);
|
||||
}
|
||||
const springEasing = createGeneratorEasing(springTransition, absoluteDelta, createGenerator);
|
||||
ease = springEasing.ease;
|
||||
duration = springEasing.duration;
|
||||
}
|
||||
duration ?? (duration = defaultDuration);
|
||||
const startTime = currentTime + calculatedDelay;
|
||||
/**
|
||||
* If there's only one time offset of 0, fill in a second with length 1
|
||||
*/
|
||||
if (times.length === 1 && times[0] === 0) {
|
||||
times[1] = 1;
|
||||
}
|
||||
/**
|
||||
* Fill out if offset if fewer offsets than keyframes
|
||||
*/
|
||||
const remainder = times.length - valueKeyframesAsList.length;
|
||||
remainder > 0 && fillOffset(times, remainder);
|
||||
/**
|
||||
* If only one value has been set, ie [1], push a null to the start of
|
||||
* the keyframe array. This will let us mark a keyframe at this point
|
||||
* that will later be hydrated with the previous value.
|
||||
*/
|
||||
valueKeyframesAsList.length === 1 &&
|
||||
valueKeyframesAsList.unshift(null);
|
||||
/**
|
||||
* Handle repeat options
|
||||
*/
|
||||
if (repeat) {
|
||||
invariant(repeat < MAX_REPEAT, "Repeat count too high, must be less than 20");
|
||||
duration = calculateRepeatDuration(duration, repeat);
|
||||
const originalKeyframes = [...valueKeyframesAsList];
|
||||
const originalTimes = [...times];
|
||||
ease = Array.isArray(ease) ? [...ease] : [ease];
|
||||
const originalEase = [...ease];
|
||||
for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
|
||||
valueKeyframesAsList.push(...originalKeyframes);
|
||||
for (let keyframeIndex = 0; keyframeIndex < originalKeyframes.length; keyframeIndex++) {
|
||||
times.push(originalTimes[keyframeIndex] + (repeatIndex + 1));
|
||||
ease.push(keyframeIndex === 0
|
||||
? "linear"
|
||||
: getEasingForSegment(originalEase, keyframeIndex - 1));
|
||||
}
|
||||
}
|
||||
normalizeTimes(times, repeat);
|
||||
}
|
||||
const targetTime = startTime + duration;
|
||||
/**
|
||||
* Add keyframes, mapping offsets to absolute time.
|
||||
*/
|
||||
addKeyframes(valueSequence, valueKeyframesAsList, ease, times, startTime, targetTime);
|
||||
maxDuration = Math.max(calculatedDelay + duration, maxDuration);
|
||||
totalDuration = Math.max(targetTime, totalDuration);
|
||||
};
|
||||
if (isMotionValue(subject)) {
|
||||
const subjectSequence = getSubjectSequence(subject, sequences);
|
||||
resolveValueSequence(keyframes, transition, getValueSequence("default", subjectSequence));
|
||||
}
|
||||
else {
|
||||
const subjects = resolveSubjects(subject, keyframes, scope, elementCache);
|
||||
const numSubjects = subjects.length;
|
||||
/**
|
||||
* For every element in this segment, process the defined values.
|
||||
*/
|
||||
for (let subjectIndex = 0; subjectIndex < numSubjects; subjectIndex++) {
|
||||
/**
|
||||
* Cast necessary, but we know these are of this type
|
||||
*/
|
||||
keyframes = keyframes;
|
||||
transition = transition;
|
||||
const thisSubject = subjects[subjectIndex];
|
||||
const subjectSequence = getSubjectSequence(thisSubject, sequences);
|
||||
for (const key in keyframes) {
|
||||
resolveValueSequence(keyframes[key], getValueTransition(transition, key), getValueSequence(key, subjectSequence), subjectIndex, numSubjects);
|
||||
}
|
||||
}
|
||||
}
|
||||
prevTime = currentTime;
|
||||
currentTime += maxDuration;
|
||||
}
|
||||
/**
|
||||
* For every element and value combination create a new animation.
|
||||
*/
|
||||
sequences.forEach((valueSequences, element) => {
|
||||
for (const key in valueSequences) {
|
||||
const valueSequence = valueSequences[key];
|
||||
/**
|
||||
* Arrange all the keyframes in ascending time order.
|
||||
*/
|
||||
valueSequence.sort(compareByTime);
|
||||
const keyframes = [];
|
||||
const valueOffset = [];
|
||||
const valueEasing = [];
|
||||
/**
|
||||
* For each keyframe, translate absolute times into
|
||||
* relative offsets based on the total duration of the timeline.
|
||||
*/
|
||||
for (let i = 0; i < valueSequence.length; i++) {
|
||||
const { at, value, easing } = valueSequence[i];
|
||||
keyframes.push(value);
|
||||
valueOffset.push(progress(0, totalDuration, at));
|
||||
valueEasing.push(easing || "easeOut");
|
||||
}
|
||||
/**
|
||||
* If the first keyframe doesn't land on offset: 0
|
||||
* provide one by duplicating the initial keyframe. This ensures
|
||||
* it snaps to the first keyframe when the animation starts.
|
||||
*/
|
||||
if (valueOffset[0] !== 0) {
|
||||
valueOffset.unshift(0);
|
||||
keyframes.unshift(keyframes[0]);
|
||||
valueEasing.unshift(defaultSegmentEasing);
|
||||
}
|
||||
/**
|
||||
* If the last keyframe doesn't land on offset: 1
|
||||
* provide one with a null wildcard value. This will ensure it
|
||||
* stays static until the end of the animation.
|
||||
*/
|
||||
if (valueOffset[valueOffset.length - 1] !== 1) {
|
||||
valueOffset.push(1);
|
||||
keyframes.push(null);
|
||||
}
|
||||
if (!animationDefinitions.has(element)) {
|
||||
animationDefinitions.set(element, {
|
||||
keyframes: {},
|
||||
transition: {},
|
||||
});
|
||||
}
|
||||
const definition = animationDefinitions.get(element);
|
||||
definition.keyframes[key] = keyframes;
|
||||
definition.transition[key] = {
|
||||
...defaultTransition,
|
||||
duration: totalDuration,
|
||||
ease: valueEasing,
|
||||
times: valueOffset,
|
||||
...sequenceTransition,
|
||||
};
|
||||
}
|
||||
});
|
||||
return animationDefinitions;
|
||||
}
|
||||
function getSubjectSequence(subject, sequences) {
|
||||
!sequences.has(subject) && sequences.set(subject, {});
|
||||
return sequences.get(subject);
|
||||
}
|
||||
function getValueSequence(name, sequences) {
|
||||
if (!sequences[name])
|
||||
sequences[name] = [];
|
||||
return sequences[name];
|
||||
}
|
||||
function keyframesAsList(keyframes) {
|
||||
return Array.isArray(keyframes) ? keyframes : [keyframes];
|
||||
}
|
||||
function getValueTransition(transition, key) {
|
||||
return transition && transition[key]
|
||||
? {
|
||||
...transition,
|
||||
...transition[key],
|
||||
}
|
||||
: { ...transition };
|
||||
}
|
||||
const isNumber = (keyframe) => typeof keyframe === "number";
|
||||
const isNumberKeyframesArray = (keyframes) => keyframes.every(isNumber);
|
||||
|
||||
export { createAnimationsFromSequence, getValueTransition };
|
||||
Generated
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
function calculateRepeatDuration(duration, repeat, _repeatDelay) {
|
||||
return duration * (repeat + 1);
|
||||
}
|
||||
|
||||
export { calculateRepeatDuration };
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
/**
|
||||
* Given a absolute or relative time definition and current/prev time state of the sequence,
|
||||
* calculate an absolute time for the next keyframes.
|
||||
*/
|
||||
function calcNextTime(current, next, prev, labels) {
|
||||
if (typeof next === "number") {
|
||||
return next;
|
||||
}
|
||||
else if (next.startsWith("-") || next.startsWith("+")) {
|
||||
return Math.max(0, current + parseFloat(next));
|
||||
}
|
||||
else if (next === "<") {
|
||||
return prev;
|
||||
}
|
||||
else if (next.startsWith("<")) {
|
||||
return Math.max(0, prev + parseFloat(next.slice(1)));
|
||||
}
|
||||
else {
|
||||
return labels.get(next) ?? current;
|
||||
}
|
||||
}
|
||||
|
||||
export { calcNextTime };
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
import { mixNumber } from 'motion-dom';
|
||||
import { getEasingForSegment, removeItem } from 'motion-utils';
|
||||
|
||||
function eraseKeyframes(sequence, startTime, endTime) {
|
||||
for (let i = 0; i < sequence.length; i++) {
|
||||
const keyframe = sequence[i];
|
||||
if (keyframe.at > startTime && keyframe.at < endTime) {
|
||||
removeItem(sequence, keyframe);
|
||||
// If we remove this item we have to push the pointer back one
|
||||
i--;
|
||||
}
|
||||
}
|
||||
}
|
||||
function addKeyframes(sequence, keyframes, easing, offset, startTime, endTime) {
|
||||
/**
|
||||
* Erase every existing value between currentTime and targetTime,
|
||||
* this will essentially splice this timeline into any currently
|
||||
* defined ones.
|
||||
*/
|
||||
eraseKeyframes(sequence, startTime, endTime);
|
||||
for (let i = 0; i < keyframes.length; i++) {
|
||||
sequence.push({
|
||||
value: keyframes[i],
|
||||
at: mixNumber(startTime, endTime, offset[i]),
|
||||
easing: getEasingForSegment(easing, i),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { addKeyframes, eraseKeyframes };
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
/**
|
||||
* Take an array of times that represent repeated keyframes. For instance
|
||||
* if we have original times of [0, 0.5, 1] then our repeated times will
|
||||
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
|
||||
* down to a 0-1 scale.
|
||||
*/
|
||||
function normalizeTimes(times, repeat) {
|
||||
for (let i = 0; i < times.length; i++) {
|
||||
times[i] = times[i] / (repeat + 1);
|
||||
}
|
||||
}
|
||||
|
||||
export { normalizeTimes };
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
function compareByTime(a, b) {
|
||||
if (a.at === b.at) {
|
||||
if (a.value === null)
|
||||
return 1;
|
||||
if (b.value === null)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
else {
|
||||
return a.at - b.at;
|
||||
}
|
||||
}
|
||||
|
||||
export { compareByTime };
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
import { isSVGElement, isSVGSVGElement } from 'motion-dom';
|
||||
import { HTMLVisualElement } from '../../render/html/HTMLVisualElement.mjs';
|
||||
import { ObjectVisualElement } from '../../render/object/ObjectVisualElement.mjs';
|
||||
import { visualElementStore } from '../../render/store.mjs';
|
||||
import { SVGVisualElement } from '../../render/svg/SVGVisualElement.mjs';
|
||||
|
||||
function createDOMVisualElement(element) {
|
||||
const options = {
|
||||
presenceContext: null,
|
||||
props: {},
|
||||
visualState: {
|
||||
renderState: {
|
||||
transform: {},
|
||||
transformOrigin: {},
|
||||
style: {},
|
||||
vars: {},
|
||||
attrs: {},
|
||||
},
|
||||
latestValues: {},
|
||||
},
|
||||
};
|
||||
const node = isSVGElement(element) && !isSVGSVGElement(element)
|
||||
? new SVGVisualElement(options)
|
||||
: new HTMLVisualElement(options);
|
||||
node.mount(element);
|
||||
visualElementStore.set(element, node);
|
||||
}
|
||||
function createObjectVisualElement(subject) {
|
||||
const options = {
|
||||
presenceContext: null,
|
||||
props: {},
|
||||
visualState: {
|
||||
renderState: {
|
||||
output: {},
|
||||
},
|
||||
latestValues: {},
|
||||
},
|
||||
};
|
||||
const node = new ObjectVisualElement(options);
|
||||
node.mount(subject);
|
||||
visualElementStore.set(subject, node);
|
||||
}
|
||||
|
||||
export { createDOMVisualElement, createObjectVisualElement };
|
||||
-40
@@ -1,40 +0,0 @@
|
||||
import { transformProps } from 'motion-dom';
|
||||
|
||||
const underDampedSpring = {
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 25,
|
||||
restSpeed: 10,
|
||||
};
|
||||
const criticallyDampedSpring = (target) => ({
|
||||
type: "spring",
|
||||
stiffness: 550,
|
||||
damping: target === 0 ? 2 * Math.sqrt(550) : 30,
|
||||
restSpeed: 10,
|
||||
});
|
||||
const keyframesTransition = {
|
||||
type: "keyframes",
|
||||
duration: 0.8,
|
||||
};
|
||||
/**
|
||||
* Default easing curve is a slightly shallower version of
|
||||
* the default browser easing curve.
|
||||
*/
|
||||
const ease = {
|
||||
type: "keyframes",
|
||||
ease: [0.25, 0.1, 0.35, 1],
|
||||
duration: 0.3,
|
||||
};
|
||||
const getDefaultTransition = (valueKey, { keyframes }) => {
|
||||
if (keyframes.length > 2) {
|
||||
return keyframesTransition;
|
||||
}
|
||||
else if (transformProps.has(valueKey)) {
|
||||
return valueKey.startsWith("scale")
|
||||
? criticallyDampedSpring(keyframes[1])
|
||||
: underDampedSpring;
|
||||
}
|
||||
return ease;
|
||||
};
|
||||
|
||||
export { getDefaultTransition };
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
function isAnimationControls(v) {
|
||||
return (v !== null &&
|
||||
typeof v === "object" &&
|
||||
typeof v.start === "function");
|
||||
}
|
||||
|
||||
export { isAnimationControls };
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
function isDOMKeyframes(keyframes) {
|
||||
return typeof keyframes === "object" && !Array.isArray(keyframes);
|
||||
}
|
||||
|
||||
export { isDOMKeyframes };
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
const isKeyframesTarget = (v) => {
|
||||
return Array.isArray(v);
|
||||
};
|
||||
|
||||
export { isKeyframesTarget };
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
/**
|
||||
* Decide whether a transition is defined on a given Transition.
|
||||
* This filters out orchestration options and returns true
|
||||
* if any options are left.
|
||||
*/
|
||||
function isTransitionDefined({ when, delay: _delay, delayChildren, staggerChildren, staggerDirection, repeat, repeatType, repeatDelay, from, elapsed, ...transition }) {
|
||||
return !!Object.keys(transition).length;
|
||||
}
|
||||
|
||||
export { isTransitionDefined };
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
export { MotionA as a, MotionAbbr as abbr, MotionAddress as address, MotionAnimate as animate, MotionArea as area, MotionArticle as article, MotionAside as aside, MotionAudio as audio, MotionB as b, MotionBase as base, MotionBdi as bdi, MotionBdo as bdo, MotionBig as big, MotionBlockquote as blockquote, MotionBody as body, MotionButton as button, MotionCanvas as canvas, MotionCaption as caption, MotionCircle as circle, MotionCite as cite, MotionClipPath as clipPath, MotionCode as code, MotionCol as col, MotionColgroup as colgroup, MotionData as data, MotionDatalist as datalist, MotionDd as dd, MotionDefs as defs, MotionDel as del, MotionDesc as desc, MotionDetails as details, MotionDfn as dfn, MotionDialog as dialog, MotionDiv as div, MotionDl as dl, MotionDt as dt, MotionEllipse as ellipse, MotionEm as em, MotionEmbed as embed, MotionFeBlend as feBlend, MotionFeColorMatrix as feColorMatrix, MotionFeComponentTransfer as feComponentTransfer, MotionFeComposite as feComposite, MotionFeConvolveMatrix as feConvolveMatrix, MotionFeDiffuseLighting as feDiffuseLighting, MotionFeDisplacementMap as feDisplacementMap, MotionFeDistantLight as feDistantLight, MotionFeDropShadow as feDropShadow, MotionFeFlood as feFlood, MotionFeFuncA as feFuncA, MotionFeFuncB as feFuncB, MotionFeFuncG as feFuncG, MotionFeFuncR as feFuncR, MotionFeGaussianBlur as feGaussianBlur, MotionFeImage as feImage, MotionFeMerge as feMerge, MotionFeMergeNode as feMergeNode, MotionFeMorphology as feMorphology, MotionFeOffset as feOffset, MotionFePointLight as fePointLight, MotionFeSpecularLighting as feSpecularLighting, MotionFeSpotLight as feSpotLight, MotionFeTile as feTile, MotionFeTurbulence as feTurbulence, MotionFieldset as fieldset, MotionFigcaption as figcaption, MotionFigure as figure, MotionFilter as filter, MotionFooter as footer, MotionForeignObject as foreignObject, MotionForm as form, MotionG as g, MotionH1 as h1, MotionH2 as h2, MotionH3 as h3, MotionH4 as h4, MotionH5 as h5, MotionH6 as h6, MotionHead as head, MotionHeader as header, MotionHgroup as hgroup, MotionHr as hr, MotionHtml as html, MotionI as i, MotionIframe as iframe, MotionImage as image, MotionImg as img, MotionInput as input, MotionIns as ins, MotionKbd as kbd, MotionKeygen as keygen, MotionLabel as label, MotionLegend as legend, MotionLi as li, MotionLine as line, MotionLinearGradient as linearGradient, MotionLink as link, MotionMain as main, MotionMap as map, MotionMark as mark, MotionMarker as marker, MotionMask as mask, MotionMenu as menu, MotionMenuitem as menuitem, MotionMetadata as metadata, MotionMeter as meter, MotionNav as nav, MotionObject as object, MotionOl as ol, MotionOptgroup as optgroup, MotionOption as option, MotionOutput as output, MotionP as p, MotionParam as param, MotionPath as path, MotionPattern as pattern, MotionPicture as picture, MotionPolygon as polygon, MotionPolyline as polyline, MotionPre as pre, MotionProgress as progress, MotionQ as q, MotionRadialGradient as radialGradient, MotionRect as rect, MotionRp as rp, MotionRt as rt, MotionRuby as ruby, MotionS as s, MotionSamp as samp, MotionScript as script, MotionSection as section, MotionSelect as select, MotionSmall as small, MotionSource as source, MotionSpan as span, MotionStop as stop, MotionStrong as strong, MotionStyle as style, MotionSub as sub, MotionSummary as summary, MotionSup as sup, MotionSvg as svg, MotionSymbol as symbol, MotionTable as table, MotionTbody as tbody, MotionTd as td, MotionText as text, MotionTextPath as textPath, MotionTextarea as textarea, MotionTfoot as tfoot, MotionTh as th, MotionThead as thead, MotionTime as time, MotionTitle as title, MotionTr as tr, MotionTrack as track, MotionTspan as tspan, MotionU as u, MotionUl as ul, MotionUse as use, MotionVideo as video, MotionView as view, MotionWbr as wbr, MotionWebview as webview } from './render/components/motion/elements.mjs';
|
||||
export { createMotionComponent as create } from './render/components/motion/create.mjs';
|
||||
-89
@@ -1,89 +0,0 @@
|
||||
"use client";
|
||||
import { jsx } from 'react/jsx-runtime';
|
||||
import { isHTMLElement } from 'motion-dom';
|
||||
import * as React from 'react';
|
||||
import { useId, useRef, useContext, useInsertionEffect } from 'react';
|
||||
import { MotionConfigContext } from '../../context/MotionConfigContext.mjs';
|
||||
|
||||
/**
|
||||
* Measurement functionality has to be within a separate component
|
||||
* to leverage snapshot lifecycle.
|
||||
*/
|
||||
class PopChildMeasure extends React.Component {
|
||||
getSnapshotBeforeUpdate(prevProps) {
|
||||
const element = this.props.childRef.current;
|
||||
if (element && prevProps.isPresent && !this.props.isPresent) {
|
||||
const parent = element.offsetParent;
|
||||
const parentWidth = isHTMLElement(parent)
|
||||
? parent.offsetWidth || 0
|
||||
: 0;
|
||||
const size = this.props.sizeRef.current;
|
||||
size.height = element.offsetHeight || 0;
|
||||
size.width = element.offsetWidth || 0;
|
||||
size.top = element.offsetTop;
|
||||
size.left = element.offsetLeft;
|
||||
size.right = parentWidth - size.width - size.left;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* Required with getSnapshotBeforeUpdate to stop React complaining.
|
||||
*/
|
||||
componentDidUpdate() { }
|
||||
render() {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
function PopChild({ children, isPresent, anchorX, root }) {
|
||||
const id = useId();
|
||||
const ref = useRef(null);
|
||||
const size = useRef({
|
||||
width: 0,
|
||||
height: 0,
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
});
|
||||
const { nonce } = useContext(MotionConfigContext);
|
||||
/**
|
||||
* We create and inject a style block so we can apply this explicit
|
||||
* sizing in a non-destructive manner by just deleting the style block.
|
||||
*
|
||||
* We can't apply size via render as the measurement happens
|
||||
* in getSnapshotBeforeUpdate (post-render), likewise if we apply the
|
||||
* styles directly on the DOM node, we might be overwriting
|
||||
* styles set via the style prop.
|
||||
*/
|
||||
useInsertionEffect(() => {
|
||||
const { width, height, top, left, right } = size.current;
|
||||
if (isPresent || !ref.current || !width || !height)
|
||||
return;
|
||||
const x = anchorX === "left" ? `left: ${left}` : `right: ${right}`;
|
||||
ref.current.dataset.motionPopId = id;
|
||||
const style = document.createElement("style");
|
||||
if (nonce)
|
||||
style.nonce = nonce;
|
||||
const parent = root ?? document.head;
|
||||
parent.appendChild(style);
|
||||
if (style.sheet) {
|
||||
style.sheet.insertRule(`
|
||||
[data-motion-pop-id="${id}"] {
|
||||
position: absolute !important;
|
||||
width: ${width}px !important;
|
||||
height: ${height}px !important;
|
||||
${x}px !important;
|
||||
top: ${top}px !important;
|
||||
}
|
||||
`);
|
||||
}
|
||||
return () => {
|
||||
parent.removeChild(style);
|
||||
if (parent.contains(style)) {
|
||||
parent.removeChild(style);
|
||||
}
|
||||
};
|
||||
}, [isPresent]);
|
||||
return (jsx(PopChildMeasure, { isPresent: isPresent, childRef: ref, sizeRef: size, children: React.cloneElement(children, { ref }) }));
|
||||
}
|
||||
|
||||
export { PopChild };
|
||||
-64
@@ -1,64 +0,0 @@
|
||||
"use client";
|
||||
import { jsx } from 'react/jsx-runtime';
|
||||
import * as React from 'react';
|
||||
import { useId, useMemo } from 'react';
|
||||
import { PresenceContext } from '../../context/PresenceContext.mjs';
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { PopChild } from './PopChild.mjs';
|
||||
|
||||
const PresenceChild = ({ children, initial, isPresent, onExitComplete, custom, presenceAffectsLayout, mode, anchorX, root }) => {
|
||||
const presenceChildren = useConstant(newChildrenMap);
|
||||
const id = useId();
|
||||
let isReusedContext = true;
|
||||
let context = useMemo(() => {
|
||||
isReusedContext = false;
|
||||
return {
|
||||
id,
|
||||
initial,
|
||||
isPresent,
|
||||
custom,
|
||||
onExitComplete: (childId) => {
|
||||
presenceChildren.set(childId, true);
|
||||
for (const isComplete of presenceChildren.values()) {
|
||||
if (!isComplete)
|
||||
return; // can stop searching when any is incomplete
|
||||
}
|
||||
onExitComplete && onExitComplete();
|
||||
},
|
||||
register: (childId) => {
|
||||
presenceChildren.set(childId, false);
|
||||
return () => presenceChildren.delete(childId);
|
||||
},
|
||||
};
|
||||
}, [isPresent, presenceChildren, onExitComplete]);
|
||||
/**
|
||||
* If the presence of a child affects the layout of the components around it,
|
||||
* we want to make a new context value to ensure they get re-rendered
|
||||
* so they can detect that layout change.
|
||||
*/
|
||||
if (presenceAffectsLayout && isReusedContext) {
|
||||
context = { ...context };
|
||||
}
|
||||
useMemo(() => {
|
||||
presenceChildren.forEach((_, key) => presenceChildren.set(key, false));
|
||||
}, [isPresent]);
|
||||
/**
|
||||
* If there's no `motion` components to fire exit animations, we want to remove this
|
||||
* component immediately.
|
||||
*/
|
||||
React.useEffect(() => {
|
||||
!isPresent &&
|
||||
!presenceChildren.size &&
|
||||
onExitComplete &&
|
||||
onExitComplete();
|
||||
}, [isPresent]);
|
||||
if (mode === "popLayout") {
|
||||
children = (jsx(PopChild, { isPresent: isPresent, anchorX: anchorX, root: root, children: children }));
|
||||
}
|
||||
return (jsx(PresenceContext.Provider, { value: context, children: children }));
|
||||
};
|
||||
function newChildrenMap() {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
export { PresenceChild };
|
||||
-166
@@ -1,166 +0,0 @@
|
||||
"use client";
|
||||
import { jsx, Fragment } from 'react/jsx-runtime';
|
||||
import { useMemo, useRef, useState, useContext } from 'react';
|
||||
import { LayoutGroupContext } from '../../context/LayoutGroupContext.mjs';
|
||||
import { useConstant } from '../../utils/use-constant.mjs';
|
||||
import { useIsomorphicLayoutEffect } from '../../utils/use-isomorphic-effect.mjs';
|
||||
import { PresenceChild } from './PresenceChild.mjs';
|
||||
import { usePresence } from './use-presence.mjs';
|
||||
import { onlyElements, getChildKey } from './utils.mjs';
|
||||
|
||||
/**
|
||||
* `AnimatePresence` enables the animation of components that have been removed from the tree.
|
||||
*
|
||||
* When adding/removing more than a single child, every child **must** be given a unique `key` prop.
|
||||
*
|
||||
* Any `motion` components that have an `exit` property defined will animate out when removed from
|
||||
* the tree.
|
||||
*
|
||||
* ```jsx
|
||||
* import { motion, AnimatePresence } from 'framer-motion'
|
||||
*
|
||||
* export const Items = ({ items }) => (
|
||||
* <AnimatePresence>
|
||||
* {items.map(item => (
|
||||
* <motion.div
|
||||
* key={item.id}
|
||||
* initial={{ opacity: 0 }}
|
||||
* animate={{ opacity: 1 }}
|
||||
* exit={{ opacity: 0 }}
|
||||
* />
|
||||
* ))}
|
||||
* </AnimatePresence>
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* You can sequence exit animations throughout a tree using variants.
|
||||
*
|
||||
* If a child contains multiple `motion` components with `exit` props, it will only unmount the child
|
||||
* once all `motion` components have finished animating out. Likewise, any components using
|
||||
* `usePresence` all need to call `safeToRemove`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
const AnimatePresence = ({ children, custom, initial = true, onExitComplete, presenceAffectsLayout = true, mode = "sync", propagate = false, anchorX = "left", root }) => {
|
||||
const [isParentPresent, safeToRemove] = usePresence(propagate);
|
||||
/**
|
||||
* Filter any children that aren't ReactElements. We can only track components
|
||||
* between renders with a props.key.
|
||||
*/
|
||||
const presentChildren = useMemo(() => onlyElements(children), [children]);
|
||||
/**
|
||||
* Track the keys of the currently rendered children. This is used to
|
||||
* determine which children are exiting.
|
||||
*/
|
||||
const presentKeys = propagate && !isParentPresent ? [] : presentChildren.map(getChildKey);
|
||||
/**
|
||||
* If `initial={false}` we only want to pass this to components in the first render.
|
||||
*/
|
||||
const isInitialRender = useRef(true);
|
||||
/**
|
||||
* A ref containing the currently present children. When all exit animations
|
||||
* are complete, we use this to re-render the component with the latest children
|
||||
* *committed* rather than the latest children *rendered*.
|
||||
*/
|
||||
const pendingPresentChildren = useRef(presentChildren);
|
||||
/**
|
||||
* Track which exiting children have finished animating out.
|
||||
*/
|
||||
const exitComplete = useConstant(() => new Map());
|
||||
/**
|
||||
* Save children to render as React state. To ensure this component is concurrent-safe,
|
||||
* we check for exiting children via an effect.
|
||||
*/
|
||||
const [diffedChildren, setDiffedChildren] = useState(presentChildren);
|
||||
const [renderedChildren, setRenderedChildren] = useState(presentChildren);
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
isInitialRender.current = false;
|
||||
pendingPresentChildren.current = presentChildren;
|
||||
/**
|
||||
* Update complete status of exiting children.
|
||||
*/
|
||||
for (let i = 0; i < renderedChildren.length; i++) {
|
||||
const key = getChildKey(renderedChildren[i]);
|
||||
if (!presentKeys.includes(key)) {
|
||||
if (exitComplete.get(key) !== true) {
|
||||
exitComplete.set(key, false);
|
||||
}
|
||||
}
|
||||
else {
|
||||
exitComplete.delete(key);
|
||||
}
|
||||
}
|
||||
}, [renderedChildren, presentKeys.length, presentKeys.join("-")]);
|
||||
const exitingChildren = [];
|
||||
if (presentChildren !== diffedChildren) {
|
||||
let nextChildren = [...presentChildren];
|
||||
/**
|
||||
* Loop through all the currently rendered components and decide which
|
||||
* are exiting.
|
||||
*/
|
||||
for (let i = 0; i < renderedChildren.length; i++) {
|
||||
const child = renderedChildren[i];
|
||||
const key = getChildKey(child);
|
||||
if (!presentKeys.includes(key)) {
|
||||
nextChildren.splice(i, 0, child);
|
||||
exitingChildren.push(child);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* If we're in "wait" mode, and we have exiting children, we want to
|
||||
* only render these until they've all exited.
|
||||
*/
|
||||
if (mode === "wait" && exitingChildren.length) {
|
||||
nextChildren = exitingChildren;
|
||||
}
|
||||
setRenderedChildren(onlyElements(nextChildren));
|
||||
setDiffedChildren(presentChildren);
|
||||
/**
|
||||
* Early return to ensure once we've set state with the latest diffed
|
||||
* children, we can immediately re-render.
|
||||
*/
|
||||
return null;
|
||||
}
|
||||
if (process.env.NODE_ENV !== "production" &&
|
||||
mode === "wait" &&
|
||||
renderedChildren.length > 1) {
|
||||
console.warn(`You're attempting to animate multiple children within AnimatePresence, but its mode is set to "wait". This will lead to odd visual behaviour.`);
|
||||
}
|
||||
/**
|
||||
* If we've been provided a forceRender function by the LayoutGroupContext,
|
||||
* we can use it to force a re-render amongst all surrounding components once
|
||||
* all components have finished animating out.
|
||||
*/
|
||||
const { forceRender } = useContext(LayoutGroupContext);
|
||||
return (jsx(Fragment, { children: renderedChildren.map((child) => {
|
||||
const key = getChildKey(child);
|
||||
const isPresent = propagate && !isParentPresent
|
||||
? false
|
||||
: presentChildren === renderedChildren ||
|
||||
presentKeys.includes(key);
|
||||
const onExit = () => {
|
||||
if (exitComplete.has(key)) {
|
||||
exitComplete.set(key, true);
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
let isEveryExitComplete = true;
|
||||
exitComplete.forEach((isExitComplete) => {
|
||||
if (!isExitComplete)
|
||||
isEveryExitComplete = false;
|
||||
});
|
||||
if (isEveryExitComplete) {
|
||||
forceRender?.();
|
||||
setRenderedChildren(pendingPresentChildren.current);
|
||||
propagate && safeToRemove?.();
|
||||
onExitComplete && onExitComplete();
|
||||
}
|
||||
};
|
||||
return (jsx(PresenceChild, { isPresent: isPresent, initial: !isInitialRender.current || initial
|
||||
? undefined
|
||||
: false, custom: custom, presenceAffectsLayout: presenceAffectsLayout, mode: mode, root: root, onExitComplete: isPresent ? undefined : onExit, anchorX: anchorX, children: child }, key));
|
||||
}) }));
|
||||
};
|
||||
|
||||
export { AnimatePresence };
|
||||
Generated
Vendored
-9
@@ -1,9 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { PresenceContext } from '../../context/PresenceContext.mjs';
|
||||
|
||||
function usePresenceData() {
|
||||
const context = useContext(PresenceContext);
|
||||
return context ? context.custom : undefined;
|
||||
}
|
||||
|
||||
export { usePresenceData };
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
import { useContext, useId, useEffect, useCallback } from 'react';
|
||||
import { PresenceContext } from '../../context/PresenceContext.mjs';
|
||||
|
||||
/**
|
||||
* When a component is the child of `AnimatePresence`, it can use `usePresence`
|
||||
* to access information about whether it's still present in the React tree.
|
||||
*
|
||||
* ```jsx
|
||||
* import { usePresence } from "framer-motion"
|
||||
*
|
||||
* export const Component = () => {
|
||||
* const [isPresent, safeToRemove] = usePresence()
|
||||
*
|
||||
* useEffect(() => {
|
||||
* !isPresent && setTimeout(safeToRemove, 1000)
|
||||
* }, [isPresent])
|
||||
*
|
||||
* return <div />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* If `isPresent` is `false`, it means that a component has been removed the tree, but
|
||||
* `AnimatePresence` won't really remove it until `safeToRemove` has been called.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function usePresence(subscribe = true) {
|
||||
const context = useContext(PresenceContext);
|
||||
if (context === null)
|
||||
return [true, null];
|
||||
const { isPresent, onExitComplete, register } = context;
|
||||
// It's safe to call the following hooks conditionally (after an early return) because the context will always
|
||||
// either be null or non-null for the lifespan of the component.
|
||||
const id = useId();
|
||||
useEffect(() => {
|
||||
if (subscribe) {
|
||||
return register(id);
|
||||
}
|
||||
}, [subscribe]);
|
||||
const safeToRemove = useCallback(() => subscribe && onExitComplete && onExitComplete(id), [id, onExitComplete, subscribe]);
|
||||
return !isPresent && onExitComplete ? [false, safeToRemove] : [true];
|
||||
}
|
||||
/**
|
||||
* Similar to `usePresence`, except `useIsPresent` simply returns whether or not the component is present.
|
||||
* There is no `safeToRemove` function.
|
||||
*
|
||||
* ```jsx
|
||||
* import { useIsPresent } from "framer-motion"
|
||||
*
|
||||
* export const Component = () => {
|
||||
* const isPresent = useIsPresent()
|
||||
*
|
||||
* useEffect(() => {
|
||||
* !isPresent && console.log("I've been removed!")
|
||||
* }, [isPresent])
|
||||
*
|
||||
* return <div />
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
function useIsPresent() {
|
||||
return isPresent(useContext(PresenceContext));
|
||||
}
|
||||
function isPresent(context) {
|
||||
return context === null ? true : context.isPresent;
|
||||
}
|
||||
|
||||
export { isPresent, useIsPresent, usePresence };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user