authscape 1.0.738 → 1.0.742
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +479 -45
- package/package.json +10 -2
- package/readme.md +103 -103
- package/src/components/AuthScapeApp.js +0 -1
- package/src/components/LexicalEditor.js +470 -0
- package/src/components/RichTextEditor.js +33 -55
package/readme.md
CHANGED
|
@@ -1,103 +1,103 @@
|
|
|
1
|
-
# AuthScape NPM Package
|
|
2
|
-
|
|
3
|
-
Complete authentication and user management solution for Next.js applications.
|
|
4
|
-
|
|
5
|
-
## Installation
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install authscape
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quick Start
|
|
12
|
-
|
|
13
|
-
See the main [AuthScape Documentation](https://authscape.com/docs) for complete setup instructions.
|
|
14
|
-
|
|
15
|
-
## Features
|
|
16
|
-
|
|
17
|
-
### Core Features
|
|
18
|
-
- OAuth2/PKCE Authentication
|
|
19
|
-
- Multi-tenant support
|
|
20
|
-
- User management
|
|
21
|
-
- Role-based permissions
|
|
22
|
-
- Analytics integration (GA4, Microsoft Clarity)
|
|
23
|
-
- Material-UI components
|
|
24
|
-
|
|
25
|
-
### Components
|
|
26
|
-
- Document Manager
|
|
27
|
-
- File Uploader
|
|
28
|
-
- Rich Text Editor
|
|
29
|
-
- Data Tables
|
|
30
|
-
- Stripe Payment Integration
|
|
31
|
-
- Google Maps Integration
|
|
32
|
-
- And more...
|
|
33
|
-
|
|
34
|
-
## Additional Features
|
|
35
|
-
|
|
36
|
-
### Sitemap Generation (NEW!)
|
|
37
|
-
|
|
38
|
-
Automatically generate SEO-friendly sitemaps for your Next.js application.
|
|
39
|
-
|
|
40
|
-
**Automatic setup on install:**
|
|
41
|
-
|
|
42
|
-
When you run `npm install authscape`, a sitemap is automatically configured at `/sitemap.xml` that syncs with your AuthScape content.
|
|
43
|
-
|
|
44
|
-
- Supports both Pages Router and App Router
|
|
45
|
-
- Automatically detects your Next.js project structure
|
|
46
|
-
- Works with both `pages/` and `src/pages/` layouts
|
|
47
|
-
- Works with both `app/` and `src/app/` layouts
|
|
48
|
-
|
|
49
|
-
**To disable:** Simply delete the auto-generated file:
|
|
50
|
-
- Pages Router: `pages/sitemap.xml.js`
|
|
51
|
-
- App Router: `app/sitemap.xml/route.js`
|
|
52
|
-
|
|
53
|
-
## Environment Variables
|
|
54
|
-
|
|
55
|
-
Required environment variables in your `.env.local`:
|
|
56
|
-
|
|
57
|
-
```env
|
|
58
|
-
apiUri=https://your-authscape-api.com
|
|
59
|
-
authorityUri=https://your-auth-server.com
|
|
60
|
-
client_id=your-client-id
|
|
61
|
-
client_secret=your-client-secret
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Optional analytics:
|
|
65
|
-
|
|
66
|
-
```env
|
|
67
|
-
googleAnalytics4=G-XXXXXXXXXX
|
|
68
|
-
microsoftClarityTrackingCode=xxxxxxxxxx
|
|
69
|
-
enableDatabaseAnalytics=true
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
## Usage Example
|
|
73
|
-
|
|
74
|
-
```javascript
|
|
75
|
-
// pages/_app.js
|
|
76
|
-
import { AuthScapeApp } from 'authscape';
|
|
77
|
-
import 'react-toastify/dist/ReactToastify.css';
|
|
78
|
-
|
|
79
|
-
function MyApp({ Component, pageProps }) {
|
|
80
|
-
return (
|
|
81
|
-
<AuthScapeApp
|
|
82
|
-
Component={Component}
|
|
83
|
-
pageProps={pageProps}
|
|
84
|
-
enforceLoggedIn={false}
|
|
85
|
-
enableAuth={true}
|
|
86
|
-
/>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export default MyApp;
|
|
91
|
-
```
|
|
92
|
-
|
|
93
|
-
## Documentation
|
|
94
|
-
|
|
95
|
-
- [AuthScape Docs](https://authscape.com/docs) - Complete documentation
|
|
96
|
-
|
|
97
|
-
## Support
|
|
98
|
-
|
|
99
|
-
For issues or questions, contact AuthScape support or visit [authscape.com](https://authscape.com).
|
|
100
|
-
|
|
101
|
-
## License
|
|
102
|
-
|
|
103
|
-
ISC
|
|
1
|
+
# AuthScape NPM Package
|
|
2
|
+
|
|
3
|
+
Complete authentication and user management solution for Next.js applications.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install authscape
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
See the main [AuthScape Documentation](https://authscape.com/docs) for complete setup instructions.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
### Core Features
|
|
18
|
+
- OAuth2/PKCE Authentication
|
|
19
|
+
- Multi-tenant support
|
|
20
|
+
- User management
|
|
21
|
+
- Role-based permissions
|
|
22
|
+
- Analytics integration (GA4, Microsoft Clarity)
|
|
23
|
+
- Material-UI components
|
|
24
|
+
|
|
25
|
+
### Components
|
|
26
|
+
- Document Manager
|
|
27
|
+
- File Uploader
|
|
28
|
+
- Rich Text Editor
|
|
29
|
+
- Data Tables
|
|
30
|
+
- Stripe Payment Integration
|
|
31
|
+
- Google Maps Integration
|
|
32
|
+
- And more...
|
|
33
|
+
|
|
34
|
+
## Additional Features
|
|
35
|
+
|
|
36
|
+
### Sitemap Generation (NEW!)
|
|
37
|
+
|
|
38
|
+
Automatically generate SEO-friendly sitemaps for your Next.js application.
|
|
39
|
+
|
|
40
|
+
**Automatic setup on install:**
|
|
41
|
+
|
|
42
|
+
When you run `npm install authscape`, a sitemap is automatically configured at `/sitemap.xml` that syncs with your AuthScape content.
|
|
43
|
+
|
|
44
|
+
- Supports both Pages Router and App Router
|
|
45
|
+
- Automatically detects your Next.js project structure
|
|
46
|
+
- Works with both `pages/` and `src/pages/` layouts
|
|
47
|
+
- Works with both `app/` and `src/app/` layouts
|
|
48
|
+
|
|
49
|
+
**To disable:** Simply delete the auto-generated file:
|
|
50
|
+
- Pages Router: `pages/sitemap.xml.js`
|
|
51
|
+
- App Router: `app/sitemap.xml/route.js`
|
|
52
|
+
|
|
53
|
+
## Environment Variables
|
|
54
|
+
|
|
55
|
+
Required environment variables in your `.env.local`:
|
|
56
|
+
|
|
57
|
+
```env
|
|
58
|
+
apiUri=https://your-authscape-api.com
|
|
59
|
+
authorityUri=https://your-auth-server.com
|
|
60
|
+
client_id=your-client-id
|
|
61
|
+
client_secret=your-client-secret
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Optional analytics:
|
|
65
|
+
|
|
66
|
+
```env
|
|
67
|
+
googleAnalytics4=G-XXXXXXXXXX
|
|
68
|
+
microsoftClarityTrackingCode=xxxxxxxxxx
|
|
69
|
+
enableDatabaseAnalytics=true
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage Example
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
// pages/_app.js
|
|
76
|
+
import { AuthScapeApp } from 'authscape';
|
|
77
|
+
import 'react-toastify/dist/ReactToastify.css';
|
|
78
|
+
|
|
79
|
+
function MyApp({ Component, pageProps }) {
|
|
80
|
+
return (
|
|
81
|
+
<AuthScapeApp
|
|
82
|
+
Component={Component}
|
|
83
|
+
pageProps={pageProps}
|
|
84
|
+
enforceLoggedIn={false}
|
|
85
|
+
enableAuth={true}
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default MyApp;
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Documentation
|
|
94
|
+
|
|
95
|
+
- [AuthScape Docs](https://authscape.com/docs) - Complete documentation
|
|
96
|
+
|
|
97
|
+
## Support
|
|
98
|
+
|
|
99
|
+
For issues or questions, contact AuthScape support or visit [authscape.com](https://authscape.com).
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
ISC
|
|
@@ -9,7 +9,6 @@ import Router from "next/router";
|
|
|
9
9
|
import GA4React from "ga-4-react";
|
|
10
10
|
import { create } from "zustand";
|
|
11
11
|
import { clarity } from "react-microsoft-clarity";
|
|
12
|
-
import { authService } from "authscape";
|
|
13
12
|
|
|
14
13
|
// ---- optional: import your cookie util if not global ----
|
|
15
14
|
// import { setCookie } from "cookies-next";
|
|
@@ -0,0 +1,470 @@
|
|
|
1
|
+
import React, { useEffect, useCallback } from 'react';
|
|
2
|
+
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
3
|
+
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
4
|
+
import { ContentEditable } from '@lexical/react/LexicalContentEditable';
|
|
5
|
+
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
6
|
+
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
7
|
+
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
8
|
+
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
9
|
+
import { ListPlugin } from '@lexical/react/LexicalListPlugin';
|
|
10
|
+
import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin';
|
|
11
|
+
import { $generateHtmlFromNodes, $generateNodesFromDOM } from '@lexical/html';
|
|
12
|
+
import { $getRoot, $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, $createParagraphNode } from 'lexical';
|
|
13
|
+
import { $setBlocksType } from '@lexical/selection';
|
|
14
|
+
import { $createHeadingNode, $createQuoteNode, HeadingNode, QuoteNode } from '@lexical/rich-text';
|
|
15
|
+
import { ListItemNode, ListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND } from '@lexical/list';
|
|
16
|
+
import { LinkNode, TOGGLE_LINK_COMMAND } from '@lexical/link';
|
|
17
|
+
import { CodeNode } from '@lexical/code';
|
|
18
|
+
import Box from '@mui/material/Box';
|
|
19
|
+
import IconButton from '@mui/material/IconButton';
|
|
20
|
+
import Divider from '@mui/material/Divider';
|
|
21
|
+
import Select from '@mui/material/Select';
|
|
22
|
+
import MenuItem from '@mui/material/MenuItem';
|
|
23
|
+
import Tooltip from '@mui/material/Tooltip';
|
|
24
|
+
import FormatBoldIcon from '@mui/icons-material/FormatBold';
|
|
25
|
+
import FormatItalicIcon from '@mui/icons-material/FormatItalic';
|
|
26
|
+
import FormatUnderlinedIcon from '@mui/icons-material/FormatUnderlined';
|
|
27
|
+
import StrikethroughSIcon from '@mui/icons-material/StrikethroughS';
|
|
28
|
+
import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted';
|
|
29
|
+
import FormatListNumberedIcon from '@mui/icons-material/FormatListNumbered';
|
|
30
|
+
import FormatQuoteIcon from '@mui/icons-material/FormatQuote';
|
|
31
|
+
import CodeIcon from '@mui/icons-material/Code';
|
|
32
|
+
import LinkIcon from '@mui/icons-material/Link';
|
|
33
|
+
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
|
34
|
+
import UndoIcon from '@mui/icons-material/Undo';
|
|
35
|
+
import RedoIcon from '@mui/icons-material/Redo';
|
|
36
|
+
|
|
37
|
+
const theme = {
|
|
38
|
+
paragraph: 'lexical-paragraph',
|
|
39
|
+
quote: 'lexical-quote',
|
|
40
|
+
heading: {
|
|
41
|
+
h1: 'lexical-h1',
|
|
42
|
+
h2: 'lexical-h2',
|
|
43
|
+
h3: 'lexical-h3',
|
|
44
|
+
h4: 'lexical-h4',
|
|
45
|
+
h5: 'lexical-h5',
|
|
46
|
+
h6: 'lexical-h6',
|
|
47
|
+
},
|
|
48
|
+
list: {
|
|
49
|
+
nested: {
|
|
50
|
+
listitem: 'lexical-nested-listitem',
|
|
51
|
+
},
|
|
52
|
+
ol: 'lexical-ol',
|
|
53
|
+
ul: 'lexical-ul',
|
|
54
|
+
listitem: 'lexical-listitem',
|
|
55
|
+
},
|
|
56
|
+
text: {
|
|
57
|
+
bold: 'lexical-bold',
|
|
58
|
+
italic: 'lexical-italic',
|
|
59
|
+
underline: 'lexical-underline',
|
|
60
|
+
strikethrough: 'lexical-strikethrough',
|
|
61
|
+
code: 'lexical-code',
|
|
62
|
+
},
|
|
63
|
+
link: 'lexical-link',
|
|
64
|
+
code: 'lexical-code-block',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const editorStyles = `
|
|
68
|
+
.lexical-editor-container {
|
|
69
|
+
border: 1px solid #ccc;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
72
|
+
}
|
|
73
|
+
.lexical-editor-inner {
|
|
74
|
+
position: relative;
|
|
75
|
+
}
|
|
76
|
+
.lexical-editor-input {
|
|
77
|
+
padding: 12px;
|
|
78
|
+
outline: none;
|
|
79
|
+
overflow-y: auto;
|
|
80
|
+
}
|
|
81
|
+
.lexical-editor-input:focus {
|
|
82
|
+
outline: none;
|
|
83
|
+
}
|
|
84
|
+
.lexical-placeholder {
|
|
85
|
+
position: absolute;
|
|
86
|
+
top: 12px;
|
|
87
|
+
left: 12px;
|
|
88
|
+
color: #999;
|
|
89
|
+
pointer-events: none;
|
|
90
|
+
user-select: none;
|
|
91
|
+
}
|
|
92
|
+
.lexical-paragraph {
|
|
93
|
+
margin: 0 0 8px 0;
|
|
94
|
+
}
|
|
95
|
+
.lexical-h1 { font-size: 2em; font-weight: bold; margin: 16px 0 8px 0; }
|
|
96
|
+
.lexical-h2 { font-size: 1.5em; font-weight: bold; margin: 14px 0 8px 0; }
|
|
97
|
+
.lexical-h3 { font-size: 1.17em; font-weight: bold; margin: 12px 0 8px 0; }
|
|
98
|
+
.lexical-h4 { font-size: 1em; font-weight: bold; margin: 10px 0 8px 0; }
|
|
99
|
+
.lexical-h5 { font-size: 0.83em; font-weight: bold; margin: 8px 0 8px 0; }
|
|
100
|
+
.lexical-h6 { font-size: 0.67em; font-weight: bold; margin: 6px 0 8px 0; }
|
|
101
|
+
.lexical-quote {
|
|
102
|
+
margin: 8px 0;
|
|
103
|
+
padding: 8px 16px;
|
|
104
|
+
border-left: 4px solid #ccc;
|
|
105
|
+
background: #f9f9f9;
|
|
106
|
+
font-style: italic;
|
|
107
|
+
}
|
|
108
|
+
.lexical-ul, .lexical-ol {
|
|
109
|
+
margin: 8px 0;
|
|
110
|
+
padding-left: 24px;
|
|
111
|
+
}
|
|
112
|
+
.lexical-listitem {
|
|
113
|
+
margin: 4px 0;
|
|
114
|
+
}
|
|
115
|
+
.lexical-bold { font-weight: bold; }
|
|
116
|
+
.lexical-italic { font-style: italic; }
|
|
117
|
+
.lexical-underline { text-decoration: underline; }
|
|
118
|
+
.lexical-strikethrough { text-decoration: line-through; }
|
|
119
|
+
.lexical-code {
|
|
120
|
+
background: #f0f0f0;
|
|
121
|
+
padding: 2px 4px;
|
|
122
|
+
border-radius: 3px;
|
|
123
|
+
font-family: monospace;
|
|
124
|
+
}
|
|
125
|
+
.lexical-link {
|
|
126
|
+
color: #0066cc;
|
|
127
|
+
text-decoration: underline;
|
|
128
|
+
}
|
|
129
|
+
.lexical-code-block {
|
|
130
|
+
background: #f5f5f5;
|
|
131
|
+
padding: 12px;
|
|
132
|
+
border-radius: 4px;
|
|
133
|
+
font-family: monospace;
|
|
134
|
+
overflow-x: auto;
|
|
135
|
+
}
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
// Toolbar button style to match react-draft-wysiwyg
|
|
139
|
+
const toolbarButtonStyle = {
|
|
140
|
+
minWidth: 32,
|
|
141
|
+
width: 32,
|
|
142
|
+
height: 32,
|
|
143
|
+
padding: '4px',
|
|
144
|
+
margin: '2px',
|
|
145
|
+
borderRadius: '2px',
|
|
146
|
+
border: '1px solid transparent',
|
|
147
|
+
'&:hover': {
|
|
148
|
+
backgroundColor: '#f0f0f0',
|
|
149
|
+
border: '1px solid #ccc',
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const activeButtonStyle = {
|
|
154
|
+
...toolbarButtonStyle,
|
|
155
|
+
backgroundColor: '#e0e0e0',
|
|
156
|
+
border: '1px solid #ccc',
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// HTML Import Plugin
|
|
160
|
+
function HtmlImportPlugin({ initialHtml }) {
|
|
161
|
+
const [editor] = useLexicalComposerContext();
|
|
162
|
+
const [initialized, setInitialized] = React.useState(false);
|
|
163
|
+
|
|
164
|
+
useEffect(() => {
|
|
165
|
+
if (initialHtml && !initialized) {
|
|
166
|
+
editor.update(() => {
|
|
167
|
+
const parser = new DOMParser();
|
|
168
|
+
const dom = parser.parseFromString(initialHtml, 'text/html');
|
|
169
|
+
const nodes = $generateNodesFromDOM(editor, dom);
|
|
170
|
+
const root = $getRoot();
|
|
171
|
+
root.clear();
|
|
172
|
+
nodes.forEach(node => root.append(node));
|
|
173
|
+
});
|
|
174
|
+
setInitialized(true);
|
|
175
|
+
}
|
|
176
|
+
}, [editor, initialHtml, initialized]);
|
|
177
|
+
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// HTML Export Plugin
|
|
182
|
+
function HtmlExportPlugin({ onChange }) {
|
|
183
|
+
const [editor] = useLexicalComposerContext();
|
|
184
|
+
|
|
185
|
+
const handleChange = useCallback(() => {
|
|
186
|
+
editor.update(() => {
|
|
187
|
+
const html = $generateHtmlFromNodes(editor, null);
|
|
188
|
+
onChange(html);
|
|
189
|
+
});
|
|
190
|
+
}, [editor, onChange]);
|
|
191
|
+
|
|
192
|
+
return <OnChangePlugin onChange={handleChange} />;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Toolbar Component
|
|
196
|
+
function Toolbar({ isDisabled }) {
|
|
197
|
+
const [editor] = useLexicalComposerContext();
|
|
198
|
+
const [isBold, setIsBold] = React.useState(false);
|
|
199
|
+
const [isItalic, setIsItalic] = React.useState(false);
|
|
200
|
+
const [isUnderline, setIsUnderline] = React.useState(false);
|
|
201
|
+
const [isStrikethrough, setIsStrikethrough] = React.useState(false);
|
|
202
|
+
const [isCode, setIsCode] = React.useState(false);
|
|
203
|
+
const [isLink, setIsLink] = React.useState(false);
|
|
204
|
+
const [blockType, setBlockType] = React.useState('paragraph');
|
|
205
|
+
|
|
206
|
+
const updateToolbar = useCallback(() => {
|
|
207
|
+
const selection = $getSelection();
|
|
208
|
+
if ($isRangeSelection(selection)) {
|
|
209
|
+
setIsBold(selection.hasFormat('bold'));
|
|
210
|
+
setIsItalic(selection.hasFormat('italic'));
|
|
211
|
+
setIsUnderline(selection.hasFormat('underline'));
|
|
212
|
+
setIsStrikethrough(selection.hasFormat('strikethrough'));
|
|
213
|
+
setIsCode(selection.hasFormat('code'));
|
|
214
|
+
}
|
|
215
|
+
}, []);
|
|
216
|
+
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
return editor.registerUpdateListener(({ editorState }) => {
|
|
219
|
+
editorState.read(() => {
|
|
220
|
+
updateToolbar();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}, [editor, updateToolbar]);
|
|
224
|
+
|
|
225
|
+
const formatText = (format) => {
|
|
226
|
+
editor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
const formatBlock = (type) => {
|
|
230
|
+
editor.update(() => {
|
|
231
|
+
const selection = $getSelection();
|
|
232
|
+
if ($isRangeSelection(selection)) {
|
|
233
|
+
if (type === 'paragraph') {
|
|
234
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
235
|
+
} else if (type === 'quote') {
|
|
236
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
237
|
+
} else if (type.startsWith('h')) {
|
|
238
|
+
$setBlocksType(selection, () => $createHeadingNode(type));
|
|
239
|
+
}
|
|
240
|
+
setBlockType(type);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
const formatList = (listType) => {
|
|
246
|
+
if (listType === 'bullet') {
|
|
247
|
+
editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
248
|
+
} else if (listType === 'number') {
|
|
249
|
+
editor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const insertLink = () => {
|
|
254
|
+
const url = prompt('Enter URL:');
|
|
255
|
+
if (url) {
|
|
256
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const removeLink = () => {
|
|
261
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, null);
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const undo = () => {
|
|
265
|
+
editor.dispatchCommand('UNDO', undefined);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const redo = () => {
|
|
269
|
+
editor.dispatchCommand('REDO', undefined);
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
if (isDisabled) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return (
|
|
277
|
+
<Box
|
|
278
|
+
sx={{
|
|
279
|
+
display: 'flex',
|
|
280
|
+
flexWrap: 'wrap',
|
|
281
|
+
alignItems: 'center',
|
|
282
|
+
padding: '8px',
|
|
283
|
+
borderBottom: '1px solid #ccc',
|
|
284
|
+
backgroundColor: '#f9f9f9',
|
|
285
|
+
gap: '4px',
|
|
286
|
+
}}
|
|
287
|
+
>
|
|
288
|
+
{/* Block Type Dropdown */}
|
|
289
|
+
<Select
|
|
290
|
+
size="small"
|
|
291
|
+
value={blockType}
|
|
292
|
+
onChange={(e) => formatBlock(e.target.value)}
|
|
293
|
+
sx={{ minWidth: 120, height: 32, mr: 1 }}
|
|
294
|
+
>
|
|
295
|
+
<MenuItem value="paragraph">Normal</MenuItem>
|
|
296
|
+
<MenuItem value="h1">Heading 1</MenuItem>
|
|
297
|
+
<MenuItem value="h2">Heading 2</MenuItem>
|
|
298
|
+
<MenuItem value="h3">Heading 3</MenuItem>
|
|
299
|
+
<MenuItem value="h4">Heading 4</MenuItem>
|
|
300
|
+
<MenuItem value="h5">Heading 5</MenuItem>
|
|
301
|
+
<MenuItem value="h6">Heading 6</MenuItem>
|
|
302
|
+
<MenuItem value="quote">Blockquote</MenuItem>
|
|
303
|
+
</Select>
|
|
304
|
+
|
|
305
|
+
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
306
|
+
|
|
307
|
+
{/* Text Formatting */}
|
|
308
|
+
<Tooltip title="Bold (Ctrl+B)">
|
|
309
|
+
<IconButton
|
|
310
|
+
size="small"
|
|
311
|
+
onClick={() => formatText('bold')}
|
|
312
|
+
sx={isBold ? activeButtonStyle : toolbarButtonStyle}
|
|
313
|
+
>
|
|
314
|
+
<FormatBoldIcon fontSize="small" />
|
|
315
|
+
</IconButton>
|
|
316
|
+
</Tooltip>
|
|
317
|
+
|
|
318
|
+
<Tooltip title="Italic (Ctrl+I)">
|
|
319
|
+
<IconButton
|
|
320
|
+
size="small"
|
|
321
|
+
onClick={() => formatText('italic')}
|
|
322
|
+
sx={isItalic ? activeButtonStyle : toolbarButtonStyle}
|
|
323
|
+
>
|
|
324
|
+
<FormatItalicIcon fontSize="small" />
|
|
325
|
+
</IconButton>
|
|
326
|
+
</Tooltip>
|
|
327
|
+
|
|
328
|
+
<Tooltip title="Underline (Ctrl+U)">
|
|
329
|
+
<IconButton
|
|
330
|
+
size="small"
|
|
331
|
+
onClick={() => formatText('underline')}
|
|
332
|
+
sx={isUnderline ? activeButtonStyle : toolbarButtonStyle}
|
|
333
|
+
>
|
|
334
|
+
<FormatUnderlinedIcon fontSize="small" />
|
|
335
|
+
</IconButton>
|
|
336
|
+
</Tooltip>
|
|
337
|
+
|
|
338
|
+
<Tooltip title="Strikethrough">
|
|
339
|
+
<IconButton
|
|
340
|
+
size="small"
|
|
341
|
+
onClick={() => formatText('strikethrough')}
|
|
342
|
+
sx={isStrikethrough ? activeButtonStyle : toolbarButtonStyle}
|
|
343
|
+
>
|
|
344
|
+
<StrikethroughSIcon fontSize="small" />
|
|
345
|
+
</IconButton>
|
|
346
|
+
</Tooltip>
|
|
347
|
+
|
|
348
|
+
<Tooltip title="Code">
|
|
349
|
+
<IconButton
|
|
350
|
+
size="small"
|
|
351
|
+
onClick={() => formatText('code')}
|
|
352
|
+
sx={isCode ? activeButtonStyle : toolbarButtonStyle}
|
|
353
|
+
>
|
|
354
|
+
<CodeIcon fontSize="small" />
|
|
355
|
+
</IconButton>
|
|
356
|
+
</Tooltip>
|
|
357
|
+
|
|
358
|
+
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
359
|
+
|
|
360
|
+
{/* Lists */}
|
|
361
|
+
<Tooltip title="Bullet List">
|
|
362
|
+
<IconButton
|
|
363
|
+
size="small"
|
|
364
|
+
onClick={() => formatList('bullet')}
|
|
365
|
+
sx={toolbarButtonStyle}
|
|
366
|
+
>
|
|
367
|
+
<FormatListBulletedIcon fontSize="small" />
|
|
368
|
+
</IconButton>
|
|
369
|
+
</Tooltip>
|
|
370
|
+
|
|
371
|
+
<Tooltip title="Numbered List">
|
|
372
|
+
<IconButton
|
|
373
|
+
size="small"
|
|
374
|
+
onClick={() => formatList('number')}
|
|
375
|
+
sx={toolbarButtonStyle}
|
|
376
|
+
>
|
|
377
|
+
<FormatListNumberedIcon fontSize="small" />
|
|
378
|
+
</IconButton>
|
|
379
|
+
</Tooltip>
|
|
380
|
+
|
|
381
|
+
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
382
|
+
|
|
383
|
+
{/* Links */}
|
|
384
|
+
<Tooltip title="Insert Link">
|
|
385
|
+
<IconButton
|
|
386
|
+
size="small"
|
|
387
|
+
onClick={insertLink}
|
|
388
|
+
sx={toolbarButtonStyle}
|
|
389
|
+
>
|
|
390
|
+
<LinkIcon fontSize="small" />
|
|
391
|
+
</IconButton>
|
|
392
|
+
</Tooltip>
|
|
393
|
+
|
|
394
|
+
<Tooltip title="Remove Link">
|
|
395
|
+
<IconButton
|
|
396
|
+
size="small"
|
|
397
|
+
onClick={removeLink}
|
|
398
|
+
sx={toolbarButtonStyle}
|
|
399
|
+
>
|
|
400
|
+
<LinkOffIcon fontSize="small" />
|
|
401
|
+
</IconButton>
|
|
402
|
+
</Tooltip>
|
|
403
|
+
|
|
404
|
+
<Divider orientation="vertical" flexItem sx={{ mx: 1 }} />
|
|
405
|
+
|
|
406
|
+
{/* Undo/Redo */}
|
|
407
|
+
<Tooltip title="Undo (Ctrl+Z)">
|
|
408
|
+
<IconButton
|
|
409
|
+
size="small"
|
|
410
|
+
onClick={undo}
|
|
411
|
+
sx={toolbarButtonStyle}
|
|
412
|
+
>
|
|
413
|
+
<UndoIcon fontSize="small" />
|
|
414
|
+
</IconButton>
|
|
415
|
+
</Tooltip>
|
|
416
|
+
|
|
417
|
+
<Tooltip title="Redo (Ctrl+Y)">
|
|
418
|
+
<IconButton
|
|
419
|
+
size="small"
|
|
420
|
+
onClick={redo}
|
|
421
|
+
sx={toolbarButtonStyle}
|
|
422
|
+
>
|
|
423
|
+
<RedoIcon fontSize="small" />
|
|
424
|
+
</IconButton>
|
|
425
|
+
</Tooltip>
|
|
426
|
+
</Box>
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
function onError(error) {
|
|
431
|
+
console.error('Lexical error:', error);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function LexicalEditor({ initialHtml, onChange, height = 400, isDisabled = false }) {
|
|
435
|
+
const initialConfig = {
|
|
436
|
+
namespace: 'RichTextEditor',
|
|
437
|
+
theme,
|
|
438
|
+
onError,
|
|
439
|
+
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, CodeNode],
|
|
440
|
+
editable: !isDisabled,
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return (
|
|
444
|
+
<>
|
|
445
|
+
<style>{editorStyles}</style>
|
|
446
|
+
<LexicalComposer initialConfig={initialConfig}>
|
|
447
|
+
<div className="lexical-editor-container">
|
|
448
|
+
<Toolbar isDisabled={isDisabled} />
|
|
449
|
+
<div className="lexical-editor-inner">
|
|
450
|
+
<RichTextPlugin
|
|
451
|
+
contentEditable={
|
|
452
|
+
<ContentEditable
|
|
453
|
+
className="lexical-editor-input"
|
|
454
|
+
style={{ minHeight: height, maxHeight: height }}
|
|
455
|
+
/>
|
|
456
|
+
}
|
|
457
|
+
placeholder={<div className="lexical-placeholder">Enter text...</div>}
|
|
458
|
+
ErrorBoundary={LexicalErrorBoundary}
|
|
459
|
+
/>
|
|
460
|
+
<HistoryPlugin />
|
|
461
|
+
<ListPlugin />
|
|
462
|
+
<LinkPlugin />
|
|
463
|
+
<HtmlImportPlugin initialHtml={initialHtml} />
|
|
464
|
+
<HtmlExportPlugin onChange={onChange} />
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</LexicalComposer>
|
|
468
|
+
</>
|
|
469
|
+
);
|
|
470
|
+
}
|