|
1 | 1 | 'use client' |
| 2 | +import type React from 'react' |
| 3 | + |
| 4 | +import Color from '@tiptap/extension-color' |
| 5 | +import Link from '@tiptap/extension-link' |
| 6 | +import TextAlign from '@tiptap/extension-text-align' |
| 7 | +import TextStyle from '@tiptap/extension-text-style' |
| 8 | +import Underline from '@tiptap/extension-underline' |
| 9 | +import StarterKit from '@tiptap/starter-kit' |
| 10 | + |
2 | 11 | import { |
3 | 12 | EditorBar, |
4 | | - EditorBarGroup, |
5 | | - EditorBgColorPicker, |
6 | 13 | EditorTextColorPicker, |
7 | 14 | MarkButton, |
8 | 15 | RedoButton, |
9 | 16 | SelectTextStyle, |
10 | 17 | TextAlignButton, |
11 | 18 | TextAlignButtonsGroup, |
12 | | - ToggleGroup, |
13 | 19 | ToolbarGroup, |
14 | 20 | ToolbarSeparator, |
15 | 21 | UndoButton, |
16 | 22 | UploadImageButton, |
17 | 23 | } from '@genseki/react' |
| 24 | +import { |
| 25 | + BackColorExtension, |
| 26 | + CustomImageExtension, |
| 27 | + ImageUploadNodeExtension, |
| 28 | + SelectionExtension, |
| 29 | +} from '@genseki/react' |
18 | 30 |
|
19 | 31 | export const EditorSlotBefore = () => { |
20 | 32 | return ( |
21 | 33 | <EditorBar> |
22 | | - <EditorBarGroup> |
23 | | - <EditorBgColorPicker /> |
24 | | - <EditorTextColorPicker /> |
25 | | - <SelectTextStyle /> |
26 | | - </EditorBarGroup> |
27 | | - <EditorBarGroup className="items-center"> |
28 | | - <UndoButton /> |
29 | | - <RedoButton /> |
30 | | - </EditorBarGroup> |
31 | | - <ToolbarSeparator className="h-auto" /> |
| 34 | + <SelectTextStyle /> |
32 | 35 | <ToolbarGroup className="items-center"> |
33 | 36 | <MarkButton type="bold" /> |
34 | 37 | <MarkButton type="italic" /> |
35 | 38 | <MarkButton type="underline" /> |
36 | | - <MarkButton type="strike" /> |
| 39 | + <MarkButton type="link" /> |
37 | 40 | </ToolbarGroup> |
38 | 41 | <ToolbarSeparator className="h-auto" /> |
39 | | - <ToggleGroup className="items-center"> |
40 | | - <MarkButton type="bulletList" /> |
41 | | - </ToggleGroup> |
| 42 | + <EditorTextColorPicker /> |
42 | 43 | <ToolbarSeparator className="h-auto" /> |
43 | 44 | <ToolbarGroup className="items-center"> |
44 | 45 | <TextAlignButtonsGroup> |
45 | 46 | <TextAlignButton type="left" /> |
46 | 47 | <TextAlignButton type="center" /> |
47 | 48 | <TextAlignButton type="right" /> |
48 | | - <TextAlignButton type="justify" /> |
| 49 | + <MarkButton type="bulletList" /> |
49 | 50 | </TextAlignButtonsGroup> |
50 | 51 | </ToolbarGroup> |
51 | 52 | <ToolbarSeparator /> |
52 | 53 | <ToolbarSeparator className="h-auto" /> |
53 | 54 | <UploadImageButton /> |
| 55 | + <RedoButton /> |
| 56 | + <UndoButton /> |
54 | 57 | </EditorBar> |
55 | 58 | ) |
56 | 59 | } |
| 60 | + |
| 61 | +export const postEditorProviderProps = { |
| 62 | + immediatelyRender: false, |
| 63 | + shouldRerenderOnTransaction: true, |
| 64 | + content: { |
| 65 | + type: 'doc', |
| 66 | + content: [ |
| 67 | + { |
| 68 | + type: 'paragraph', |
| 69 | + content: [ |
| 70 | + { |
| 71 | + type: 'text', |
| 72 | + text: '', |
| 73 | + }, |
| 74 | + ], |
| 75 | + }, |
| 76 | + ], |
| 77 | + }, |
| 78 | + slotBefore: <EditorSlotBefore />, |
| 79 | + extensions: [ |
| 80 | + Color, |
| 81 | + BackColorExtension, |
| 82 | + Underline.configure({ HTMLAttributes: { class: 'earth-underline' } }), |
| 83 | + SelectionExtension, |
| 84 | + TextStyle, |
| 85 | + TextAlign.configure({ |
| 86 | + types: ['heading', 'paragraph'], |
| 87 | + alignments: ['left', 'center', 'right', 'justify'], |
| 88 | + defaultAlignment: 'left', |
| 89 | + }), |
| 90 | + StarterKit.configure({ |
| 91 | + bold: { HTMLAttributes: { class: 'bold large-black' } }, |
| 92 | + paragraph: { HTMLAttributes: { class: 'paragraph-custom' } }, |
| 93 | + heading: { HTMLAttributes: { class: 'heading-custom' } }, |
| 94 | + bulletList: { HTMLAttributes: { class: 'list-custom' } }, |
| 95 | + orderedList: { HTMLAttributes: { class: 'ordered-list' } }, |
| 96 | + code: { HTMLAttributes: { class: 'code' } }, |
| 97 | + codeBlock: { HTMLAttributes: { class: 'code-block' } }, |
| 98 | + horizontalRule: { HTMLAttributes: { class: 'hr-custom' } }, |
| 99 | + italic: { HTMLAttributes: { class: 'italic-text' } }, |
| 100 | + strike: { HTMLAttributes: { class: 'strikethrough' } }, |
| 101 | + blockquote: { HTMLAttributes: { class: 'blockquote-custom' } }, |
| 102 | + }), |
| 103 | + CustomImageExtension.configure({ HTMLAttributes: { className: 'image-displayer' } }), |
| 104 | + ImageUploadNodeExtension.configure({ |
| 105 | + showProgress: false, |
| 106 | + accept: 'image/*', |
| 107 | + maxSize: 1024 * 1024 * 10, // 10MB |
| 108 | + limit: 3, |
| 109 | + pathName: 'posts/rich-text', |
| 110 | + }), |
| 111 | + Link.configure({ |
| 112 | + openOnClick: false, |
| 113 | + autolink: true, |
| 114 | + defaultProtocol: 'https', |
| 115 | + protocols: ['http', 'https'], |
| 116 | + isAllowedUri: (url, ctx) => { |
| 117 | + try { |
| 118 | + // construct URL |
| 119 | + const parsedUrl = url.includes(':') |
| 120 | + ? new URL(url) |
| 121 | + : new URL(`${ctx.defaultProtocol}://${url}`) |
| 122 | + |
| 123 | + // use default validation |
| 124 | + if (!ctx.defaultValidate(parsedUrl.href)) { |
| 125 | + return false |
| 126 | + } |
| 127 | + |
| 128 | + // disallowed protocols |
| 129 | + const disallowedProtocols = ['ftp', 'file', 'mailto'] |
| 130 | + const protocol = parsedUrl.protocol.replace(':', '') |
| 131 | + |
| 132 | + if (disallowedProtocols.includes(protocol)) { |
| 133 | + return false |
| 134 | + } |
| 135 | + |
| 136 | + // only allow protocols specified in ctx.protocols |
| 137 | + const allowedProtocols = ctx.protocols.map((p) => (typeof p === 'string' ? p : p.scheme)) |
| 138 | + |
| 139 | + if (!allowedProtocols.includes(protocol)) { |
| 140 | + return false |
| 141 | + } |
| 142 | + |
| 143 | + // disallowed domains |
| 144 | + const disallowedDomains = ['example-phishing.com', 'malicious-site.net'] |
| 145 | + const domain = parsedUrl.hostname |
| 146 | + |
| 147 | + if (disallowedDomains.includes(domain)) { |
| 148 | + return false |
| 149 | + } |
| 150 | + |
| 151 | + // all checks have passed |
| 152 | + return true |
| 153 | + } catch { |
| 154 | + return false |
| 155 | + } |
| 156 | + }, |
| 157 | + shouldAutoLink: (url) => { |
| 158 | + try { |
| 159 | + // construct URL |
| 160 | + const parsedUrl = url.includes(':') ? new URL(url) : new URL(`https://${url}`) |
| 161 | + |
| 162 | + // only auto-link if the domain is not in the disallowed list |
| 163 | + const disallowedDomains = ['example-no-autolink.com', 'another-no-autolink.com'] |
| 164 | + const domain = parsedUrl.hostname |
| 165 | + |
| 166 | + return !disallowedDomains.includes(domain) |
| 167 | + } catch { |
| 168 | + return false |
| 169 | + } |
| 170 | + }, |
| 171 | + }), |
| 172 | + ], |
| 173 | +} |
0 commit comments