@tuturuuu/ui 0.27.0 → 0.28.0
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/CHANGELOG.md +14 -0
- package/package.json +4 -3
- package/src/components/ui/button.tsx +12 -8
- package/src/components/ui/color-picker.test.tsx +23 -0
- package/src/components/ui/color-picker.tsx +1 -1
- package/src/components/ui/custom/notification-popover-client.tsx +6 -19
- package/src/components/ui/custom/notification-popover-trigger.test.tsx +37 -0
- package/src/components/ui/custom/notification-popover-trigger.tsx +44 -0
- package/src/components/ui/text-editor/__tests__/editor-classes.test.ts +36 -0
- package/src/components/ui/text-editor/__tests__/inline-task-conversion.test.tsx +30 -0
- package/src/components/ui/text-editor/__tests__/keyboard.test.ts +173 -3
- package/src/components/ui/text-editor/__tests__/toggle-block-extension.test.ts +251 -0
- package/src/components/ui/text-editor/editor-classes.ts +100 -0
- package/src/components/ui/text-editor/editor.tsx +11 -153
- package/src/components/ui/text-editor/extensions.ts +19 -0
- package/src/components/ui/text-editor/keyboard.ts +47 -0
- package/src/components/ui/text-editor/toggle-block-extension.ts +153 -0
- package/src/components/ui/text-editor/tool-bar.tsx +24 -73
- package/src/components/ui/text-editor/toolbar-config.ts +67 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Editor, generateHTML, generateJSON } from '@tiptap/core';
|
|
2
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
+
import { getEditorExtensions } from '../extensions';
|
|
4
|
+
|
|
5
|
+
const editors: Editor[] = [];
|
|
6
|
+
|
|
7
|
+
function createEditor(content: Record<string, unknown>) {
|
|
8
|
+
const editor = new Editor({
|
|
9
|
+
content,
|
|
10
|
+
extensions: getEditorExtensions(),
|
|
11
|
+
});
|
|
12
|
+
editors.push(editor);
|
|
13
|
+
return editor;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
for (const editor of editors.splice(0)) editor.destroy();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('toggle blocks', () => {
|
|
21
|
+
it('registers the native details schema and toggle command extension', () => {
|
|
22
|
+
const names = getEditorExtensions().map((extension) => extension.name);
|
|
23
|
+
|
|
24
|
+
expect(names).toEqual(
|
|
25
|
+
expect.arrayContaining([
|
|
26
|
+
'details',
|
|
27
|
+
'detailsSummary',
|
|
28
|
+
'detailsContent',
|
|
29
|
+
'toggleBlock',
|
|
30
|
+
])
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('turns a paragraph into an open toggle list and restores it', () => {
|
|
35
|
+
const editor = createEditor({
|
|
36
|
+
type: 'doc',
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: 'paragraph',
|
|
40
|
+
content: [{ type: 'text', text: 'Deployment notes' }],
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
});
|
|
44
|
+
editor.commands.setTextSelection(3);
|
|
45
|
+
|
|
46
|
+
expect(editor.commands.toggleDetailsBlock()).toBe(true);
|
|
47
|
+
expect(editor.getJSON()).toEqual({
|
|
48
|
+
type: 'doc',
|
|
49
|
+
content: [
|
|
50
|
+
{
|
|
51
|
+
type: 'details',
|
|
52
|
+
attrs: { open: true },
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: 'detailsSummary',
|
|
56
|
+
attrs: { level: null },
|
|
57
|
+
content: [{ type: 'text', text: 'Deployment notes' }],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
type: 'detailsContent',
|
|
61
|
+
content: [{ type: 'paragraph', attrs: { textAlign: null } }],
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{ type: 'paragraph', attrs: { textAlign: null } },
|
|
66
|
+
],
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(editor.commands.toggleDetailsBlock()).toBe(true);
|
|
70
|
+
expect(editor.getJSON()).toEqual({
|
|
71
|
+
type: 'doc',
|
|
72
|
+
content: [
|
|
73
|
+
{
|
|
74
|
+
type: 'paragraph',
|
|
75
|
+
attrs: { textAlign: null },
|
|
76
|
+
content: [{ type: 'text', text: 'Deployment notes' }],
|
|
77
|
+
},
|
|
78
|
+
{ type: 'paragraph', attrs: { textAlign: null } },
|
|
79
|
+
{ type: 'paragraph', attrs: { textAlign: null } },
|
|
80
|
+
],
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it.each([1, 2, 3] as const)(
|
|
85
|
+
'preserves heading %i semantics through toggle and untoggle',
|
|
86
|
+
(level) => {
|
|
87
|
+
const editor = createEditor({
|
|
88
|
+
type: 'doc',
|
|
89
|
+
content: [
|
|
90
|
+
{
|
|
91
|
+
type: 'heading',
|
|
92
|
+
attrs: { level, textAlign: null },
|
|
93
|
+
content: [{ type: 'text', text: `Heading ${level}` }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
editor.commands.setTextSelection(3);
|
|
98
|
+
|
|
99
|
+
expect(editor.commands.toggleDetailsBlock()).toBe(true);
|
|
100
|
+
const details = editor.getJSON().content?.[0];
|
|
101
|
+
expect(details).toMatchObject({ type: 'details', attrs: { open: true } });
|
|
102
|
+
expect(details?.content?.[0]).toEqual({
|
|
103
|
+
type: 'detailsSummary',
|
|
104
|
+
attrs: { level },
|
|
105
|
+
content: [{ type: 'text', text: `Heading ${level}` }],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
expect(editor.commands.toggleDetailsBlock()).toBe(true);
|
|
109
|
+
expect(editor.getJSON().content?.[0]).toMatchObject({
|
|
110
|
+
type: 'heading',
|
|
111
|
+
attrs: { level },
|
|
112
|
+
content: [{ type: 'text', text: `Heading ${level}` }],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
it('keeps selected trailing blocks inside the toggle body', () => {
|
|
118
|
+
const editor = createEditor({
|
|
119
|
+
type: 'doc',
|
|
120
|
+
content: [
|
|
121
|
+
{
|
|
122
|
+
type: 'heading',
|
|
123
|
+
attrs: { level: 2, textAlign: null },
|
|
124
|
+
content: [{ type: 'text', text: 'Summary' }],
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'paragraph',
|
|
128
|
+
content: [{ type: 'text', text: 'First detail' }],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'bulletList',
|
|
132
|
+
content: [
|
|
133
|
+
{
|
|
134
|
+
type: 'listItem',
|
|
135
|
+
content: [
|
|
136
|
+
{
|
|
137
|
+
type: 'paragraph',
|
|
138
|
+
content: [{ type: 'text', text: 'Nested detail' }],
|
|
139
|
+
},
|
|
140
|
+
],
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
editor.commands.selectAll();
|
|
147
|
+
|
|
148
|
+
expect(editor.commands.toggleDetailsBlock()).toBe(true);
|
|
149
|
+
const details = editor.getJSON().content?.[0];
|
|
150
|
+
expect(details?.content?.[1]?.type).toBe('detailsContent');
|
|
151
|
+
expect(details?.content?.[1]).toMatchObject({
|
|
152
|
+
content: [
|
|
153
|
+
{ type: 'paragraph' },
|
|
154
|
+
{ type: 'bulletList' },
|
|
155
|
+
{ type: 'paragraph' },
|
|
156
|
+
],
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('round-trips persisted open state and toggle heading level through HTML', () => {
|
|
161
|
+
const json = {
|
|
162
|
+
type: 'doc',
|
|
163
|
+
content: [
|
|
164
|
+
{
|
|
165
|
+
type: 'details',
|
|
166
|
+
attrs: { open: true },
|
|
167
|
+
content: [
|
|
168
|
+
{
|
|
169
|
+
type: 'detailsSummary',
|
|
170
|
+
attrs: { level: 2 },
|
|
171
|
+
content: [{ type: 'text', text: 'Expandable heading' }],
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
type: 'detailsContent',
|
|
175
|
+
content: [
|
|
176
|
+
{
|
|
177
|
+
type: 'paragraph',
|
|
178
|
+
attrs: { textAlign: null },
|
|
179
|
+
content: [{ type: 'text', text: 'Hidden details' }],
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
};
|
|
187
|
+
const extensions = getEditorExtensions();
|
|
188
|
+
const html = generateHTML(json, extensions);
|
|
189
|
+
|
|
190
|
+
expect(html).toContain('<details');
|
|
191
|
+
expect(html).toContain('open=""');
|
|
192
|
+
expect(html).toContain('data-heading-level="2"');
|
|
193
|
+
expect(generateJSON(html, extensions)).toEqual(json);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('persists collapse and expand interactions from the rendered toggle button', () => {
|
|
197
|
+
const element = document.createElement('div');
|
|
198
|
+
document.body.append(element);
|
|
199
|
+
const editor = new Editor({
|
|
200
|
+
element,
|
|
201
|
+
extensions: getEditorExtensions(),
|
|
202
|
+
content: {
|
|
203
|
+
type: 'doc',
|
|
204
|
+
content: [
|
|
205
|
+
{
|
|
206
|
+
type: 'details',
|
|
207
|
+
attrs: { open: false },
|
|
208
|
+
content: [
|
|
209
|
+
{
|
|
210
|
+
type: 'detailsSummary',
|
|
211
|
+
attrs: { level: null },
|
|
212
|
+
content: [{ type: 'text', text: 'More information' }],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
type: 'detailsContent',
|
|
216
|
+
content: [
|
|
217
|
+
{
|
|
218
|
+
type: 'paragraph',
|
|
219
|
+
content: [{ type: 'text', text: 'Hidden initially' }],
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
],
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
editors.push(editor);
|
|
229
|
+
|
|
230
|
+
const button = element.querySelector<HTMLButtonElement>(
|
|
231
|
+
'.toggle-details-button'
|
|
232
|
+
);
|
|
233
|
+
const body = element.querySelector<HTMLElement>(
|
|
234
|
+
'[data-type="detailsContent"]'
|
|
235
|
+
);
|
|
236
|
+
expect(button?.getAttribute('aria-expanded')).toBe('false');
|
|
237
|
+
expect(body).toHaveAttribute('hidden');
|
|
238
|
+
|
|
239
|
+
button?.click();
|
|
240
|
+
expect(editor.getJSON().content?.[0]?.attrs?.open).toBe(true);
|
|
241
|
+
expect(button?.getAttribute('aria-expanded')).toBe('true');
|
|
242
|
+
expect(body).not.toHaveAttribute('hidden');
|
|
243
|
+
|
|
244
|
+
button?.click();
|
|
245
|
+
expect(editor.getJSON().content?.[0]?.attrs?.open).toBe(false);
|
|
246
|
+
expect(button?.getAttribute('aria-expanded')).toBe('false');
|
|
247
|
+
expect(body).toHaveAttribute('hidden');
|
|
248
|
+
|
|
249
|
+
element.remove();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
export function getRichTextEditorClasses({
|
|
2
|
+
className,
|
|
3
|
+
readOnly,
|
|
4
|
+
}: {
|
|
5
|
+
className?: string;
|
|
6
|
+
readOnly: boolean;
|
|
7
|
+
}) {
|
|
8
|
+
return [
|
|
9
|
+
'border border-dynamic-border rounded-md bg-transparent',
|
|
10
|
+
'max-w-none overflow-y-auto',
|
|
11
|
+
readOnly ? 'pt-0' : 'pt-4',
|
|
12
|
+
'text-foreground leading-relaxed',
|
|
13
|
+
'[&_:first-child]:mt-0',
|
|
14
|
+
'[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:text-foreground [&_h1]:mt-6 [&_h1]:mb-4',
|
|
15
|
+
'[&_h2]:text-3xl [&_h2]:font-semibold [&_h2]:text-foreground [&_h2]:mt-5 [&_h2]:mb-3',
|
|
16
|
+
'[&_h3]:text-2xl [&_h3]:font-semibold [&_h3]:text-foreground [&_h3]:mt-4 [&_h3]:mb-2',
|
|
17
|
+
'[&_h4]:text-xl [&_h4]:font-semibold [&_h4]:text-foreground [&_h4]:mt-3 [&_h4]:mb-2',
|
|
18
|
+
'[&_h5]:text-lg [&_h5]:font-semibold [&_h5]:text-foreground [&_h5]:mt-3 [&_h5]:mb-2',
|
|
19
|
+
'[&_h6]:text-base [&_h6]:font-semibold [&_h6]:text-foreground [&_h6]:mt-3 [&_h6]:mb-2',
|
|
20
|
+
'[&_p]:my-3 [&_p]:leading-7',
|
|
21
|
+
'[&_ul]:my-3 [&_ul]:ml-6 [&_ul]:px-0 [&_ul]:mr-0',
|
|
22
|
+
'[&_ol]:my-3 [&_ol]:ml-6 [&_ol]:px-0 [&_ol]:mr-0',
|
|
23
|
+
'[&_li]:my-1 [&_li]:leading-7',
|
|
24
|
+
'[&_ul_li_p]:my-1',
|
|
25
|
+
'[&_ol_li_p]:my-1',
|
|
26
|
+
'[&_li_h1]:text-4xl [&_li_h2]:text-3xl [&_li_h3]:text-2xl',
|
|
27
|
+
'[&_ul[data-type="taskList"]]:list-none [&_ul[data-type="taskList"]]:ml-6 [&_ul[data-type="taskList"]]:pl-0 [&_ul[data-type="taskList"]]:pr-0 [&_ul[data-type="taskList"]]:mr-0 [&_ul[data-type="taskList"]]:my-3',
|
|
28
|
+
'[&_ul[data-type="taskList"]_li]:flex [&_ul[data-type="taskList"]_li]:items-start [&_ul[data-type="taskList"]_li]:my-1',
|
|
29
|
+
'[&_ul[data-type="taskList"]_.task-list-checkbox-label]:flex-[0_0_auto]',
|
|
30
|
+
'[&_ul[data-type="taskList"]_li>div:not(.task-list-checkbox-label)]:flex-1 [&_ul[data-type="taskList"]_li>div:not(.task-list-checkbox-label)]:min-w-0',
|
|
31
|
+
'[&_ul[data-type="taskList"]_li_p]:my-1',
|
|
32
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:appearance-none [&_ul[data-type="taskList"]_input[type="checkbox"]]:h-[18px] [&_ul[data-type="taskList"]_input[type="checkbox"]]:w-[18px]',
|
|
33
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:cursor-pointer [&_ul[data-type="taskList"]_input[type="checkbox"]]:rounded-[4px] [&_ul[data-type="taskList"]_input[type="checkbox"]]:border-2 [&_ul[data-type="taskList"]_input[type="checkbox"]]:border-input',
|
|
34
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:bg-background [&_ul[data-type="taskList"]_input[type="checkbox"]]:transition-all [&_ul[data-type="taskList"]_input[type="checkbox"]]:duration-150',
|
|
35
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:shrink-0',
|
|
36
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]:hover]:border-dynamic-gray [&_ul[data-type="taskList"]_input[type="checkbox"]:hover]:bg-dynamic-gray/10 [&_ul[data-type="taskList"]_input[type="checkbox"]:hover]:scale-105',
|
|
37
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:outline-none [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:ring-2 [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:ring-dynamic-gray/30 [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:ring-offset-2 [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:border-dynamic-gray',
|
|
38
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:bg-dynamic-green/20 [&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:border-dynamic-green',
|
|
39
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]:checked:hover]:bg-dynamic-green/10 [&_ul[data-type="taskList"]_input[type="checkbox"]:checked:hover]:border-dynamic-green',
|
|
40
|
+
`[&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2309090b%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22M4%208l2.5%202.5L12%205%22%2F%3E%3C%2Fsvg%3E')]`,
|
|
41
|
+
`dark:[&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22white%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22M4%208l2.5%202.5L12%205%22%2F%3E%3C%2Fsvg%3E')]`,
|
|
42
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]:checked:bg-[length:14px_14px]',
|
|
43
|
+
'[&_ul[data-type="taskList"]_input[type="checkbox"]:indeterminate]:bg-dynamic-orange/20 [&_ul[data-type="taskList"]_input[type="checkbox"]:indeterminate]:border-dynamic-orange',
|
|
44
|
+
'[&_li_ul]:my-1 [&_li_ul]:ml-6 [&_li_ol]:my-1 [&_li_ol]:ml-6',
|
|
45
|
+
'[&_ul[data-type="taskList"]_ul[data-type="taskList"]]:my-1 [&_ul[data-type="taskList"]_ul[data-type="taskList"]]:ml-6',
|
|
46
|
+
'[&_div[data-type="details"]]:relative [&_div[data-type="details"]]:my-3 [&_div[data-type="details"]]:pl-7',
|
|
47
|
+
'[&_.toggle-details-button]:absolute [&_.toggle-details-button]:top-1 [&_.toggle-details-button]:left-0 [&_.toggle-details-button]:size-6 [&_.toggle-details-button]:rounded-md [&_.toggle-details-button]:text-muted-foreground [&_.toggle-details-button]:transition-transform [&_.toggle-details-button:hover]:bg-dynamic-surface [&_.toggle-details-button:hover]:text-foreground',
|
|
48
|
+
'[&_.toggle-details-button]:before:content-["›"] [&_.toggle-details-button]:before:text-xl [&_.toggle-details-button]:before:leading-none',
|
|
49
|
+
'[&_.toggle-details.is-open>.toggle-details-button]:rotate-90',
|
|
50
|
+
'[&_summary]:cursor-text [&_summary]:list-none [&_summary::-webkit-details-marker]:hidden',
|
|
51
|
+
'[&_summary[data-heading-level="1"]]:text-4xl [&_summary[data-heading-level="1"]]:font-bold',
|
|
52
|
+
'[&_summary[data-heading-level="2"]]:text-3xl [&_summary[data-heading-level="2"]]:font-semibold',
|
|
53
|
+
'[&_summary[data-heading-level="3"]]:text-2xl [&_summary[data-heading-level="3"]]:font-semibold',
|
|
54
|
+
'[&_div[data-type="detailsContent"]]:mt-1 [&_div[data-type="detailsContent"]]:border-dynamic-border/60 [&_div[data-type="detailsContent"]]:border-l [&_div[data-type="detailsContent"]]:pl-4',
|
|
55
|
+
'[&_blockquote]:border-l-4 [&_blockquote]:border-dynamic-border [&_blockquote]:pl-4 [&_blockquote]:my-4',
|
|
56
|
+
'[&_blockquote]:text-muted-foreground [&_blockquote]:italic',
|
|
57
|
+
'[&_code]:text-dynamic-pink [&_code]:bg-dynamic-pink/10 [&_code]:px-1.5 [&_code]:py-0.5',
|
|
58
|
+
'[&_code]:rounded [&_code]:text-sm [&_code]:font-mono',
|
|
59
|
+
'[&_pre]:bg-dynamic-border/50 [&_pre]:p-4 [&_pre]:rounded-md [&_pre]:my-4 [&_pre]:overflow-x-auto',
|
|
60
|
+
'[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-foreground',
|
|
61
|
+
'[&_strong]:font-bold [&_strong]:text-foreground',
|
|
62
|
+
'[&_b]:font-bold [&_b]:text-foreground',
|
|
63
|
+
'[&_em]:italic [&_em]:text-foreground',
|
|
64
|
+
'[&_i]:italic [&_i]:text-foreground',
|
|
65
|
+
'[&_a]:text-dynamic-cyan [&_a]:underline [&_a]:cursor-pointer',
|
|
66
|
+
'[&_a:hover]:text-dynamic-cyan/80',
|
|
67
|
+
'[&_a_strong]:text-dynamic-cyan [&_a_b]:text-dynamic-cyan',
|
|
68
|
+
'[&_strong_a]:text-dynamic-cyan [&_b_a]:text-dynamic-cyan',
|
|
69
|
+
'[&_hr]:border-dynamic-border [&_hr]:my-8',
|
|
70
|
+
'[&_table]:my-6 [&_table]:w-full [&_table]:overflow-hidden [&_table]:rounded-lg [&_table]:border [&_table]:border-collapse [&_table]:border-dynamic-border',
|
|
71
|
+
'[&_table]:shadow-sm',
|
|
72
|
+
'[&_th]:relative [&_th]:border-r [&_th]:border-dynamic-border [&_th]:bg-dynamic-border/30',
|
|
73
|
+
'[&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold [&_th]:text-foreground',
|
|
74
|
+
'[&_th]:last:border-r-0 [&_th_p]:my-0',
|
|
75
|
+
'[&_tr]:border-b [&_tr]:border-dynamic-border [&_tr]:last:border-0',
|
|
76
|
+
'[&_tr]:transition-colors [&_tr:hover]:bg-dynamic-surface/30',
|
|
77
|
+
'[&_td]:relative [&_td]:border-r [&_td]:border-dynamic-border [&_td]:px-4 [&_td]:py-3',
|
|
78
|
+
'[&_td]:last:border-r-0 [&_td_p]:my-0',
|
|
79
|
+
'[&_td]:transition-colors [&_td:hover]:bg-dynamic-surface/50',
|
|
80
|
+
'[&_td:focus]:bg-dynamic-surface/70 [&_td:focus]:outline-none',
|
|
81
|
+
'[&_td:focus]:ring-2 [&_td:focus]:ring-dynamic-blue/50 [&_td:focus]:ring-inset',
|
|
82
|
+
'[&_.selectedCell]:bg-dynamic-blue/10 [&_.selectedCell]:ring-2 [&_.selectedCell]:ring-dynamic-blue/30',
|
|
83
|
+
'[&_.column-resize-handle]:absolute [&_.column-resize-handle]:right-[-2px] [&_.column-resize-handle]:top-0',
|
|
84
|
+
'[&_.column-resize-handle]:bottom-0 [&_.column-resize-handle]:w-1 [&_.column-resize-handle]:cursor-col-resize',
|
|
85
|
+
'[&_.column-resize-handle]:bg-dynamic-blue/50 [&_.column-resize-handle]:opacity-0',
|
|
86
|
+
'[&_.column-resize-handle:hover]:opacity-100 [&_.column-resize-handle]:transition-opacity',
|
|
87
|
+
'[&_*:is(p,h1,h2,h3).is-empty::before]:content-[attr(data-placeholder)]',
|
|
88
|
+
'[&_*:is(p,h1,h2,h3).is-empty::before]:text-muted-foreground',
|
|
89
|
+
'[&_*:is(p,h1,h2,h3).is-empty::before]:float-left',
|
|
90
|
+
'[&_*:is(p,h1,h2,h3).is-empty::before]:h-0',
|
|
91
|
+
'[&_*:is(p,h1,h2,h3).is-empty::before]:pointer-events-none',
|
|
92
|
+
readOnly
|
|
93
|
+
? '[&_ul[data-type="taskList"]_input[type="checkbox"]]:!pointer-events-none [&_ul[data-type="taskList"]_input[type="checkbox"]]:!opacity-70 [&_ul[data-type="taskList"]_li>label]:!pointer-events-none [&_input[type="checkbox"]]:!pointer-events-none [&_.task-list-checkbox]:!pointer-events-none [&_.task-list-checkbox-label]:!pointer-events-none [&_li_div[draggable="true"]]:hidden [&_ul[data-type="taskList"]_li]:![-webkit-user-drag:none] [&_ul[data-type="taskList"]_li]:!user-drag-none'
|
|
94
|
+
: '',
|
|
95
|
+
readOnly ? '[&_.drag-handle]:hidden' : '',
|
|
96
|
+
className,
|
|
97
|
+
]
|
|
98
|
+
.filter(Boolean)
|
|
99
|
+
.join(' ');
|
|
100
|
+
}
|
|
@@ -15,8 +15,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
15
15
|
import { flushSync } from 'react-dom';
|
|
16
16
|
import type * as Y from 'yjs';
|
|
17
17
|
import { migrateInlineImagesToBlock } from './content-migration';
|
|
18
|
+
import { getRichTextEditorClasses } from './editor-classes';
|
|
18
19
|
import { getEditorExtensions } from './extensions';
|
|
19
|
-
import { handlePlainEnterFallback } from './keyboard';
|
|
20
|
+
import { handleListIndentation, handlePlainEnterFallback } from './keyboard';
|
|
20
21
|
import type { TaskMentionNodeViewRenderer } from './mention-extension';
|
|
21
22
|
import { FixedToolbar, ToolBar } from './tool-bar';
|
|
22
23
|
|
|
@@ -128,6 +129,7 @@ export interface RichTextEditorProps {
|
|
|
128
129
|
create_project?: string;
|
|
129
130
|
};
|
|
130
131
|
renderTaskMention?: TaskMentionNodeViewRenderer;
|
|
132
|
+
toggleBlockLabel?: string;
|
|
131
133
|
}
|
|
132
134
|
|
|
133
135
|
export function RichTextEditor({
|
|
@@ -156,6 +158,7 @@ export function RichTextEditor({
|
|
|
156
158
|
revealToolbarOnFocus = false,
|
|
157
159
|
mentionTranslations,
|
|
158
160
|
renderTaskMention,
|
|
161
|
+
toggleBlockLabel,
|
|
159
162
|
}: RichTextEditorProps) {
|
|
160
163
|
// Use refs to ensure we have stable references for handlers
|
|
161
164
|
const onImageUploadRef = useRef(onImageUpload);
|
|
@@ -230,115 +233,10 @@ export function RichTextEditor({
|
|
|
230
233
|
};
|
|
231
234
|
}, [debouncedOnChange]);
|
|
232
235
|
|
|
233
|
-
const getEditorClasses = useMemo(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
readOnly ? 'pt-0' : 'pt-4',
|
|
238
|
-
// Typography base
|
|
239
|
-
'text-foreground leading-relaxed',
|
|
240
|
-
// First child margin reset
|
|
241
|
-
'[&_:first-child]:mt-0',
|
|
242
|
-
// Headings
|
|
243
|
-
'[&_h1]:text-4xl [&_h1]:font-bold [&_h1]:text-foreground [&_h1]:mt-6 [&_h1]:mb-4',
|
|
244
|
-
'[&_h2]:text-3xl [&_h2]:font-semibold [&_h2]:text-foreground [&_h2]:mt-5 [&_h2]:mb-3',
|
|
245
|
-
'[&_h3]:text-2xl [&_h3]:font-semibold [&_h3]:text-foreground [&_h3]:mt-4 [&_h3]:mb-2',
|
|
246
|
-
'[&_h4]:text-xl [&_h4]:font-semibold [&_h4]:text-foreground [&_h4]:mt-3 [&_h4]:mb-2',
|
|
247
|
-
'[&_h5]:text-lg [&_h5]:font-semibold [&_h5]:text-foreground [&_h5]:mt-3 [&_h5]:mb-2',
|
|
248
|
-
'[&_h6]:text-base [&_h6]:font-semibold [&_h6]:text-foreground [&_h6]:mt-3 [&_h6]:mb-2',
|
|
249
|
-
// Paragraphs
|
|
250
|
-
'[&_p]:my-3 [&_p]:leading-7',
|
|
251
|
-
// Lists - general styling with consistent spacing
|
|
252
|
-
'[&_ul]:my-3 [&_ul]:ml-6 [&_ul]:px-0 [&_ul]:mr-0',
|
|
253
|
-
'[&_ol]:my-3 [&_ol]:ml-6 [&_ol]:px-0 [&_ol]:mr-0',
|
|
254
|
-
'[&_li]:my-1 [&_li]:leading-7',
|
|
255
|
-
'[&_ul_li_p]:my-1',
|
|
256
|
-
'[&_ol_li_p]:my-1',
|
|
257
|
-
'[&_li_h1]:text-4xl [&_li_h2]:text-3xl [&_li_h3]:text-2xl',
|
|
258
|
-
// Task list specific styles - match regular list spacing
|
|
259
|
-
'[&_ul[data-type="taskList"]]:list-none [&_ul[data-type="taskList"]]:ml-6 [&_ul[data-type="taskList"]]:pl-0 [&_ul[data-type="taskList"]]:pr-0 [&_ul[data-type="taskList"]]:mr-0 [&_ul[data-type="taskList"]]:my-3',
|
|
260
|
-
'[&_ul[data-type="taskList"]_li]:flex [&_ul[data-type="taskList"]_li]:items-start [&_ul[data-type="taskList"]_li]:my-1',
|
|
261
|
-
'[&_ul[data-type="taskList"]_.task-list-checkbox-label]:flex-[0_0_auto]',
|
|
262
|
-
'[&_ul[data-type="taskList"]_li>div:not(.task-list-checkbox-label)]:flex-1 [&_ul[data-type="taskList"]_li>div:not(.task-list-checkbox-label)]:min-w-0',
|
|
263
|
-
'[&_ul[data-type="taskList"]_li_p]:my-1',
|
|
264
|
-
// Checkbox styling
|
|
265
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:appearance-none [&_ul[data-type="taskList"]_input[type="checkbox"]]:h-[18px] [&_ul[data-type="taskList"]_input[type="checkbox"]]:w-[18px]',
|
|
266
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:cursor-pointer [&_ul[data-type="taskList"]_input[type="checkbox"]]:rounded-[4px] [&_ul[data-type="taskList"]_input[type="checkbox"]]:border-2 [&_ul[data-type="taskList"]_input[type="checkbox"]]:border-input',
|
|
267
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:bg-background [&_ul[data-type="taskList"]_input[type="checkbox"]]:transition-all [&_ul[data-type="taskList"]_input[type="checkbox"]]:duration-150',
|
|
268
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]]:shrink-0',
|
|
269
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]:hover]:border-dynamic-gray [&_ul[data-type="taskList"]_input[type="checkbox"]:hover]:bg-dynamic-gray/10 [&_ul[data-type="taskList"]_input[type="checkbox"]:hover]:scale-105',
|
|
270
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:outline-none [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:ring-2 [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:ring-dynamic-gray/30 [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:ring-offset-2 [&_ul[data-type="taskList"]_input[type="checkbox"]:focus]:border-dynamic-gray',
|
|
271
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:bg-dynamic-green/20 [&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:border-dynamic-green',
|
|
272
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]:checked:hover]:bg-dynamic-green/10 [&_ul[data-type="taskList"]_input[type="checkbox"]:checked:hover]:border-dynamic-green',
|
|
273
|
-
// Light mode checkmark (dark/black)
|
|
274
|
-
`[&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22%2309090b%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22M4%208l2.5%202.5L12%205%22%2F%3E%3C%2Fsvg%3E')]`,
|
|
275
|
-
// Dark mode checkmark (white)
|
|
276
|
-
`dark:[&_ul[data-type="taskList"]_input[type="checkbox"]:checked]:bg-[url('data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2016%2016%22%3E%3Cpath%20fill%3D%22none%22%20stroke%3D%22white%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20stroke-width%3D%222%22%20d%3D%22M4%208l2.5%202.5L12%205%22%2F%3E%3C%2Fsvg%3E')]`,
|
|
277
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]:checked:bg-[length:14px_14px]',
|
|
278
|
-
// Indeterminate checkbox styling (fallback)
|
|
279
|
-
'[&_ul[data-type="taskList"]_input[type="checkbox"]:indeterminate]:bg-dynamic-orange/20 [&_ul[data-type="taskList"]_input[type="checkbox"]:indeterminate]:border-dynamic-orange',
|
|
280
|
-
// Nested task lists
|
|
281
|
-
'[&_ul[data-type="taskList"]_ul[data-type="taskList"]]:my-0 [&_ul[data-type="taskList"]_ul[data-type="taskList"]]:ml-0',
|
|
282
|
-
// Blockquotes
|
|
283
|
-
'[&_blockquote]:border-l-4 [&_blockquote]:border-dynamic-border [&_blockquote]:pl-4 [&_blockquote]:my-4',
|
|
284
|
-
'[&_blockquote]:text-muted-foreground [&_blockquote]:italic',
|
|
285
|
-
// Code
|
|
286
|
-
'[&_code]:text-dynamic-pink [&_code]:bg-dynamic-pink/10 [&_code]:px-1.5 [&_code]:py-0.5',
|
|
287
|
-
'[&_code]:rounded [&_code]:text-sm [&_code]:font-mono',
|
|
288
|
-
'[&_pre]:bg-dynamic-border/50 [&_pre]:p-4 [&_pre]:rounded-md [&_pre]:my-4 [&_pre]:overflow-x-auto',
|
|
289
|
-
'[&_pre_code]:bg-transparent [&_pre_code]:p-0 [&_pre_code]:text-foreground',
|
|
290
|
-
// Strong/Bold
|
|
291
|
-
'[&_strong]:font-bold [&_strong]:text-foreground',
|
|
292
|
-
'[&_b]:font-bold [&_b]:text-foreground',
|
|
293
|
-
// Emphasis/Italic
|
|
294
|
-
'[&_em]:italic [&_em]:text-foreground',
|
|
295
|
-
'[&_i]:italic [&_i]:text-foreground',
|
|
296
|
-
// Links (ensure they maintain cyan color even when bold)
|
|
297
|
-
'[&_a]:text-dynamic-cyan [&_a]:underline [&_a]:cursor-pointer',
|
|
298
|
-
'[&_a:hover]:text-dynamic-cyan/80',
|
|
299
|
-
'[&_a_strong]:text-dynamic-cyan [&_a_b]:text-dynamic-cyan',
|
|
300
|
-
'[&_strong_a]:text-dynamic-cyan [&_b_a]:text-dynamic-cyan',
|
|
301
|
-
// Horizontal rule
|
|
302
|
-
'[&_hr]:border-dynamic-border [&_hr]:my-8',
|
|
303
|
-
// Tables - Enhanced styling for better UX
|
|
304
|
-
'[&_table]:my-6 [&_table]:w-full [&_table]:overflow-hidden [&_table]:rounded-lg [&_table]:border [&_table]:border-collapse [&_table]:border-dynamic-border',
|
|
305
|
-
'[&_table]:shadow-sm',
|
|
306
|
-
// Table headers
|
|
307
|
-
'[&_th]:relative [&_th]:border-r [&_th]:border-dynamic-border [&_th]:bg-dynamic-border/30',
|
|
308
|
-
'[&_th]:px-4 [&_th]:py-3 [&_th]:text-left [&_th]:font-semibold [&_th]:text-foreground',
|
|
309
|
-
'[&_th]:last:border-r-0 [&_th_p]:my-0',
|
|
310
|
-
// Table rows
|
|
311
|
-
'[&_tr]:border-b [&_tr]:border-dynamic-border [&_tr]:last:border-0',
|
|
312
|
-
'[&_tr]:transition-colors [&_tr:hover]:bg-dynamic-surface/30',
|
|
313
|
-
// Table cells
|
|
314
|
-
'[&_td]:relative [&_td]:border-r [&_td]:border-dynamic-border [&_td]:px-4 [&_td]:py-3',
|
|
315
|
-
'[&_td]:last:border-r-0 [&_td_p]:my-0',
|
|
316
|
-
'[&_td]:transition-colors [&_td:hover]:bg-dynamic-surface/50',
|
|
317
|
-
'[&_td:focus]:bg-dynamic-surface/70 [&_td:focus]:outline-none',
|
|
318
|
-
'[&_td:focus]:ring-2 [&_td:focus]:ring-dynamic-blue/50 [&_td:focus]:ring-inset',
|
|
319
|
-
// Selected cells/rows
|
|
320
|
-
'[&_.selectedCell]:bg-dynamic-blue/10 [&_.selectedCell]:ring-2 [&_.selectedCell]:ring-dynamic-blue/30',
|
|
321
|
-
// Column resize handle
|
|
322
|
-
'[&_.column-resize-handle]:absolute [&_.column-resize-handle]:right-[-2px] [&_.column-resize-handle]:top-0',
|
|
323
|
-
'[&_.column-resize-handle]:bottom-0 [&_.column-resize-handle]:w-1 [&_.column-resize-handle]:cursor-col-resize',
|
|
324
|
-
'[&_.column-resize-handle]:bg-dynamic-blue/50 [&_.column-resize-handle]:opacity-0',
|
|
325
|
-
'[&_.column-resize-handle:hover]:opacity-100 [&_.column-resize-handle]:transition-opacity',
|
|
326
|
-
// Placeholder styles
|
|
327
|
-
'[&_*:is(p,h1,h2,h3).is-empty::before]:content-[attr(data-placeholder)]',
|
|
328
|
-
'[&_*:is(p,h1,h2,h3).is-empty::before]:text-muted-foreground',
|
|
329
|
-
'[&_*:is(p,h1,h2,h3).is-empty::before]:float-left',
|
|
330
|
-
'[&_*:is(p,h1,h2,h3).is-empty::before]:h-0',
|
|
331
|
-
'[&_*:is(p,h1,h2,h3).is-empty::before]:pointer-events-none',
|
|
332
|
-
// Read-only specific styles
|
|
333
|
-
readOnly
|
|
334
|
-
? '[&_ul[data-type="taskList"]_input[type="checkbox"]]:!pointer-events-none [&_ul[data-type="taskList"]_input[type="checkbox"]]:!opacity-70 [&_ul[data-type="taskList"]_li>label]:!pointer-events-none [&_input[type="checkbox"]]:!pointer-events-none [&_.task-list-checkbox]:!pointer-events-none [&_.task-list-checkbox-label]:!pointer-events-none [&_li_div[draggable="true"]]:hidden [&_ul[data-type="taskList"]_li]:![-webkit-user-drag:none] [&_ul[data-type="taskList"]_li]:!user-drag-none'
|
|
335
|
-
: '',
|
|
336
|
-
// Hide drag handle in read-only mode (double safety, though conditional render should handle it)
|
|
337
|
-
readOnly ? '[&_.drag-handle]:hidden' : '',
|
|
338
|
-
className,
|
|
339
|
-
].filter(Boolean);
|
|
340
|
-
return baseClasses.join(' ');
|
|
341
|
-
}, [className, readOnly]);
|
|
236
|
+
const getEditorClasses = useMemo(
|
|
237
|
+
() => getRichTextEditorClasses({ className, readOnly }),
|
|
238
|
+
[className, readOnly]
|
|
239
|
+
);
|
|
342
240
|
|
|
343
241
|
const editor = useEditor(
|
|
344
242
|
{
|
|
@@ -495,49 +393,7 @@ export function RichTextEditor({
|
|
|
495
393
|
}
|
|
496
394
|
}
|
|
497
395
|
|
|
498
|
-
|
|
499
|
-
if (event.key === 'Tab') {
|
|
500
|
-
// Always prevent default Tab behavior to avoid focus loss
|
|
501
|
-
event.preventDefault();
|
|
502
|
-
event.stopPropagation();
|
|
503
|
-
|
|
504
|
-
const { $from } = selection;
|
|
505
|
-
|
|
506
|
-
// Check if we're in a regular list item (bullet or ordered)
|
|
507
|
-
const isInListItem =
|
|
508
|
-
$from.node(-1)?.type.name === 'listItem' ||
|
|
509
|
-
$from.node(-2)?.type.name === 'listItem';
|
|
510
|
-
|
|
511
|
-
// Check if we're in a task item (checkbox)
|
|
512
|
-
const isInTaskItem =
|
|
513
|
-
$from.node(-1)?.type.name === 'taskItem' ||
|
|
514
|
-
$from.node(-2)?.type.name === 'taskItem';
|
|
515
|
-
|
|
516
|
-
if (isInListItem) {
|
|
517
|
-
if (event.shiftKey) {
|
|
518
|
-
// Shift+Tab: Outdent (lift) the list item
|
|
519
|
-
editor?.chain().focus().liftListItem('listItem').run();
|
|
520
|
-
} else {
|
|
521
|
-
// Tab: Indent (sink) the list item
|
|
522
|
-
editor?.chain().focus().sinkListItem('listItem').run();
|
|
523
|
-
}
|
|
524
|
-
} else if (isInTaskItem) {
|
|
525
|
-
if (event.shiftKey) {
|
|
526
|
-
// Shift+Tab: Outdent (lift) the task item
|
|
527
|
-
editor?.chain().focus().liftListItem('taskItem').run();
|
|
528
|
-
} else {
|
|
529
|
-
// Tab: Indent (sink) the task item
|
|
530
|
-
editor?.chain().focus().sinkListItem('taskItem').run();
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
// Ensure focus stays in the editor
|
|
535
|
-
setTimeout(() => {
|
|
536
|
-
view.focus();
|
|
537
|
-
}, 0);
|
|
538
|
-
|
|
539
|
-
return true; // We handled the event
|
|
540
|
-
}
|
|
396
|
+
if (handleListIndentation(view, event)) return true;
|
|
541
397
|
|
|
542
398
|
return false;
|
|
543
399
|
},
|
|
@@ -779,6 +635,7 @@ export function RichTextEditor({
|
|
|
779
635
|
workspaceId={workspaceId}
|
|
780
636
|
onImageUpload={onImageUpload}
|
|
781
637
|
onConvertToTask={onConvertToTask}
|
|
638
|
+
toggleBlockLabel={toggleBlockLabel}
|
|
782
639
|
/>
|
|
783
640
|
)}
|
|
784
641
|
{!readOnly && (
|
|
@@ -790,6 +647,7 @@ export function RichTextEditor({
|
|
|
790
647
|
workspaceId={workspaceId}
|
|
791
648
|
onImageUpload={onImageUpload}
|
|
792
649
|
onConvertToTask={onConvertToTask}
|
|
650
|
+
toggleBlockLabel={toggleBlockLabel}
|
|
793
651
|
/>
|
|
794
652
|
)}
|
|
795
653
|
{/* Temporarily hide drag handle to resolve 'removeChild' error until finding a more robust solution
|
|
@@ -2,6 +2,7 @@ import { InputRule } from '@tiptap/core';
|
|
|
2
2
|
import Collaboration from '@tiptap/extension-collaboration';
|
|
3
3
|
import CollaborationCaret from '@tiptap/extension-collaboration-caret';
|
|
4
4
|
import { Color } from '@tiptap/extension-color';
|
|
5
|
+
import { Details, DetailsContent } from '@tiptap/extension-details';
|
|
5
6
|
import HorizontalRule from '@tiptap/extension-horizontal-rule';
|
|
6
7
|
import Link from '@tiptap/extension-link';
|
|
7
8
|
import { TaskList } from '@tiptap/extension-list';
|
|
@@ -30,6 +31,7 @@ import { Mention, type TaskMentionNodeViewRenderer } from './mention-extension';
|
|
|
30
31
|
import { NodeDrag } from './node-drag-extension';
|
|
31
32
|
import { TaskItemCheckbox } from './task-item-checkbox-extension';
|
|
32
33
|
import { TextShortcuts } from './text-shortcuts-extension';
|
|
34
|
+
import { ToggleBlock, ToggleSummary } from './toggle-block-extension';
|
|
33
35
|
import { Video } from './video-extension';
|
|
34
36
|
|
|
35
37
|
interface EditorExtensionsOptions {
|
|
@@ -181,6 +183,23 @@ export function getEditorExtensions({
|
|
|
181
183
|
Strike,
|
|
182
184
|
Subscript,
|
|
183
185
|
Superscript,
|
|
186
|
+
Details.configure({
|
|
187
|
+
persist: true,
|
|
188
|
+
HTMLAttributes: {
|
|
189
|
+
class: 'toggle-details',
|
|
190
|
+
},
|
|
191
|
+
renderToggleButton: ({ element, isOpen }) => {
|
|
192
|
+
element.className = 'toggle-details-button';
|
|
193
|
+
element.setAttribute(
|
|
194
|
+
'aria-label',
|
|
195
|
+
isOpen ? 'Collapse toggle' : 'Expand toggle'
|
|
196
|
+
);
|
|
197
|
+
element.setAttribute('aria-expanded', String(isOpen));
|
|
198
|
+
},
|
|
199
|
+
}),
|
|
200
|
+
ToggleSummary,
|
|
201
|
+
DetailsContent,
|
|
202
|
+
ToggleBlock,
|
|
184
203
|
Mention.configure({
|
|
185
204
|
renderTaskMention,
|
|
186
205
|
translations: mentionTranslations,
|