@lark-apaas/coding-template-nestjs-react-fullstack 0.1.35 → 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 +10339 -18979
- 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
|
@@ -1,456 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import * as React from 'react';
|
|
4
|
-
import { mergeAttributes, type CommandProps } from '@tiptap/core';
|
|
5
|
-
import TiptapImage from '@tiptap/extension-image';
|
|
6
|
-
import { type Node as ProseMirrorNode } from '@tiptap/pm/model';
|
|
7
|
-
import {
|
|
8
|
-
NodeViewWrapper,
|
|
9
|
-
ReactNodeViewRenderer,
|
|
10
|
-
type NodeViewProps,
|
|
11
|
-
} from '@tiptap/react';
|
|
12
|
-
import { nanoid } from 'nanoid';
|
|
13
|
-
|
|
14
|
-
import { uploadFile } from '@/components/business-ui/api/files/service';
|
|
15
|
-
import { cn } from '@/lib/utils';
|
|
16
|
-
|
|
17
|
-
export interface ImageAttrs {
|
|
18
|
-
src: string;
|
|
19
|
-
alt?: string;
|
|
20
|
-
title?: string;
|
|
21
|
-
width?: number | string | null;
|
|
22
|
-
height?: number | null;
|
|
23
|
-
uploadId?: string | null;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export type ImageUploadFn = (file: File) => Promise<string>;
|
|
27
|
-
|
|
28
|
-
export interface ImageOptions {
|
|
29
|
-
upload?: ImageUploadFn;
|
|
30
|
-
HTMLAttributes?: Record<string, unknown>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
declare module '@tiptap/core' {
|
|
34
|
-
interface Commands<ReturnType> {
|
|
35
|
-
imageUpload: {
|
|
36
|
-
insertImages: (files: File[]) => ReturnType;
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function findImageByUploadId(doc: ProseMirrorNode, uploadId: string) {
|
|
42
|
-
let foundPos: number | null = null;
|
|
43
|
-
doc.descendants((node: ProseMirrorNode, pos: number) => {
|
|
44
|
-
if (foundPos != null) return false;
|
|
45
|
-
if (node.type?.name === 'image' && node.attrs?.uploadId === uploadId) {
|
|
46
|
-
foundPos = pos;
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
});
|
|
51
|
-
return foundPos;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function clampSize(value: number, { min, max }: { min: number; max: number }) {
|
|
55
|
-
if (!Number.isFinite(value)) return min;
|
|
56
|
-
return Math.max(min, Math.min(max, value));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function ImageNodeView(props: NodeViewProps) {
|
|
60
|
-
const { editor, selected } = props;
|
|
61
|
-
const attrs = props.node.attrs as ImageAttrs;
|
|
62
|
-
const wrapperRef = React.useRef<HTMLDivElement>(null);
|
|
63
|
-
const imgRef = React.useRef<HTMLImageElement>(null);
|
|
64
|
-
const [naturalRatio, setNaturalRatio] = React.useState<number | null>(null);
|
|
65
|
-
|
|
66
|
-
const isEditable = editor.isEditable;
|
|
67
|
-
|
|
68
|
-
// 计算宽高比:优先使用属性中的宽高,否则使用自然宽高比
|
|
69
|
-
const aspectRatio =
|
|
70
|
-
typeof attrs.width === 'number' && attrs.height
|
|
71
|
-
? attrs.width / attrs.height
|
|
72
|
-
: naturalRatio;
|
|
73
|
-
|
|
74
|
-
const style = React.useMemo<React.CSSProperties>(() => {
|
|
75
|
-
if (attrs.width === '100%') {
|
|
76
|
-
return {
|
|
77
|
-
width: '100%',
|
|
78
|
-
height: 'auto',
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
if (typeof attrs.width === 'number' && attrs.height) {
|
|
82
|
-
return {
|
|
83
|
-
width: `${attrs.width}px`,
|
|
84
|
-
aspectRatio: `${attrs.width} / ${attrs.height}`,
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
return {};
|
|
88
|
-
}, [attrs.width, attrs.height]);
|
|
89
|
-
|
|
90
|
-
const startResize = React.useCallback(
|
|
91
|
-
(corner: 'nw' | 'ne' | 'sw' | 'se') =>
|
|
92
|
-
(e: React.PointerEvent<HTMLButtonElement>) => {
|
|
93
|
-
if (!isEditable) return;
|
|
94
|
-
e.preventDefault();
|
|
95
|
-
e.stopPropagation();
|
|
96
|
-
|
|
97
|
-
const img = imgRef.current;
|
|
98
|
-
const wrapper = wrapperRef.current;
|
|
99
|
-
if (!img || !wrapper) return;
|
|
100
|
-
|
|
101
|
-
const rect = img.getBoundingClientRect();
|
|
102
|
-
const startWidth = rect.width;
|
|
103
|
-
const startHeight = rect.height;
|
|
104
|
-
|
|
105
|
-
const ratio =
|
|
106
|
-
aspectRatio ?? (startHeight ? startWidth / startHeight : null);
|
|
107
|
-
if (!ratio) return;
|
|
108
|
-
|
|
109
|
-
const startX = e.clientX;
|
|
110
|
-
const startY = e.clientY;
|
|
111
|
-
|
|
112
|
-
const containerWidth = wrapper.parentElement?.clientWidth ?? rect.width;
|
|
113
|
-
const minWidth = 80;
|
|
114
|
-
|
|
115
|
-
const onMove = (ev: PointerEvent) => {
|
|
116
|
-
const dx = ev.clientX - startX;
|
|
117
|
-
const dy = ev.clientY - startY;
|
|
118
|
-
|
|
119
|
-
// 保持宽高比
|
|
120
|
-
// 根据角落方向调整宽度
|
|
121
|
-
let nextWidth = startWidth;
|
|
122
|
-
if (corner === 'ne' || corner === 'se') {
|
|
123
|
-
nextWidth = startWidth + dx;
|
|
124
|
-
} else {
|
|
125
|
-
nextWidth = startWidth - dx;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// 允许垂直拖拽调整大小,根据角落映射 dy 到宽度变化
|
|
129
|
-
const dyAsWidth = dy * ratio;
|
|
130
|
-
if (corner === 'se' || corner === 'sw') {
|
|
131
|
-
nextWidth =
|
|
132
|
-
Math.abs(dyAsWidth) > Math.abs(dx)
|
|
133
|
-
? startWidth + dyAsWidth
|
|
134
|
-
: nextWidth;
|
|
135
|
-
} else {
|
|
136
|
-
nextWidth =
|
|
137
|
-
Math.abs(dyAsWidth) > Math.abs(dx)
|
|
138
|
-
? startWidth - dyAsWidth
|
|
139
|
-
: nextWidth;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
nextWidth = clampSize(nextWidth, {
|
|
143
|
-
min: minWidth,
|
|
144
|
-
max: containerWidth,
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
const nextHeight = nextWidth / ratio;
|
|
148
|
-
|
|
149
|
-
// 如果接近容器宽度,吸附到 100%
|
|
150
|
-
if (Math.abs(containerWidth - nextWidth) < 10) {
|
|
151
|
-
props.updateAttributes({
|
|
152
|
-
width: '100%',
|
|
153
|
-
height: null,
|
|
154
|
-
});
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
props.updateAttributes({
|
|
159
|
-
width: Math.round(nextWidth),
|
|
160
|
-
height: Math.round(nextHeight),
|
|
161
|
-
});
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const onUp = () => {
|
|
165
|
-
window.removeEventListener('pointermove', onMove);
|
|
166
|
-
window.removeEventListener('pointerup', onUp);
|
|
167
|
-
};
|
|
168
|
-
|
|
169
|
-
window.addEventListener('pointermove', onMove);
|
|
170
|
-
window.addEventListener('pointerup', onUp);
|
|
171
|
-
},
|
|
172
|
-
[aspectRatio, isEditable, props],
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
return (
|
|
176
|
-
<NodeViewWrapper
|
|
177
|
-
className={cn('not-prose my-3 flex justify-center')}
|
|
178
|
-
data-type="image-block"
|
|
179
|
-
>
|
|
180
|
-
<div
|
|
181
|
-
ref={wrapperRef}
|
|
182
|
-
className={cn(
|
|
183
|
-
'relative inline-block max-w-full rounded-md',
|
|
184
|
-
selected && isEditable ? 'ring-2 ring-ring/50' : '',
|
|
185
|
-
)}
|
|
186
|
-
style={style}
|
|
187
|
-
contentEditable={false}
|
|
188
|
-
>
|
|
189
|
-
<img
|
|
190
|
-
ref={imgRef}
|
|
191
|
-
src={attrs.src}
|
|
192
|
-
alt={attrs.alt ?? ''}
|
|
193
|
-
title={attrs.title}
|
|
194
|
-
className={cn('block size-full rounded-md object-cover')}
|
|
195
|
-
draggable={false}
|
|
196
|
-
onLoad={(e) => {
|
|
197
|
-
const img = e.currentTarget;
|
|
198
|
-
if (img.naturalWidth && img.naturalHeight) {
|
|
199
|
-
setNaturalRatio(img.naturalWidth / img.naturalHeight);
|
|
200
|
-
}
|
|
201
|
-
}}
|
|
202
|
-
/>
|
|
203
|
-
|
|
204
|
-
{attrs.uploadId ? (
|
|
205
|
-
<>
|
|
206
|
-
<div className="pointer-events-none absolute inset-0 rounded-md bg-black/25" />
|
|
207
|
-
<div className="pointer-events-none absolute top-2 right-2 flex items-center gap-1 rounded-md bg-primary-foreground/20 px-2 py-1 text-sm/5 text-primary-foreground">
|
|
208
|
-
<span
|
|
209
|
-
className="size-4 animate-spin rounded-full border-2 border-primary-foreground border-t-transparent"
|
|
210
|
-
aria-hidden="true"
|
|
211
|
-
/>
|
|
212
|
-
<span className="sr-only">图片上传中</span>
|
|
213
|
-
</div>
|
|
214
|
-
</>
|
|
215
|
-
) : null}
|
|
216
|
-
|
|
217
|
-
{selected && isEditable && (
|
|
218
|
-
<>
|
|
219
|
-
<ResizeHandle corner="nw" onPointerDown={startResize('nw')} />
|
|
220
|
-
<ResizeHandle corner="ne" onPointerDown={startResize('ne')} />
|
|
221
|
-
<ResizeHandle corner="sw" onPointerDown={startResize('sw')} />
|
|
222
|
-
<ResizeHandle corner="se" onPointerDown={startResize('se')} />
|
|
223
|
-
</>
|
|
224
|
-
)}
|
|
225
|
-
</div>
|
|
226
|
-
</NodeViewWrapper>
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function ResizeHandle({
|
|
231
|
-
corner,
|
|
232
|
-
onPointerDown,
|
|
233
|
-
}: {
|
|
234
|
-
corner: 'nw' | 'ne' | 'sw' | 'se';
|
|
235
|
-
onPointerDown: (e: React.PointerEvent<HTMLButtonElement>) => void;
|
|
236
|
-
}) {
|
|
237
|
-
const positionClass =
|
|
238
|
-
corner === 'nw'
|
|
239
|
-
? '-left-1.5 -top-1.5'
|
|
240
|
-
: corner === 'ne'
|
|
241
|
-
? '-right-1.5 -top-1.5'
|
|
242
|
-
: corner === 'sw'
|
|
243
|
-
? '-left-1.5 -bottom-1.5'
|
|
244
|
-
: '-right-1.5 -bottom-1.5';
|
|
245
|
-
|
|
246
|
-
const cursorClass =
|
|
247
|
-
corner === 'nw' || corner === 'se'
|
|
248
|
-
? 'cursor-nwse-resize'
|
|
249
|
-
: 'cursor-nesw-resize';
|
|
250
|
-
|
|
251
|
-
const label =
|
|
252
|
-
corner === 'nw'
|
|
253
|
-
? 'Resize image from top-left'
|
|
254
|
-
: corner === 'ne'
|
|
255
|
-
? 'Resize image from top-right'
|
|
256
|
-
: corner === 'sw'
|
|
257
|
-
? 'Resize image from bottom-left'
|
|
258
|
-
: 'Resize image from bottom-right';
|
|
259
|
-
|
|
260
|
-
return (
|
|
261
|
-
<button
|
|
262
|
-
type="button"
|
|
263
|
-
aria-label={label}
|
|
264
|
-
className={cn(
|
|
265
|
-
'absolute z-10 flex size-2.5 items-center justify-center rounded-full border border-primary bg-background shadow-sm',
|
|
266
|
-
cursorClass,
|
|
267
|
-
positionClass,
|
|
268
|
-
)}
|
|
269
|
-
onPointerDown={onPointerDown}
|
|
270
|
-
/>
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
export const Image = TiptapImage.extend<ImageOptions>({
|
|
275
|
-
addOptions() {
|
|
276
|
-
return {
|
|
277
|
-
upload: async (file: File) => {
|
|
278
|
-
const data = await uploadFile(file);
|
|
279
|
-
return data.url;
|
|
280
|
-
},
|
|
281
|
-
};
|
|
282
|
-
},
|
|
283
|
-
|
|
284
|
-
addAttributes() {
|
|
285
|
-
return {
|
|
286
|
-
...this.parent?.(),
|
|
287
|
-
width: {
|
|
288
|
-
default: null,
|
|
289
|
-
parseHTML: (element) => {
|
|
290
|
-
const styleWidth = element.style?.width;
|
|
291
|
-
if (styleWidth && styleWidth.trim().endsWith('%')) {
|
|
292
|
-
return styleWidth.trim();
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
const rawWidth = element.getAttribute('width');
|
|
296
|
-
if (!rawWidth) return null;
|
|
297
|
-
if (rawWidth.trim().endsWith('%')) return rawWidth.trim();
|
|
298
|
-
|
|
299
|
-
const parsed = Number.parseInt(rawWidth, 10);
|
|
300
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
301
|
-
},
|
|
302
|
-
renderHTML: (attributes) => {
|
|
303
|
-
const width = attributes.width;
|
|
304
|
-
if (!width) return {};
|
|
305
|
-
if (width === '100%') {
|
|
306
|
-
return {
|
|
307
|
-
style: 'width: 100%; height: auto;',
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
if (typeof width === 'number') {
|
|
311
|
-
return { width: String(width) };
|
|
312
|
-
}
|
|
313
|
-
return {
|
|
314
|
-
style: `width: ${String(width)}; height: auto;`,
|
|
315
|
-
};
|
|
316
|
-
},
|
|
317
|
-
},
|
|
318
|
-
height: {
|
|
319
|
-
default: null,
|
|
320
|
-
parseHTML: (element) => {
|
|
321
|
-
const rawHeight = element.getAttribute('height');
|
|
322
|
-
if (!rawHeight) return null;
|
|
323
|
-
const parsed = Number.parseInt(rawHeight, 10);
|
|
324
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
325
|
-
},
|
|
326
|
-
renderHTML: (attributes) => {
|
|
327
|
-
const height = attributes.height;
|
|
328
|
-
if (!height) return {};
|
|
329
|
-
return { height: String(height) };
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
uploadId: {
|
|
333
|
-
default: null,
|
|
334
|
-
rendered: false,
|
|
335
|
-
},
|
|
336
|
-
};
|
|
337
|
-
},
|
|
338
|
-
|
|
339
|
-
renderHTML({ HTMLAttributes }) {
|
|
340
|
-
// Width/height serialization happens via addAttributes().
|
|
341
|
-
// We intentionally do not put layout classes on the wrapper; consumers can style by data-type.
|
|
342
|
-
return [
|
|
343
|
-
'div',
|
|
344
|
-
{
|
|
345
|
-
'data-type': 'image-block',
|
|
346
|
-
},
|
|
347
|
-
[
|
|
348
|
-
'img',
|
|
349
|
-
mergeAttributes(
|
|
350
|
-
this.options.HTMLAttributes ?? {},
|
|
351
|
-
Object.fromEntries(Object.entries(HTMLAttributes)),
|
|
352
|
-
),
|
|
353
|
-
],
|
|
354
|
-
];
|
|
355
|
-
},
|
|
356
|
-
|
|
357
|
-
parseHTML() {
|
|
358
|
-
return [
|
|
359
|
-
{
|
|
360
|
-
tag: 'div[data-type="image-block"] img[src]',
|
|
361
|
-
},
|
|
362
|
-
{
|
|
363
|
-
tag: 'img[src]:not([src^="data:"])',
|
|
364
|
-
},
|
|
365
|
-
];
|
|
366
|
-
},
|
|
367
|
-
|
|
368
|
-
addCommands() {
|
|
369
|
-
return {
|
|
370
|
-
...this.parent?.(),
|
|
371
|
-
insertImages:
|
|
372
|
-
(files: File[]) =>
|
|
373
|
-
({ editor, commands }: CommandProps) => {
|
|
374
|
-
if (!files || files.length === 0) return false;
|
|
375
|
-
|
|
376
|
-
const upload = this.options.upload;
|
|
377
|
-
if (!upload) return false;
|
|
378
|
-
|
|
379
|
-
const uploads = files.map((file) => {
|
|
380
|
-
const uploadId = nanoid();
|
|
381
|
-
const previewUrl = URL.createObjectURL(file);
|
|
382
|
-
|
|
383
|
-
return {
|
|
384
|
-
file,
|
|
385
|
-
uploadId,
|
|
386
|
-
previewUrl,
|
|
387
|
-
};
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
// 一次性插入多个节点,避免 NodeSelection 导致后续插入替换掉前一个节点
|
|
391
|
-
const inserted = commands.insertContent([
|
|
392
|
-
...uploads.map(({ uploadId, previewUrl }) => ({
|
|
393
|
-
type: this.name,
|
|
394
|
-
attrs: {
|
|
395
|
-
src: previewUrl,
|
|
396
|
-
uploadId,
|
|
397
|
-
},
|
|
398
|
-
})),
|
|
399
|
-
{ type: 'paragraph' },
|
|
400
|
-
]);
|
|
401
|
-
|
|
402
|
-
// 并发上传:不阻塞命令返回
|
|
403
|
-
void Promise.all(
|
|
404
|
-
uploads.map(async ({ file, uploadId, previewUrl }) => {
|
|
405
|
-
try {
|
|
406
|
-
const url = await upload(file);
|
|
407
|
-
|
|
408
|
-
// Ensure state is settled
|
|
409
|
-
await new Promise((resolve) => requestAnimationFrame(resolve));
|
|
410
|
-
|
|
411
|
-
const pos = findImageByUploadId(editor.state.doc, uploadId);
|
|
412
|
-
if (pos == null) return;
|
|
413
|
-
|
|
414
|
-
const node = editor.state.doc.nodeAt(pos);
|
|
415
|
-
if (!node || node.type.name !== this.name) return;
|
|
416
|
-
|
|
417
|
-
editor
|
|
418
|
-
.chain()
|
|
419
|
-
.command(({ tr }: CommandProps) => {
|
|
420
|
-
tr.setNodeMarkup(pos, undefined, {
|
|
421
|
-
...node.attrs,
|
|
422
|
-
src: url,
|
|
423
|
-
uploadId: null,
|
|
424
|
-
});
|
|
425
|
-
return true;
|
|
426
|
-
})
|
|
427
|
-
.run();
|
|
428
|
-
} catch {
|
|
429
|
-
const pos = findImageByUploadId(editor.state.doc, uploadId);
|
|
430
|
-
if (pos != null) {
|
|
431
|
-
editor
|
|
432
|
-
.chain()
|
|
433
|
-
.deleteRange({ from: pos, to: pos + 1 })
|
|
434
|
-
.run();
|
|
435
|
-
}
|
|
436
|
-
} finally {
|
|
437
|
-
if (previewUrl.startsWith('blob:')) {
|
|
438
|
-
try {
|
|
439
|
-
URL.revokeObjectURL(previewUrl);
|
|
440
|
-
} catch {
|
|
441
|
-
// ignore
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
}),
|
|
446
|
-
);
|
|
447
|
-
|
|
448
|
-
return inserted;
|
|
449
|
-
},
|
|
450
|
-
};
|
|
451
|
-
},
|
|
452
|
-
|
|
453
|
-
addNodeView() {
|
|
454
|
-
return ReactNodeViewRenderer(ImageNodeView);
|
|
455
|
-
},
|
|
456
|
-
});
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import * as React from 'react';
|
|
2
|
-
import { useCurrentEditor, useEditorState, type Editor } from '@tiptap/react';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Hook that provides access to a Tiptap editor instance.
|
|
6
|
-
*
|
|
7
|
-
* Accepts an optional editor instance directly, or falls back to retrieving
|
|
8
|
-
* the editor from the Tiptap context if available. This allows components
|
|
9
|
-
* to work both when given an editor directly and when used within a Tiptap
|
|
10
|
-
* editor context.
|
|
11
|
-
*
|
|
12
|
-
* @param providedEditor - Optional editor instance to use instead of the context editor
|
|
13
|
-
* @returns The provided editor or the editor from context, whichever is available
|
|
14
|
-
*/
|
|
15
|
-
export function useTiptapEditor(providedEditor?: Editor | null): {
|
|
16
|
-
editor: Editor | null;
|
|
17
|
-
editorState?: Editor['state'];
|
|
18
|
-
canCommand?: Editor['can'];
|
|
19
|
-
} {
|
|
20
|
-
const { editor: coreEditor } = useCurrentEditor();
|
|
21
|
-
|
|
22
|
-
const mainEditor = React.useMemo(
|
|
23
|
-
() => providedEditor || coreEditor,
|
|
24
|
-
[providedEditor, coreEditor],
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
const editorState = useEditorState({
|
|
28
|
-
editor: mainEditor,
|
|
29
|
-
selector(context) {
|
|
30
|
-
if (!context.editor) {
|
|
31
|
-
return {
|
|
32
|
-
editor: null,
|
|
33
|
-
editorState: undefined,
|
|
34
|
-
canCommand: undefined,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
editor: context.editor,
|
|
40
|
-
editorState: context.editor.state,
|
|
41
|
-
canCommand: context.editor.can.bind(context.editor),
|
|
42
|
-
};
|
|
43
|
-
},
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
return editorState || { editor: null };
|
|
47
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// #region 完整版组件
|
|
2
|
-
export {
|
|
3
|
-
TiptapEditorComplete,
|
|
4
|
-
type TiptapEditorCompleteProps,
|
|
5
|
-
} from '@/components/business-ui/tiptap-editor/tiptap-editor-complete';
|
|
6
|
-
// #endregion
|
|
7
|
-
|
|
8
|
-
// #region 组合式 API
|
|
9
|
-
export {
|
|
10
|
-
TiptapEditor,
|
|
11
|
-
TiptapEditorContent,
|
|
12
|
-
TiptapEditorToolbar,
|
|
13
|
-
TiptapEditorToolbarSeparator,
|
|
14
|
-
type TiptapEditorProps,
|
|
15
|
-
} from '@/components/business-ui/tiptap-editor/tiptap-editor';
|
|
16
|
-
// #endregion
|
|
17
|
-
|
|
18
|
-
// #region 扩展
|
|
19
|
-
export {
|
|
20
|
-
CompleteKit,
|
|
21
|
-
type CompleteKitOptions,
|
|
22
|
-
} from '@/components/business-ui/tiptap-editor/extensions/complete-kit';
|
|
23
|
-
// #endregion
|
|
24
|
-
|
|
25
|
-
// #region 工具栏可用工具按钮
|
|
26
|
-
export { AttachmentToolbarButton } from '@/components/business-ui/tiptap-editor/components/attachment-toolbar-button';
|
|
27
|
-
export { BlockquoteToolbarButton } from '@/components/business-ui/tiptap-editor/components/blockquote-toolbar-button';
|
|
28
|
-
export { CodeBlockToolbarButton } from '@/components/business-ui/tiptap-editor/components/code-block-toolbar-button';
|
|
29
|
-
export { ColorHighlightToolbarButton } from '@/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button';
|
|
30
|
-
export { HeadingToolbarButton } from '@/components/business-ui/tiptap-editor/components/heading-toolbar-button';
|
|
31
|
-
export { HorizontalRuleToolbarButton } from '@/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button';
|
|
32
|
-
export { ImageUploadToolbarButton } from '@/components/business-ui/tiptap-editor/components/image-upload-toolbar-button';
|
|
33
|
-
export { LinkToolbarButton } from '@/components/business-ui/tiptap-editor/components/link-toolbar-button';
|
|
34
|
-
export { ListToolbarButton } from '@/components/business-ui/tiptap-editor/components/list-toolbar-button';
|
|
35
|
-
export { MarkToolbarButton } from '@/components/business-ui/tiptap-editor/components/mark-toolbar-button';
|
|
36
|
-
export { TextAlignToolbarButton } from '@/components/business-ui/tiptap-editor/components/text-align-toolbar-button';
|
|
37
|
-
export { UndoRedoToolbarButton } from '@/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button';
|
|
38
|
-
// #endregion
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { AttachmentToolbarButton } from '@/components/business-ui/tiptap-editor/components/attachment-toolbar-button';
|
|
4
|
-
import { BlockquoteToolbarButton } from '@/components/business-ui/tiptap-editor/components/blockquote-toolbar-button';
|
|
5
|
-
import { CodeBlockToolbarButton } from '@/components/business-ui/tiptap-editor/components/code-block-toolbar-button';
|
|
6
|
-
import { ColorHighlightToolbarButton } from '@/components/business-ui/tiptap-editor/components/color-highlight-toolbar-button';
|
|
7
|
-
import { HeadingToolbarButton } from '@/components/business-ui/tiptap-editor/components/heading-toolbar-button';
|
|
8
|
-
import { HorizontalRuleToolbarButton } from '@/components/business-ui/tiptap-editor/components/horizontal-rule-toolbar-button';
|
|
9
|
-
import { ImageUploadToolbarButton } from '@/components/business-ui/tiptap-editor/components/image-upload-toolbar-button';
|
|
10
|
-
import { LinkToolbarButton } from '@/components/business-ui/tiptap-editor/components/link-toolbar-button';
|
|
11
|
-
import { ListToolbarButton } from '@/components/business-ui/tiptap-editor/components/list-toolbar-button';
|
|
12
|
-
import { MarkToolbarButton } from '@/components/business-ui/tiptap-editor/components/mark-toolbar-button';
|
|
13
|
-
import { TextAlignToolbarButton } from '@/components/business-ui/tiptap-editor/components/text-align-toolbar-button';
|
|
14
|
-
import { UndoRedoToolbarButton } from '@/components/business-ui/tiptap-editor/components/undo-redo-toolbar-button';
|
|
15
|
-
import { CompleteKit } from '@/components/business-ui/tiptap-editor/extensions/complete-kit';
|
|
16
|
-
import {
|
|
17
|
-
TiptapEditor,
|
|
18
|
-
TiptapEditorContent,
|
|
19
|
-
TiptapEditorToolbar,
|
|
20
|
-
TiptapEditorToolbarSeparator,
|
|
21
|
-
type TiptapEditorProps,
|
|
22
|
-
} from '@/components/business-ui/tiptap-editor/tiptap-editor';
|
|
23
|
-
import { cn } from '@/lib/utils';
|
|
24
|
-
|
|
25
|
-
export interface TiptapEditorCompleteProps extends Omit<
|
|
26
|
-
TiptapEditorProps,
|
|
27
|
-
'extensions'
|
|
28
|
-
> {
|
|
29
|
-
/** 占位符文本 */
|
|
30
|
-
placeholder?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 一个完整、开箱即用的富文本编辑器组件,包含预配置的工具栏。
|
|
35
|
-
* 这是大多数使用场景下的推荐组件。
|
|
36
|
-
*
|
|
37
|
-
* 功能特性:
|
|
38
|
-
* - 预配置的工具栏,包含常用的格式化选项
|
|
39
|
-
* - 已内置完整样式,包括边框、焦点态等,你无需在此编辑器外包裹 div 实现样式
|
|
40
|
-
* - 支持撤销/重做
|
|
41
|
-
* - 标题样式 (H1-H6)
|
|
42
|
-
* - 文本对齐
|
|
43
|
-
* - 列表 (无序、有序、任务)
|
|
44
|
-
* - 文本格式化 (加粗、斜体、下划线、删除线、代码)
|
|
45
|
-
*
|
|
46
|
-
* @example
|
|
47
|
-
* ```tsx
|
|
48
|
-
* <TiptapEditorComplete
|
|
49
|
-
* value={value}
|
|
50
|
-
* onValueChange={setValue}
|
|
51
|
-
* placeholder="在此输入消息..."
|
|
52
|
-
* aria-invalid={invalid}
|
|
53
|
-
* aria-disabled={disabled}
|
|
54
|
-
* />
|
|
55
|
-
* ```
|
|
56
|
-
*/
|
|
57
|
-
export function TiptapEditorComplete({
|
|
58
|
-
className,
|
|
59
|
-
placeholder,
|
|
60
|
-
...props
|
|
61
|
-
}: TiptapEditorCompleteProps) {
|
|
62
|
-
const extensions = [CompleteKit.configure({ placeholder: { placeholder } })];
|
|
63
|
-
|
|
64
|
-
return (
|
|
65
|
-
<TiptapEditor
|
|
66
|
-
className={cn('max-h-140 min-h-80', className)}
|
|
67
|
-
extensions={extensions}
|
|
68
|
-
{...props}
|
|
69
|
-
>
|
|
70
|
-
<DefaultToolbar />
|
|
71
|
-
<TiptapEditorContent />
|
|
72
|
-
</TiptapEditor>
|
|
73
|
-
);
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function DefaultToolbar() {
|
|
77
|
-
return (
|
|
78
|
-
<TiptapEditorToolbar>
|
|
79
|
-
{/* 撤销/重做 */}
|
|
80
|
-
<UndoRedoToolbarButton action="undo" />
|
|
81
|
-
<UndoRedoToolbarButton action="redo" />
|
|
82
|
-
<TiptapEditorToolbarSeparator />
|
|
83
|
-
|
|
84
|
-
{/* 标题/正文 */}
|
|
85
|
-
<HeadingToolbarButton />
|
|
86
|
-
<TiptapEditorToolbarSeparator />
|
|
87
|
-
|
|
88
|
-
{/* 文本对齐 & 列表 */}
|
|
89
|
-
<TextAlignToolbarButton />
|
|
90
|
-
<ListToolbarButton />
|
|
91
|
-
<TiptapEditorToolbarSeparator />
|
|
92
|
-
|
|
93
|
-
{/* 文本格式化: 加粗、删除线、斜体、下划线、颜色 */}
|
|
94
|
-
<MarkToolbarButton format="bold" />
|
|
95
|
-
<MarkToolbarButton format="strike" />
|
|
96
|
-
<MarkToolbarButton format="italic" />
|
|
97
|
-
<MarkToolbarButton format="underline" />
|
|
98
|
-
<ColorHighlightToolbarButton />
|
|
99
|
-
<TiptapEditorToolbarSeparator />
|
|
100
|
-
|
|
101
|
-
{/* 块级元素: 引用、分割线、代码块 */}
|
|
102
|
-
<BlockquoteToolbarButton />
|
|
103
|
-
<HorizontalRuleToolbarButton />
|
|
104
|
-
<CodeBlockToolbarButton />
|
|
105
|
-
|
|
106
|
-
{/* 链接 & 媒体: 图片、附件 */}
|
|
107
|
-
<LinkToolbarButton />
|
|
108
|
-
<ImageUploadToolbarButton />
|
|
109
|
-
<AttachmentToolbarButton />
|
|
110
|
-
</TiptapEditorToolbar>
|
|
111
|
-
);
|
|
112
|
-
}
|