feat: implement shadow DOM; make notification hookable
This commit is contained in:
parent
9a5ec9f0a1
commit
313d24d808
13
index.html
13
index.html
@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="https://vitejs.dev/logo.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + React + TS</title>
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
</html>
|
@ -11,6 +11,7 @@
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/cache": "^11.10.5",
|
||||
"@emotion/react": "^11.10.6",
|
||||
"@emotion/styled": "^11.10.6",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
|
2
pnpm-lock.yaml
generated
2
pnpm-lock.yaml
generated
@ -1,6 +1,7 @@
|
||||
lockfileVersion: 5.4
|
||||
|
||||
specifiers:
|
||||
'@emotion/cache': ^11.10.5
|
||||
'@emotion/react': ^11.10.6
|
||||
'@emotion/styled': ^11.10.6
|
||||
'@mui/icons-material': ^5.11.11
|
||||
@ -17,6 +18,7 @@ specifiers:
|
||||
vite-plugin-monkey: 1.1.4
|
||||
|
||||
dependencies:
|
||||
'@emotion/cache': 11.10.5
|
||||
'@emotion/react': 11.10.6_pmekkgnqduwlme35zpnqhenc34
|
||||
'@emotion/styled': 11.10.6_oouaibmszuch5k64ms7uxp2aia
|
||||
'@mui/icons-material': 5.11.11_4lyzeezzeeal3x6jtb4ni26w7u
|
||||
|
66
src/App.tsx
66
src/App.tsx
@ -1,17 +1,18 @@
|
||||
/* eslint-disable no-undef */
|
||||
import styled from '@emotion/styled';
|
||||
import { ContentCopy, ContentPaste, DeleteForever } from '@mui/icons-material';
|
||||
import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh';
|
||||
import {
|
||||
Divider,
|
||||
IconButton,
|
||||
ListItemIcon,
|
||||
ListItemText,
|
||||
Menu,
|
||||
MenuItem,
|
||||
} from '@mui/material';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import ContentPaste from '@mui/icons-material/ContentPaste';
|
||||
import DeleteForever from '@mui/icons-material/DeleteForever';
|
||||
import Alert from '@mui/material/Alert';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Snackbar from '@mui/material/Snackbar';
|
||||
import { useState } from 'react';
|
||||
import notification from './components/notification';
|
||||
import useToast from './hooks/useToast';
|
||||
|
||||
const Z_INDEX_MAX = 2 ** 31 - 1;
|
||||
|
||||
@ -36,6 +37,15 @@ const CustomMenu = styled(Menu)`
|
||||
const App = () => {
|
||||
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const menuOpen = Boolean(anchorEl);
|
||||
|
||||
const {
|
||||
open: toastOpen,
|
||||
message: toastMessage,
|
||||
severity: toastSeverity,
|
||||
handleOpen: handleToastOpen,
|
||||
handleClose: handleToastClose,
|
||||
} = useToast();
|
||||
|
||||
const handleMenuOpen = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
@ -89,9 +99,9 @@ const App = () => {
|
||||
const cookie = await getCookie();
|
||||
const exportSessionData = JSON.stringify(cookie);
|
||||
copyToClipboard(exportSessionData);
|
||||
notification.success({ message: 'Session 数据已复制到剪贴板' });
|
||||
handleToastOpen('Session 数据已复制到剪贴板', 'success');
|
||||
} catch (error) {
|
||||
notification.error({ message: '获取 Cookie 失败' });
|
||||
handleToastOpen('获取 Cookie 失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@ -103,18 +113,18 @@ const App = () => {
|
||||
cookie.forEach(async (item) => {
|
||||
await setCookie(item);
|
||||
});
|
||||
notification.success({ message: 'Session 数据已导入' });
|
||||
handleToastOpen('Session 数据已导入', 'success');
|
||||
} catch (error) {
|
||||
notification.error({ message: '导入 Cookie 失败' });
|
||||
handleToastOpen('导入 Cookie 失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
const deleteSession = async () => {
|
||||
try {
|
||||
await deleteCookie();
|
||||
notification.success({ message: 'Session 数据已删除' });
|
||||
handleToastOpen('Session 数据已删除', 'success');
|
||||
} catch (error) {
|
||||
notification.error({ message: '删除 Cookie 失败' });
|
||||
handleToastOpen('删除 Cookie 失败', 'error');
|
||||
}
|
||||
};
|
||||
|
||||
@ -134,7 +144,8 @@ const App = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<>
|
||||
{/* 工具按钮 */}
|
||||
<CustomButton
|
||||
color="primary"
|
||||
id="custom-button"
|
||||
@ -146,6 +157,7 @@ const App = () => {
|
||||
<AutoFixHighIcon />
|
||||
</CustomButton>
|
||||
|
||||
{/* 工具菜单 */}
|
||||
<CustomMenu
|
||||
id="custom-menu"
|
||||
anchorEl={anchorEl}
|
||||
@ -178,7 +190,23 @@ const App = () => {
|
||||
<ListItemText>清除 Session 数据</ListItemText>
|
||||
</MenuItem>
|
||||
</CustomMenu>
|
||||
</div>
|
||||
|
||||
{/* 提示 */}
|
||||
<Snackbar
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
open={toastOpen}
|
||||
autoHideDuration={2000}
|
||||
onClose={handleToastClose}
|
||||
>
|
||||
<Alert
|
||||
onClose={handleToastClose}
|
||||
severity={toastSeverity}
|
||||
sx={{ width: '100%' }}
|
||||
>
|
||||
{toastMessage}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,84 +0,0 @@
|
||||
import { Alert, Snackbar } from '@mui/material';
|
||||
import { useEffect, useState } from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
const defaultDuration = 2000;
|
||||
|
||||
interface NotificationProps {
|
||||
message: string;
|
||||
type: 'success' | 'error' | 'info' | 'warning';
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
interface ConfigProps {
|
||||
message: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
const Notification = (props: NotificationProps) => {
|
||||
const { message, type, duration } = props;
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const handleClose = () => {
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(true);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Snackbar
|
||||
open={open}
|
||||
autoHideDuration={duration}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
|
||||
>
|
||||
<Alert severity={type} variant="filled" onClose={handleClose}>
|
||||
{message}
|
||||
</Alert>
|
||||
</Snackbar>
|
||||
);
|
||||
};
|
||||
|
||||
const notification = {
|
||||
dom: null,
|
||||
|
||||
success({ message, duration = defaultDuration }: ConfigProps) {
|
||||
const dom = document.createElement('div');
|
||||
const JSXdom = (
|
||||
<Notification message={message} duration={duration} type="success" />
|
||||
);
|
||||
ReactDOM.render(JSXdom, dom);
|
||||
document.body.appendChild(dom);
|
||||
},
|
||||
|
||||
error({ message, duration = defaultDuration }: ConfigProps) {
|
||||
const dom = document.createElement('div');
|
||||
const JSXdom = (
|
||||
<Notification message={message} duration={duration} type="error" />
|
||||
);
|
||||
ReactDOM.render(JSXdom, dom);
|
||||
document.body.appendChild(dom);
|
||||
},
|
||||
|
||||
warning({ message, duration = defaultDuration }: ConfigProps) {
|
||||
const dom = document.createElement('div');
|
||||
const JSXdom = (
|
||||
<Notification message={message} duration={duration} type="warning" />
|
||||
);
|
||||
ReactDOM.render(JSXdom, dom);
|
||||
document.body.appendChild(dom);
|
||||
},
|
||||
|
||||
info({ message, duration = defaultDuration }: ConfigProps) {
|
||||
const dom = document.createElement('div');
|
||||
const JSXdom = (
|
||||
<Notification message={message} duration={duration} type="warning" />
|
||||
);
|
||||
ReactDOM.render(JSXdom, dom);
|
||||
document.body.appendChild(dom);
|
||||
},
|
||||
};
|
||||
|
||||
export default notification;
|
43
src/hooks/useToast.ts
Normal file
43
src/hooks/useToast.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
type SeverityType = 'success' | 'error' | 'info' | 'warning';
|
||||
|
||||
interface ToastState {
|
||||
open: boolean;
|
||||
message: string;
|
||||
severity: SeverityType;
|
||||
}
|
||||
|
||||
interface ToastActions {
|
||||
handleOpen: (toastMessage: string, toastSeverity: SeverityType) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
type Toast = ToastState & ToastActions;
|
||||
|
||||
const useToast = (): Toast => {
|
||||
const [state, setState] = useState<ToastState>({
|
||||
open: false,
|
||||
message: '',
|
||||
severity: 'success',
|
||||
});
|
||||
|
||||
const handleOpen = (toastMessage: string, toastSeverity: SeverityType) => {
|
||||
setState({
|
||||
open: true,
|
||||
message: toastMessage,
|
||||
severity: toastSeverity,
|
||||
});
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setState({
|
||||
...state,
|
||||
open: false,
|
||||
});
|
||||
};
|
||||
|
||||
return { ...state, handleOpen, handleClose };
|
||||
};
|
||||
|
||||
export default useToast;
|
65
src/main.tsx
65
src/main.tsx
@ -1,16 +1,67 @@
|
||||
import createCache from '@emotion/cache';
|
||||
import { CacheProvider } from '@emotion/react';
|
||||
import createTheme from '@mui/material/styles/createTheme';
|
||||
import ThemeProvider from '@mui/material/styles/ThemeProvider';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './index.css';
|
||||
|
||||
ReactDOM.createRoot(
|
||||
(() => {
|
||||
// 注入应用容器
|
||||
const container = document.createElement('div');
|
||||
container.id = 'monkey';
|
||||
container.style.all = 'initial';
|
||||
document.body.appendChild(container);
|
||||
|
||||
// 创建 shadow DOM
|
||||
const shadowContainer = container.attachShadow({ mode: 'closed' });
|
||||
|
||||
// 将应用添加到 shadow DOM 中
|
||||
const app = document.createElement('div');
|
||||
document.body.append(app);
|
||||
return app;
|
||||
})()
|
||||
).render(
|
||||
shadowContainer.appendChild(app);
|
||||
|
||||
// 将 emotion 样式添加到 shadow DOM 中
|
||||
const emotionRoot = document.createElement('style');
|
||||
shadowContainer.appendChild(emotionRoot);
|
||||
|
||||
const cache = createCache({
|
||||
key: 'css',
|
||||
prepend: true,
|
||||
container: emotionRoot,
|
||||
});
|
||||
|
||||
// 获取浏览器的字体大小
|
||||
const { fontSize } = window.getComputedStyle(document.documentElement);
|
||||
const htmlFontSize = parseFloat(fontSize);
|
||||
|
||||
const theme = createTheme({
|
||||
components: {
|
||||
MuiPopover: {
|
||||
defaultProps: {
|
||||
container: app,
|
||||
},
|
||||
},
|
||||
MuiPopper: {
|
||||
defaultProps: {
|
||||
container: app,
|
||||
},
|
||||
},
|
||||
MuiModal: {
|
||||
defaultProps: {
|
||||
container: app,
|
||||
},
|
||||
},
|
||||
},
|
||||
typography: {
|
||||
htmlFontSize,
|
||||
},
|
||||
});
|
||||
|
||||
ReactDOM.createRoot(app).render(
|
||||
<React.StrictMode>
|
||||
<CacheProvider value={cache}>
|
||||
<ThemeProvider theme={theme}>
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</CacheProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
@ -12,7 +12,7 @@ export default defineConfig({
|
||||
name: 'Session Magician',
|
||||
namespace: 'https://www.imbytecat.com/',
|
||||
icon: 'https://vitejs.dev/logo.svg',
|
||||
version: '2.2.3',
|
||||
version: '3.1.0',
|
||||
description: 'Session Magician & Session Tools & Export/Import Sessions',
|
||||
author: 'imbytecat',
|
||||
match: ['*://*/*'],
|
||||
|
Loading…
x
Reference in New Issue
Block a user