@lark-apaas/coding-template-nestjs-react-fullstack 0.1.34 → 0.1.36-alpha.20260901035538
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/package.json +1 -1
- package/template/client/src/components/ui/markdown.tsx +43 -0
- package/template/client/src/index.css +0 -1
- package/template/nest-cli.json +10 -9
- package/template/package-lock.json +10325 -18965
- package/template/package.json +7 -70
- package/template/scripts/build.sh +12 -0
- package/template/vite.config.ts +1 -1
- package/template/client/src/components/business-ui/tiptap-editor/README.md +0 -324
- package/template/client/src/components/business-ui/tiptap-editor/components/attachment-toolbar-button.tsx +0 -74
- package/template/client/src/components/business-ui/tiptap-editor/components/blockquote-toolbar-button.tsx +0 -41
- package/template/client/src/components/business-ui/tiptap-editor/components/code-block-toolbar-button.tsx +0 -41
- package/template/client/src/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button.tsx +0 -141
- package/template/client/src/components/business-ui/tiptap-editor/components/heading-toolbar-button.tsx +0 -99
- package/template/client/src/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button.tsx +0 -39
- package/template/client/src/components/business-ui/tiptap-editor/components/image-upload-toolbar-button.tsx +0 -65
- package/template/client/src/components/business-ui/tiptap-editor/components/link-edit-form.tsx +0 -127
- package/template/client/src/components/business-ui/tiptap-editor/components/link-hover-toolbar.tsx +0 -380
- package/template/client/src/components/business-ui/tiptap-editor/components/link-toolbar-button.tsx +0 -96
- package/template/client/src/components/business-ui/tiptap-editor/components/list-toolbar-button.tsx +0 -75
- package/template/client/src/components/business-ui/tiptap-editor/components/mark-toolbar-button.tsx +0 -118
- package/template/client/src/components/business-ui/tiptap-editor/components/text-align-toolbar-button.tsx +0 -84
- package/template/client/src/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button.tsx +0 -54
- package/template/client/src/components/business-ui/tiptap-editor/extensions/attachment.tsx +0 -539
- package/template/client/src/components/business-ui/tiptap-editor/extensions/code-block-shiki.tsx +0 -236
- package/template/client/src/components/business-ui/tiptap-editor/extensions/complete-kit.ts +0 -260
- package/template/client/src/components/business-ui/tiptap-editor/extensions/image.tsx +0 -456
- package/template/client/src/components/business-ui/tiptap-editor/hooks/use-tiptap-editor.ts +0 -47
- package/template/client/src/components/business-ui/tiptap-editor/index.ts +0 -38
- package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor-complete.tsx +0 -112
- package/template/client/src/components/business-ui/tiptap-editor/tiptap-editor.tsx +0 -169
- package/template/client/src/components/ui/streamdown.tsx +0 -186
- package/template/client/src/lib/shiki.ts +0 -92
package/template/client/src/components/business-ui/tiptap-editor/components/link-hover-toolbar.tsx
DELETED
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import { Link2Off, Pencil } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
import { LinkEditForm } from '@/components/business-ui/tiptap-editor/components/link-edit-form';
|
|
7
|
-
import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
|
|
8
|
-
import { cn } from '@/lib/utils';
|
|
9
|
-
import { Button } from '@/components/ui/button';
|
|
10
|
-
import {
|
|
11
|
-
Popover,
|
|
12
|
-
PopoverAnchor,
|
|
13
|
-
PopoverContent,
|
|
14
|
-
} from '@/components/ui/popover';
|
|
15
|
-
import {
|
|
16
|
-
Tooltip,
|
|
17
|
-
TooltipContent,
|
|
18
|
-
TooltipProvider,
|
|
19
|
-
TooltipTrigger,
|
|
20
|
-
} from '@/components/ui/tooltip';
|
|
21
|
-
|
|
22
|
-
type LinkHoverMode = 'toolbar' | 'edit';
|
|
23
|
-
|
|
24
|
-
export interface LinkHoverToolbarProps extends React.ComponentProps<'div'> {
|
|
25
|
-
/** Hover 链接后延时展示工具栏的时长(ms)。默认 700ms。 */
|
|
26
|
-
openDelay?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getClosestLinkEl(
|
|
30
|
-
target: EventTarget | null,
|
|
31
|
-
): HTMLAnchorElement | null {
|
|
32
|
-
if (!target || !(target instanceof HTMLElement)) return null;
|
|
33
|
-
const el = target.closest('a[href]');
|
|
34
|
-
if (!el) return null;
|
|
35
|
-
return el as HTMLAnchorElement;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function safeTextContent(el: HTMLElement | null) {
|
|
39
|
-
return (el?.textContent || '').trim();
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function LinkHoverToolbar({
|
|
43
|
-
openDelay = 700,
|
|
44
|
-
className,
|
|
45
|
-
...props
|
|
46
|
-
}: LinkHoverToolbarProps) {
|
|
47
|
-
const { editor } = useTiptapEditor();
|
|
48
|
-
const showTimerRef = React.useRef<number | null>(null);
|
|
49
|
-
const closeTimerRef = React.useRef<number | null>(null);
|
|
50
|
-
const afterCloseTimerRef = React.useRef<number | null>(null);
|
|
51
|
-
const linkElRef = React.useRef<HTMLAnchorElement | null>(null);
|
|
52
|
-
const hoveringLinkRef = React.useRef(false);
|
|
53
|
-
const hoveringPopoverRef = React.useRef(false);
|
|
54
|
-
|
|
55
|
-
const [open, setOpen] = React.useState(false);
|
|
56
|
-
const [mode, setMode] = React.useState<LinkHoverMode>('toolbar');
|
|
57
|
-
const [anchor, setAnchor] = React.useState<{
|
|
58
|
-
left: number;
|
|
59
|
-
top: number;
|
|
60
|
-
} | null>(null);
|
|
61
|
-
const [initialText, setInitialText] = React.useState('');
|
|
62
|
-
const [initialHref, setInitialHref] = React.useState('');
|
|
63
|
-
|
|
64
|
-
const clearShowTimer = React.useCallback(() => {
|
|
65
|
-
if (showTimerRef.current) window.clearTimeout(showTimerRef.current);
|
|
66
|
-
showTimerRef.current = null;
|
|
67
|
-
}, []);
|
|
68
|
-
|
|
69
|
-
const clearCloseTimer = React.useCallback(() => {
|
|
70
|
-
if (closeTimerRef.current) window.clearTimeout(closeTimerRef.current);
|
|
71
|
-
closeTimerRef.current = null;
|
|
72
|
-
}, []);
|
|
73
|
-
|
|
74
|
-
const clearAfterCloseTimer = React.useCallback(() => {
|
|
75
|
-
if (afterCloseTimerRef.current) {
|
|
76
|
-
window.clearTimeout(afterCloseTimerRef.current);
|
|
77
|
-
}
|
|
78
|
-
afterCloseTimerRef.current = null;
|
|
79
|
-
}, []);
|
|
80
|
-
|
|
81
|
-
const resetStateAfterClose = React.useCallback(() => {
|
|
82
|
-
setMode('toolbar');
|
|
83
|
-
setAnchor(null);
|
|
84
|
-
setInitialHref('');
|
|
85
|
-
setInitialText('');
|
|
86
|
-
}, []);
|
|
87
|
-
|
|
88
|
-
const scheduleResetAfterClose = React.useCallback(() => {
|
|
89
|
-
// PopoverContent 有关闭动画(animate-out)。关闭时如果立刻清空 anchor/href/text/mode,
|
|
90
|
-
// 会出现:内容先变空(布局抖动)或失去 anchor(跳到左上角)再消失。
|
|
91
|
-
clearAfterCloseTimer();
|
|
92
|
-
|
|
93
|
-
afterCloseTimerRef.current = window.setTimeout(() => {
|
|
94
|
-
resetStateAfterClose();
|
|
95
|
-
afterCloseTimerRef.current = null;
|
|
96
|
-
}, 200);
|
|
97
|
-
}, [clearAfterCloseTimer, resetStateAfterClose]);
|
|
98
|
-
|
|
99
|
-
const resetInteractionState = React.useCallback(() => {
|
|
100
|
-
clearShowTimer();
|
|
101
|
-
clearCloseTimer();
|
|
102
|
-
hoveringLinkRef.current = false;
|
|
103
|
-
hoveringPopoverRef.current = false;
|
|
104
|
-
linkElRef.current = null;
|
|
105
|
-
}, [clearCloseTimer, clearShowTimer]);
|
|
106
|
-
|
|
107
|
-
const closePopover = React.useCallback(() => {
|
|
108
|
-
setOpen(false);
|
|
109
|
-
resetInteractionState();
|
|
110
|
-
scheduleResetAfterClose();
|
|
111
|
-
}, [resetInteractionState, scheduleResetAfterClose]);
|
|
112
|
-
|
|
113
|
-
const computeAnchor = React.useCallback(() => {
|
|
114
|
-
if (!editor) return;
|
|
115
|
-
const linkEl = linkElRef.current;
|
|
116
|
-
if (!linkEl) return;
|
|
117
|
-
|
|
118
|
-
const editorRoot = editor.view.dom.closest(
|
|
119
|
-
"[data-slot='tiptap-editor']",
|
|
120
|
-
) as HTMLElement | null;
|
|
121
|
-
|
|
122
|
-
const linkRect = linkEl.getBoundingClientRect();
|
|
123
|
-
const rootRect = editorRoot?.getBoundingClientRect();
|
|
124
|
-
if (!rootRect) return;
|
|
125
|
-
|
|
126
|
-
setAnchor({
|
|
127
|
-
left: linkRect.left - rootRect.left,
|
|
128
|
-
top: linkRect.bottom - rootRect.top,
|
|
129
|
-
});
|
|
130
|
-
}, [editor]);
|
|
131
|
-
|
|
132
|
-
const scheduleCloseIfNeeded = React.useCallback(() => {
|
|
133
|
-
if (mode === 'edit') return;
|
|
134
|
-
clearCloseTimer();
|
|
135
|
-
|
|
136
|
-
closeTimerRef.current = window.setTimeout(() => {
|
|
137
|
-
if (hoveringLinkRef.current || hoveringPopoverRef.current) return;
|
|
138
|
-
closePopover();
|
|
139
|
-
}, 120);
|
|
140
|
-
}, [clearCloseTimer, closePopover, mode]);
|
|
141
|
-
|
|
142
|
-
const openForLink = React.useCallback(
|
|
143
|
-
(linkEl: HTMLAnchorElement) => {
|
|
144
|
-
clearAfterCloseTimer();
|
|
145
|
-
linkElRef.current = linkEl;
|
|
146
|
-
setInitialHref(linkEl.getAttribute('href') || '');
|
|
147
|
-
setInitialText(safeTextContent(linkEl));
|
|
148
|
-
|
|
149
|
-
setMode('toolbar');
|
|
150
|
-
computeAnchor();
|
|
151
|
-
setOpen(true);
|
|
152
|
-
},
|
|
153
|
-
[clearAfterCloseTimer, computeAnchor],
|
|
154
|
-
);
|
|
155
|
-
|
|
156
|
-
React.useEffect(() => {
|
|
157
|
-
if (!editor || !editor.view?.dom) return;
|
|
158
|
-
if (!editor.isEditable) return;
|
|
159
|
-
|
|
160
|
-
const dom = editor.view.dom;
|
|
161
|
-
|
|
162
|
-
const handlePointerOver = (event: PointerEvent) => {
|
|
163
|
-
const linkEl = getClosestLinkEl(event.target);
|
|
164
|
-
if (!linkEl) return;
|
|
165
|
-
|
|
166
|
-
if (linkElRef.current !== linkEl) {
|
|
167
|
-
clearShowTimer();
|
|
168
|
-
clearCloseTimer();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
hoveringLinkRef.current = true;
|
|
172
|
-
linkElRef.current = linkEl;
|
|
173
|
-
|
|
174
|
-
if (open && mode === 'edit') {
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
if (open && linkElRef.current === linkEl) {
|
|
179
|
-
computeAnchor();
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
clearShowTimer();
|
|
184
|
-
showTimerRef.current = window.setTimeout(() => {
|
|
185
|
-
if (!hoveringLinkRef.current) return;
|
|
186
|
-
openForLink(linkEl);
|
|
187
|
-
}, openDelay);
|
|
188
|
-
};
|
|
189
|
-
|
|
190
|
-
const handlePointerOut = (event: PointerEvent) => {
|
|
191
|
-
const fromLinkEl = getClosestLinkEl(event.target);
|
|
192
|
-
if (!fromLinkEl) return;
|
|
193
|
-
|
|
194
|
-
const toLinkEl = getClosestLinkEl(event.relatedTarget);
|
|
195
|
-
if (toLinkEl && toLinkEl === fromLinkEl) return;
|
|
196
|
-
|
|
197
|
-
hoveringLinkRef.current = false;
|
|
198
|
-
clearShowTimer();
|
|
199
|
-
|
|
200
|
-
if (open) scheduleCloseIfNeeded();
|
|
201
|
-
};
|
|
202
|
-
|
|
203
|
-
dom.addEventListener('pointerover', handlePointerOver);
|
|
204
|
-
dom.addEventListener('pointerout', handlePointerOut);
|
|
205
|
-
|
|
206
|
-
return () => {
|
|
207
|
-
dom.removeEventListener('pointerover', handlePointerOver);
|
|
208
|
-
dom.removeEventListener('pointerout', handlePointerOut);
|
|
209
|
-
clearShowTimer();
|
|
210
|
-
clearCloseTimer();
|
|
211
|
-
clearAfterCloseTimer();
|
|
212
|
-
};
|
|
213
|
-
}, [
|
|
214
|
-
clearCloseTimer,
|
|
215
|
-
clearAfterCloseTimer,
|
|
216
|
-
clearShowTimer,
|
|
217
|
-
computeAnchor,
|
|
218
|
-
editor,
|
|
219
|
-
mode,
|
|
220
|
-
open,
|
|
221
|
-
openDelay,
|
|
222
|
-
openForLink,
|
|
223
|
-
scheduleCloseIfNeeded,
|
|
224
|
-
]);
|
|
225
|
-
|
|
226
|
-
React.useEffect(() => {
|
|
227
|
-
if (!open) return;
|
|
228
|
-
|
|
229
|
-
const handleWindow = () => computeAnchor();
|
|
230
|
-
window.addEventListener('scroll', handleWindow, true);
|
|
231
|
-
window.addEventListener('resize', handleWindow);
|
|
232
|
-
|
|
233
|
-
return () => {
|
|
234
|
-
window.removeEventListener('scroll', handleWindow, true);
|
|
235
|
-
window.removeEventListener('resize', handleWindow);
|
|
236
|
-
};
|
|
237
|
-
}, [computeAnchor, open]);
|
|
238
|
-
|
|
239
|
-
if (!editor || !editor.isEditable) return null;
|
|
240
|
-
|
|
241
|
-
const href = initialHref.trim();
|
|
242
|
-
|
|
243
|
-
const selectHoveredLink = (options?: { focus?: boolean }) => {
|
|
244
|
-
const focus = options?.focus ?? false;
|
|
245
|
-
const linkEl = linkElRef.current;
|
|
246
|
-
if (!linkEl) return;
|
|
247
|
-
|
|
248
|
-
try {
|
|
249
|
-
const pos = editor.view.posAtDOM(linkEl, 0);
|
|
250
|
-
if (focus) {
|
|
251
|
-
editor.chain().focus().setTextSelection(pos).run();
|
|
252
|
-
} else {
|
|
253
|
-
editor.commands.setTextSelection(pos);
|
|
254
|
-
}
|
|
255
|
-
} catch {
|
|
256
|
-
if (focus) editor.chain().focus().run();
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
|
|
260
|
-
const unlink = () => {
|
|
261
|
-
selectHoveredLink({ focus: true });
|
|
262
|
-
editor.chain().extendMarkRange('link').unsetLink().run();
|
|
263
|
-
closePopover();
|
|
264
|
-
};
|
|
265
|
-
|
|
266
|
-
return (
|
|
267
|
-
<div
|
|
268
|
-
className={cn('pointer-events-none absolute inset-0', className)}
|
|
269
|
-
{...props}
|
|
270
|
-
>
|
|
271
|
-
<Popover
|
|
272
|
-
open={open}
|
|
273
|
-
onOpenChange={(nextOpen) => {
|
|
274
|
-
setOpen(nextOpen);
|
|
275
|
-
if (!nextOpen) {
|
|
276
|
-
resetInteractionState();
|
|
277
|
-
scheduleResetAfterClose();
|
|
278
|
-
} else {
|
|
279
|
-
clearAfterCloseTimer();
|
|
280
|
-
}
|
|
281
|
-
}}
|
|
282
|
-
>
|
|
283
|
-
{anchor && (
|
|
284
|
-
<PopoverAnchor asChild>
|
|
285
|
-
<span
|
|
286
|
-
aria-hidden="true"
|
|
287
|
-
className="absolute"
|
|
288
|
-
style={{
|
|
289
|
-
left: anchor.left,
|
|
290
|
-
top: anchor.top,
|
|
291
|
-
width: 1,
|
|
292
|
-
height: 1,
|
|
293
|
-
}}
|
|
294
|
-
/>
|
|
295
|
-
</PopoverAnchor>
|
|
296
|
-
)}
|
|
297
|
-
|
|
298
|
-
<PopoverContent
|
|
299
|
-
align="start"
|
|
300
|
-
sideOffset={6}
|
|
301
|
-
className={cn(
|
|
302
|
-
'w-105 shadow-lg',
|
|
303
|
-
mode === 'toolbar' ? 'px-4 py-3' : 'p-5',
|
|
304
|
-
)}
|
|
305
|
-
onOpenAutoFocus={(e) => e.preventDefault()}
|
|
306
|
-
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
307
|
-
onMouseEnter={() => {
|
|
308
|
-
hoveringPopoverRef.current = true;
|
|
309
|
-
clearCloseTimer();
|
|
310
|
-
}}
|
|
311
|
-
onMouseLeave={() => {
|
|
312
|
-
hoveringPopoverRef.current = false;
|
|
313
|
-
scheduleCloseIfNeeded();
|
|
314
|
-
}}
|
|
315
|
-
>
|
|
316
|
-
{mode === 'toolbar' ? (
|
|
317
|
-
<div className="flex items-center gap-2">
|
|
318
|
-
<div className="min-w-0 flex-1 truncate text-sm text-foreground">
|
|
319
|
-
{href}
|
|
320
|
-
</div>
|
|
321
|
-
|
|
322
|
-
<TooltipProvider>
|
|
323
|
-
<Tooltip delayDuration={700}>
|
|
324
|
-
<TooltipTrigger asChild>
|
|
325
|
-
<Button
|
|
326
|
-
variant="ghost"
|
|
327
|
-
size="sm"
|
|
328
|
-
className="size-6 px-0"
|
|
329
|
-
onClick={() => {
|
|
330
|
-
// NOTE: 不要在这里 focus 编辑器,否则 Radix Popover 会因为 focus outside 直接 dismiss,
|
|
331
|
-
// 从而出现“偶尔没切到表单而是关闭弹层”的竞态。
|
|
332
|
-
clearCloseTimer();
|
|
333
|
-
selectHoveredLink({ focus: false });
|
|
334
|
-
setMode('edit');
|
|
335
|
-
setOpen(true);
|
|
336
|
-
}}
|
|
337
|
-
>
|
|
338
|
-
<Pencil className="size-4" />
|
|
339
|
-
<span className="sr-only">编辑链接</span>
|
|
340
|
-
</Button>
|
|
341
|
-
</TooltipTrigger>
|
|
342
|
-
<TooltipContent>
|
|
343
|
-
<p>编辑链接</p>
|
|
344
|
-
</TooltipContent>
|
|
345
|
-
</Tooltip>
|
|
346
|
-
|
|
347
|
-
<Tooltip delayDuration={700}>
|
|
348
|
-
<TooltipTrigger asChild>
|
|
349
|
-
<Button
|
|
350
|
-
variant="ghost"
|
|
351
|
-
size="sm"
|
|
352
|
-
className="size-6 px-0"
|
|
353
|
-
onClick={unlink}
|
|
354
|
-
>
|
|
355
|
-
<Link2Off className="size-4" />
|
|
356
|
-
<span className="sr-only">移除链接</span>
|
|
357
|
-
</Button>
|
|
358
|
-
</TooltipTrigger>
|
|
359
|
-
<TooltipContent>
|
|
360
|
-
<p>移除链接</p>
|
|
361
|
-
</TooltipContent>
|
|
362
|
-
</Tooltip>
|
|
363
|
-
</TooltipProvider>
|
|
364
|
-
</div>
|
|
365
|
-
) : (
|
|
366
|
-
<LinkEditForm
|
|
367
|
-
open={open && mode === 'edit'}
|
|
368
|
-
initialText={initialText}
|
|
369
|
-
initialHref={initialHref}
|
|
370
|
-
autoFocusHref
|
|
371
|
-
onDone={() => {
|
|
372
|
-
closePopover();
|
|
373
|
-
}}
|
|
374
|
-
/>
|
|
375
|
-
)}
|
|
376
|
-
</PopoverContent>
|
|
377
|
-
</Popover>
|
|
378
|
-
</div>
|
|
379
|
-
);
|
|
380
|
-
}
|
package/template/client/src/components/business-ui/tiptap-editor/components/link-toolbar-button.tsx
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import { Link as LinkIcon } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
import { LinkEditForm } from '@/components/business-ui/tiptap-editor/components/link-edit-form';
|
|
7
|
-
import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
|
|
8
|
-
import { Button } from '@/components/ui/button';
|
|
9
|
-
import {
|
|
10
|
-
Popover,
|
|
11
|
-
PopoverContent,
|
|
12
|
-
PopoverTrigger,
|
|
13
|
-
} from '@/components/ui/popover';
|
|
14
|
-
import {
|
|
15
|
-
Tooltip,
|
|
16
|
-
TooltipContent,
|
|
17
|
-
TooltipProvider,
|
|
18
|
-
TooltipTrigger,
|
|
19
|
-
} from '@/components/ui/tooltip';
|
|
20
|
-
|
|
21
|
-
export function LinkToolbarButton() {
|
|
22
|
-
const { editor } = useTiptapEditor();
|
|
23
|
-
const [open, setOpen] = React.useState(false);
|
|
24
|
-
const [initialText, setInitialText] = React.useState('');
|
|
25
|
-
const [initialHref, setInitialHref] = React.useState('');
|
|
26
|
-
|
|
27
|
-
if (!editor) return null;
|
|
28
|
-
|
|
29
|
-
const resetForm = () => {
|
|
30
|
-
setInitialText('');
|
|
31
|
-
setInitialHref('');
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const openDialog = () => {
|
|
35
|
-
const { from, to } = editor.state.selection;
|
|
36
|
-
const selectedText = editor.state.doc.textBetween(from, to, ' ');
|
|
37
|
-
const currentHref = (editor.getAttributes('link')?.href as string) || '';
|
|
38
|
-
|
|
39
|
-
setInitialText(selectedText);
|
|
40
|
-
setInitialHref(currentHref);
|
|
41
|
-
setOpen(true);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const trigger = (
|
|
45
|
-
<Button
|
|
46
|
-
variant="ghost"
|
|
47
|
-
size="sm"
|
|
48
|
-
className="size-6 px-0"
|
|
49
|
-
onClick={openDialog}
|
|
50
|
-
disabled={!editor.can().toggleLink({ href: '' })}
|
|
51
|
-
>
|
|
52
|
-
<LinkIcon className="size-4" />
|
|
53
|
-
<span className="sr-only">链接</span>
|
|
54
|
-
</Button>
|
|
55
|
-
);
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<Popover
|
|
59
|
-
open={open}
|
|
60
|
-
onOpenChange={(nextOpen) => {
|
|
61
|
-
setOpen(nextOpen);
|
|
62
|
-
if (!nextOpen) resetForm();
|
|
63
|
-
}}
|
|
64
|
-
>
|
|
65
|
-
<TooltipProvider>
|
|
66
|
-
<Tooltip delayDuration={700}>
|
|
67
|
-
<TooltipTrigger asChild>
|
|
68
|
-
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
|
69
|
-
</TooltipTrigger>
|
|
70
|
-
{!open && (
|
|
71
|
-
<TooltipContent>
|
|
72
|
-
<p>链接</p>
|
|
73
|
-
</TooltipContent>
|
|
74
|
-
)}
|
|
75
|
-
</Tooltip>
|
|
76
|
-
</TooltipProvider>
|
|
77
|
-
|
|
78
|
-
<PopoverContent
|
|
79
|
-
className="w-105 p-5 shadow-lg"
|
|
80
|
-
align="start"
|
|
81
|
-
sideOffset={6}
|
|
82
|
-
>
|
|
83
|
-
<LinkEditForm
|
|
84
|
-
open={open}
|
|
85
|
-
initialText={initialText}
|
|
86
|
-
initialHref={initialHref}
|
|
87
|
-
autoFocusHref
|
|
88
|
-
onDone={() => {
|
|
89
|
-
setOpen(false);
|
|
90
|
-
resetForm();
|
|
91
|
-
}}
|
|
92
|
-
/>
|
|
93
|
-
</PopoverContent>
|
|
94
|
-
</Popover>
|
|
95
|
-
);
|
|
96
|
-
}
|
package/template/client/src/components/business-ui/tiptap-editor/components/list-toolbar-button.tsx
DELETED
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Check, ChevronDown, List, ListOrdered, ListTodo } from 'lucide-react';
|
|
4
|
-
|
|
5
|
-
import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
|
|
6
|
-
import { cn } from '@/lib/utils';
|
|
7
|
-
import { Button } from '@/components/ui/button';
|
|
8
|
-
import {
|
|
9
|
-
DropdownMenu,
|
|
10
|
-
DropdownMenuContent,
|
|
11
|
-
DropdownMenuItem,
|
|
12
|
-
DropdownMenuTrigger,
|
|
13
|
-
} from '@/components/ui/dropdown-menu';
|
|
14
|
-
|
|
15
|
-
const LIST_OPTIONS = [
|
|
16
|
-
{ type: 'bulletList', label: '无序列表', icon: List },
|
|
17
|
-
{ type: 'orderedList', label: '有序列表', icon: ListOrdered },
|
|
18
|
-
{ type: 'taskList', label: '任务列表', icon: ListTodo },
|
|
19
|
-
] as const;
|
|
20
|
-
|
|
21
|
-
export function ListToolbarButton() {
|
|
22
|
-
const { editor } = useTiptapEditor();
|
|
23
|
-
|
|
24
|
-
if (!editor) return null;
|
|
25
|
-
|
|
26
|
-
const handleSelect = (type: string) => {
|
|
27
|
-
switch (type) {
|
|
28
|
-
case 'bulletList':
|
|
29
|
-
editor.chain().focus().toggleBulletList().run();
|
|
30
|
-
break;
|
|
31
|
-
case 'orderedList':
|
|
32
|
-
editor.chain().focus().toggleOrderedList().run();
|
|
33
|
-
break;
|
|
34
|
-
case 'taskList':
|
|
35
|
-
editor.chain().focus().toggleTaskList().run();
|
|
36
|
-
break;
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<DropdownMenu>
|
|
42
|
-
<DropdownMenuTrigger asChild>
|
|
43
|
-
<Button variant="ghost" size="sm" className="h-6 gap-0.5 px-2">
|
|
44
|
-
<List className="size-4" />
|
|
45
|
-
<ChevronDown className="size-3.5 text-muted-foreground" />
|
|
46
|
-
<span className="sr-only">列表</span>
|
|
47
|
-
</Button>
|
|
48
|
-
</DropdownMenuTrigger>
|
|
49
|
-
<DropdownMenuContent
|
|
50
|
-
align="start"
|
|
51
|
-
className="w-50"
|
|
52
|
-
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
53
|
-
>
|
|
54
|
-
{LIST_OPTIONS.map((option) => {
|
|
55
|
-
const Icon = option.icon;
|
|
56
|
-
const active = editor.isActive(option.type);
|
|
57
|
-
|
|
58
|
-
return (
|
|
59
|
-
<DropdownMenuItem
|
|
60
|
-
key={option.type}
|
|
61
|
-
onClick={() => handleSelect(option.type)}
|
|
62
|
-
className={cn('justify-between', active && 'bg-accent')}
|
|
63
|
-
>
|
|
64
|
-
<span className="flex items-center gap-2">
|
|
65
|
-
<Icon className="size-4" />
|
|
66
|
-
{option.label}
|
|
67
|
-
</span>
|
|
68
|
-
{active && <Check className="size-4" />}
|
|
69
|
-
</DropdownMenuItem>
|
|
70
|
-
);
|
|
71
|
-
})}
|
|
72
|
-
</DropdownMenuContent>
|
|
73
|
-
</DropdownMenu>
|
|
74
|
-
);
|
|
75
|
-
}
|
package/template/client/src/components/business-ui/tiptap-editor/components/mark-toolbar-button.tsx
DELETED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import { Bold, Code, Italic, Strikethrough, Underline } from 'lucide-react';
|
|
5
|
-
|
|
6
|
-
import { useTiptapEditor } from '@/components/business-ui/tiptap-editor/hooks/use-tiptap-editor';
|
|
7
|
-
import { Toggle } from '@/components/ui/toggle';
|
|
8
|
-
import {
|
|
9
|
-
Tooltip,
|
|
10
|
-
TooltipContent,
|
|
11
|
-
TooltipProvider,
|
|
12
|
-
TooltipTrigger,
|
|
13
|
-
} from '@/components/ui/tooltip';
|
|
14
|
-
|
|
15
|
-
interface MarkToolbarButtonProps {
|
|
16
|
-
format: 'bold' | 'italic' | 'underline' | 'strike' | 'code';
|
|
17
|
-
icon?: React.ReactNode;
|
|
18
|
-
tooltip?: string;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function MarkToolbarButton({
|
|
22
|
-
format,
|
|
23
|
-
icon,
|
|
24
|
-
tooltip,
|
|
25
|
-
}: MarkToolbarButtonProps) {
|
|
26
|
-
const { editor } = useTiptapEditor();
|
|
27
|
-
|
|
28
|
-
if (!editor) return null;
|
|
29
|
-
|
|
30
|
-
const isActive = editor.isActive(format);
|
|
31
|
-
|
|
32
|
-
let disabled = false;
|
|
33
|
-
|
|
34
|
-
switch (format) {
|
|
35
|
-
case 'bold':
|
|
36
|
-
disabled = !editor.can().toggleBold();
|
|
37
|
-
break;
|
|
38
|
-
case 'italic':
|
|
39
|
-
disabled = !editor.can().toggleItalic();
|
|
40
|
-
break;
|
|
41
|
-
case 'underline':
|
|
42
|
-
disabled = !editor.can().toggleUnderline();
|
|
43
|
-
break;
|
|
44
|
-
case 'strike':
|
|
45
|
-
disabled = !editor.can().toggleStrike();
|
|
46
|
-
break;
|
|
47
|
-
case 'code':
|
|
48
|
-
disabled = !editor.can().toggleCode();
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const toggle = () => {
|
|
53
|
-
switch (format) {
|
|
54
|
-
case 'bold':
|
|
55
|
-
editor.chain().focus().toggleBold().run();
|
|
56
|
-
break;
|
|
57
|
-
case 'italic':
|
|
58
|
-
editor.chain().focus().toggleItalic().run();
|
|
59
|
-
break;
|
|
60
|
-
case 'underline':
|
|
61
|
-
editor.chain().focus().toggleUnderline().run();
|
|
62
|
-
break;
|
|
63
|
-
case 'strike':
|
|
64
|
-
editor.chain().focus().toggleStrike().run();
|
|
65
|
-
break;
|
|
66
|
-
case 'code':
|
|
67
|
-
editor.chain().focus().toggleCode().run();
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
const icons = {
|
|
73
|
-
bold: <Bold className="size-4" />,
|
|
74
|
-
italic: <Italic className="size-4" />,
|
|
75
|
-
underline: <Underline className="size-4" />,
|
|
76
|
-
strike: <Strikethrough className="size-4" />,
|
|
77
|
-
code: <Code className="size-4" />,
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
const labels: Record<typeof format, string> = {
|
|
81
|
-
bold: '粗体',
|
|
82
|
-
italic: '斜体',
|
|
83
|
-
underline: '下划线',
|
|
84
|
-
strike: '删除线',
|
|
85
|
-
code: '行内代码',
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const tooltips: Record<typeof format, string> = {
|
|
89
|
-
bold: '粗体(⌘+B)',
|
|
90
|
-
italic: '斜体(⌘+I)',
|
|
91
|
-
underline: '下划线(⌘+U)',
|
|
92
|
-
strike: '删除线(⌘+Shift+X)',
|
|
93
|
-
code: '行内代码(⌘+E)',
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
return (
|
|
97
|
-
<TooltipProvider>
|
|
98
|
-
<Tooltip delayDuration={700}>
|
|
99
|
-
<TooltipTrigger>
|
|
100
|
-
<Toggle
|
|
101
|
-
size="sm"
|
|
102
|
-
pressed={isActive}
|
|
103
|
-
onPressedChange={toggle}
|
|
104
|
-
disabled={disabled}
|
|
105
|
-
aria-label={labels[format]}
|
|
106
|
-
className="size-6 p-0"
|
|
107
|
-
asChild
|
|
108
|
-
>
|
|
109
|
-
<div>{icon || icons[format]}</div>
|
|
110
|
-
</Toggle>
|
|
111
|
-
</TooltipTrigger>
|
|
112
|
-
<TooltipContent>
|
|
113
|
-
<p>{tooltip || tooltips[format]}</p>
|
|
114
|
-
</TooltipContent>
|
|
115
|
-
</Tooltip>
|
|
116
|
-
</TooltipProvider>
|
|
117
|
-
);
|
|
118
|
-
}
|