feat: implement shadow DOM; make notification hookable

This commit is contained in:
Liam Chan 2023-03-22 15:10:42 +08:00
parent 9a5ec9f0a1
commit 313d24d808
9 changed files with 154 additions and 126 deletions

View File

@ -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>

View File

@ -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
View File

@ -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

View File

@ -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>
</>
);
};

View File

@ -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
View 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;

View File

View File

@ -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 app = document.createElement('div');
document.body.append(app);
return app;
})()
).render(
// 注入应用容器
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');
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>
<App />
<CacheProvider value={cache}>
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</CacheProvider>
</React.StrictMode>
);

View File

@ -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: ['*://*/*'],