@dxpos/markdown 0.0.1
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/README.md +77 -0
- package/dist/editor/ColorMenu.d.ts +18 -0
- package/dist/editor/ColorMenu.d.ts.map +1 -0
- package/dist/editor/ColorMenu.js +90 -0
- package/dist/editor/SlashMenu.d.ts +24 -0
- package/dist/editor/SlashMenu.d.ts.map +1 -0
- package/dist/editor/SlashMenu.js +179 -0
- package/dist/editor/TextStyleMenu.d.ts +45 -0
- package/dist/editor/TextStyleMenu.d.ts.map +1 -0
- package/dist/editor/TextStyleMenu.js +65 -0
- package/dist/editor/VideoEmbed.d.ts +15 -0
- package/dist/editor/VideoEmbed.d.ts.map +1 -0
- package/dist/editor/VideoEmbed.js +95 -0
- package/dist/editor/VideoMenu.d.ts +12 -0
- package/dist/editor/VideoMenu.d.ts.map +1 -0
- package/dist/editor/VideoMenu.js +35 -0
- package/dist/editor/color-menu/index.d.ts +19 -0
- package/dist/editor/color-menu/index.d.ts.map +1 -0
- package/dist/editor/color-menu/index.js +90 -0
- package/dist/editor/color-menu/text-color-presets.d.ts +9 -0
- package/dist/editor/color-menu/text-color-presets.d.ts.map +1 -0
- package/dist/editor/color-menu/text-color-presets.js +46 -0
- package/dist/editor/index.d.ts +12 -0
- package/dist/editor/index.d.ts.map +1 -0
- package/dist/editor/index.js +314 -0
- package/dist/editor/letter-spacing/index.d.ts +17 -0
- package/dist/editor/letter-spacing/index.d.ts.map +1 -0
- package/dist/editor/letter-spacing/index.js +35 -0
- package/dist/editor/letter-spacing.d.ts +17 -0
- package/dist/editor/letter-spacing.d.ts.map +1 -0
- package/dist/editor/letter-spacing.js +35 -0
- package/dist/editor/slash-menu/index.d.ts +25 -0
- package/dist/editor/slash-menu/index.d.ts.map +1 -0
- package/dist/editor/slash-menu/index.js +179 -0
- package/dist/editor/text-color-presets.d.ts +9 -0
- package/dist/editor/text-color-presets.d.ts.map +1 -0
- package/dist/editor/text-color-presets.js +46 -0
- package/dist/editor/text-style-menu/index.d.ts +46 -0
- package/dist/editor/text-style-menu/index.d.ts.map +1 -0
- package/dist/editor/text-style-menu/index.js +65 -0
- package/dist/editor/theme.d.ts +8 -0
- package/dist/editor/theme.d.ts.map +1 -0
- package/dist/editor/theme.js +18 -0
- package/dist/editor/toolbar/index.d.ts +17 -0
- package/dist/editor/toolbar/index.d.ts.map +1 -0
- package/dist/editor/toolbar/index.js +206 -0
- package/dist/editor/toolbar/items.d.ts +10 -0
- package/dist/editor/toolbar/items.d.ts.map +1 -0
- package/dist/editor/toolbar/items.js +34 -0
- package/dist/editor/toolbar/theme.d.ts +8 -0
- package/dist/editor/toolbar/theme.d.ts.map +1 -0
- package/dist/editor/toolbar/theme.js +18 -0
- package/dist/editor/toolbar-items.d.ts +10 -0
- package/dist/editor/toolbar-items.d.ts.map +1 -0
- package/dist/editor/toolbar-items.js +34 -0
- package/dist/editor/toolbar.d.ts +16 -0
- package/dist/editor/toolbar.d.ts.map +1 -0
- package/dist/editor/toolbar.js +206 -0
- package/dist/editor/use-dom-id.d.ts +3 -0
- package/dist/editor/use-dom-id.d.ts.map +1 -0
- package/dist/editor/use-dom-id.js +11 -0
- package/dist/editor/video-embed/index.d.ts +15 -0
- package/dist/editor/video-embed/index.d.ts.map +1 -0
- package/dist/editor/video-embed/index.js +95 -0
- package/dist/editor/video-embed/video-url.d.ts +8 -0
- package/dist/editor/video-embed/video-url.d.ts.map +1 -0
- package/dist/editor/video-embed/video-url.js +102 -0
- package/dist/editor/video-menu/index.d.ts +13 -0
- package/dist/editor/video-menu/index.d.ts.map +1 -0
- package/dist/editor/video-menu/index.js +35 -0
- package/dist/editor/video-url.d.ts +8 -0
- package/dist/editor/video-url.d.ts.map +1 -0
- package/dist/editor/video-url.js +102 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/plugin.d.ts +6 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +1 -0
- package/dist/plugins/gfm/index.d.ts +3 -0
- package/dist/plugins/gfm/index.d.ts.map +1 -0
- package/dist/plugins/gfm/index.js +2 -0
- package/dist/plugins/highlight/index.d.ts +3 -0
- package/dist/plugins/highlight/index.d.ts.map +1 -0
- package/dist/plugins/highlight/index.js +2 -0
- package/dist/plugins/raw/index.d.ts +3 -0
- package/dist/plugins/raw/index.d.ts.map +1 -0
- package/dist/plugins/raw/index.js +2 -0
- package/dist/view/index.d.ts +9 -0
- package/dist/view/index.d.ts.map +1 -0
- package/dist/view/index.js +18 -0
- package/package.json +61 -0
- package/styles.css +760 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Loader2, Video } from "lucide-react";
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
/** 工具栏视频菜单:上传文件或粘贴链接 */
|
|
6
|
+
export function VideoMenu({ open, uploading, error, onOpenChange, onUpload, onSubmitUrl, }) {
|
|
7
|
+
const rootRef = useRef(null);
|
|
8
|
+
const inputId = useId();
|
|
9
|
+
const [url, setUrl] = useState("");
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (!open) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const onPointerDown = (event) => {
|
|
15
|
+
if (!rootRef.current?.contains(event.target)) {
|
|
16
|
+
onOpenChange(false);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const onKeyDown = (event) => {
|
|
20
|
+
if (event.key === "Escape") {
|
|
21
|
+
onOpenChange(false);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
25
|
+
document.addEventListener("keydown", onKeyDown);
|
|
26
|
+
return () => {
|
|
27
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
28
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
29
|
+
};
|
|
30
|
+
}, [open, onOpenChange]);
|
|
31
|
+
return (_jsxs("div", { className: "video-menu", ref: rootRef, children: [_jsx("button", { className: "icon-btn icon-btn--ghost toolbar__btn", type: "button", title: uploading ? "上传中" : "视频", "aria-label": uploading ? "上传中" : "视频", "aria-expanded": open, "aria-haspopup": "dialog", disabled: uploading, onClick: () => onOpenChange(!open), children: uploading ? _jsx(Loader2, { className: "icon-btn__spin", "aria-hidden": true }) : _jsx(Video, { "aria-hidden": true }) }), open ? (_jsxs("div", { className: "video-menu__panel", role: "dialog", "aria-label": "\u89C6\u9891", children: [_jsx("button", { className: "video-menu__upload", type: "button", onClick: onUpload, children: "\u4E0A\u4F20\u89C6\u9891" }), _jsxs("form", { className: "video-menu__form", onSubmit: (event) => {
|
|
32
|
+
event.preventDefault();
|
|
33
|
+
onSubmitUrl(url);
|
|
34
|
+
}, children: [_jsx("label", { className: "video-menu__label", htmlFor: inputId, children: "\u89C6\u9891\u94FE\u63A5" }), _jsx("input", { id: inputId, className: "video-menu__input", type: "url", value: url, placeholder: "mp4\u3001webm\u3001YouTube\u3001Bilibili", onChange: (event) => setUrl(event.target.value) }), error ? _jsx("p", { className: "video-menu__error", children: error }) : null, _jsx("button", { className: "video-menu__submit", type: "submit", children: "\u63D2\u5165" })] })] })) : null] }));
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FC } from "react";
|
|
2
|
+
export declare const PRESET_HIGHLIGHT_COLORS: readonly ["#fde047", "#fdba74", "#fca5a5", "#f9a8d4", "#d8b4fe", "#93c5fd", "#67e8f9", "#86efac", "#d4d4d8"];
|
|
3
|
+
export declare const PRESET_TEXT_COLORS: readonly ["#e11d48", "#ea580c", "#ca8a04", "#16a34a", "#0891b2", "#2563eb", "#7c3aed", "#db2777", "#71717a"];
|
|
4
|
+
interface ColorMenuProps {
|
|
5
|
+
label: string;
|
|
6
|
+
active?: boolean;
|
|
7
|
+
currentColor: string | null;
|
|
8
|
+
presets: readonly string[];
|
|
9
|
+
customPresets?: readonly string[];
|
|
10
|
+
onPick: (color: string) => void;
|
|
11
|
+
onRemember?: (color: string) => void;
|
|
12
|
+
onRemoveCustom?: (color: string) => void;
|
|
13
|
+
onClear: () => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
}
|
|
16
|
+
/** 工具栏颜色菜单:预设色板 + 任意色 + 清除 */
|
|
17
|
+
export declare const ColorMenu: FC<ColorMenuProps>;
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/color-menu/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEpE,eAAO,MAAM,uBAAuB,8GAU1B,CAAC;AAEX,eAAO,MAAM,kBAAkB,8GAUrB,CAAC;AAEX,UAAU,cAAc;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;CAC3B;AAUD,8BAA8B;AAC9B,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAmJxC,CAAC"}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Eraser, X } from "lucide-react";
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
export const PRESET_HIGHLIGHT_COLORS = [
|
|
6
|
+
"#fde047",
|
|
7
|
+
"#fdba74",
|
|
8
|
+
"#fca5a5",
|
|
9
|
+
"#f9a8d4",
|
|
10
|
+
"#d8b4fe",
|
|
11
|
+
"#93c5fd",
|
|
12
|
+
"#67e8f9",
|
|
13
|
+
"#86efac",
|
|
14
|
+
"#d4d4d8",
|
|
15
|
+
];
|
|
16
|
+
export const PRESET_TEXT_COLORS = [
|
|
17
|
+
"#e11d48",
|
|
18
|
+
"#ea580c",
|
|
19
|
+
"#ca8a04",
|
|
20
|
+
"#16a34a",
|
|
21
|
+
"#0891b2",
|
|
22
|
+
"#2563eb",
|
|
23
|
+
"#7c3aed",
|
|
24
|
+
"#db2777",
|
|
25
|
+
"#71717a",
|
|
26
|
+
];
|
|
27
|
+
const sameColor = (left, right) => {
|
|
28
|
+
return left?.toLowerCase() === right.toLowerCase();
|
|
29
|
+
};
|
|
30
|
+
const colorInputValue = (color) => {
|
|
31
|
+
return color && /^#[0-9a-fA-F]{6}$/i.test(color) ? color : "#000000";
|
|
32
|
+
};
|
|
33
|
+
/** 工具栏颜色菜单:预设色板 + 任意色 + 清除 */
|
|
34
|
+
export const ColorMenu = ({ label, active, currentColor, presets, customPresets, onPick, onRemember, onRemoveCustom, onClear, children }) => {
|
|
35
|
+
const [open, setOpen] = useState(false);
|
|
36
|
+
const rootRef = useRef(null);
|
|
37
|
+
const pickedRef = useRef(null);
|
|
38
|
+
const customId = useId();
|
|
39
|
+
const rememberPicked = () => {
|
|
40
|
+
if (!pickedRef.current) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
onRemember?.(pickedRef.current);
|
|
44
|
+
pickedRef.current = null;
|
|
45
|
+
};
|
|
46
|
+
const close = () => {
|
|
47
|
+
rememberPicked();
|
|
48
|
+
setOpen(false);
|
|
49
|
+
};
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (!open) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const onPointerDown = (event) => {
|
|
55
|
+
if (!rootRef.current?.contains(event.target)) {
|
|
56
|
+
close();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const onKeyDown = (event) => {
|
|
60
|
+
if (event.key === "Escape") {
|
|
61
|
+
close();
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
65
|
+
document.addEventListener("keydown", onKeyDown);
|
|
66
|
+
return () => {
|
|
67
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
68
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
69
|
+
};
|
|
70
|
+
}, [open, onRemember]);
|
|
71
|
+
return (_jsxs("div", { className: "wsmd-color-menu", ref: rootRef, children: [_jsxs("button", { className: `wsmd-icon-btn wsmd-icon-btn--ghost wsmd-toolbar__btn wsmd-color-menu__trigger${active ? " wsmd-toolbar__btn--active" : ""}`, type: "button", title: label, "aria-label": label, "aria-expanded": open, "aria-haspopup": "dialog", "aria-pressed": active, onClick: () => {
|
|
72
|
+
if (open) {
|
|
73
|
+
close();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
setOpen(true);
|
|
77
|
+
}, children: [children, _jsx("span", { className: "wsmd-color-menu__bar", style: currentColor ? { background: currentColor } : undefined })] }), open ? (_jsxs("div", { className: "wsmd-color-menu__panel", role: "dialog", "aria-label": label, children: [_jsxs("div", { className: "wsmd-color-menu__section", children: [_jsx("p", { className: "wsmd-color-menu__caption", children: "\u9884\u8BBE" }), _jsxs("div", { className: "wsmd-color-menu__swatches", children: [presets.map((color) => (_jsx("button", { className: `wsmd-color-menu__swatch${sameColor(currentColor, color) ? " wsmd-color-menu__swatch--active" : ""}`, type: "button", title: color, "aria-label": color, style: { background: color }, onClick: () => {
|
|
78
|
+
onPick(color);
|
|
79
|
+
close();
|
|
80
|
+
} }, color))), _jsx("button", { className: "wsmd-color-menu__swatch wsmd-color-menu__swatch--clear", type: "button", title: "\u6E05\u9664", "aria-label": "\u6E05\u9664", onClick: () => {
|
|
81
|
+
onClear();
|
|
82
|
+
close();
|
|
83
|
+
}, children: _jsx(Eraser, { "aria-hidden": true }) })] })] }), customPresets && customPresets.length > 0 ? (_jsxs("div", { className: "wsmd-color-menu__section", children: [_jsx("p", { className: "wsmd-color-menu__caption", children: "\u81EA\u5B9A\u4E49" }), _jsx("div", { className: "wsmd-color-menu__swatches", children: customPresets.map((color) => (_jsxs("span", { className: "wsmd-color-menu__custom-swatch", children: [_jsx("button", { className: `wsmd-color-menu__swatch${sameColor(currentColor, color) ? " wsmd-color-menu__swatch--active" : ""}`, type: "button", title: color, "aria-label": color, style: { background: color }, onClick: () => {
|
|
84
|
+
onPick(color);
|
|
85
|
+
close();
|
|
86
|
+
} }), _jsx("button", { className: "wsmd-color-menu__swatch-remove", type: "button", title: "\u5220\u9664", "aria-label": `删除 ${color}`, onClick: () => onRemoveCustom?.(color), children: _jsx(X, { "aria-hidden": true }) })] }, color))) })] })) : null, _jsxs("label", { className: "wsmd-color-menu__custom", htmlFor: customId, children: [_jsx("span", { className: "wsmd-color-menu__caption", children: "\u4EFB\u610F" }), _jsx("input", { id: customId, className: "wsmd-color-menu__picker", type: "color", value: colorInputValue(currentColor), onChange: (event) => {
|
|
87
|
+
pickedRef.current = event.target.value;
|
|
88
|
+
onPick(event.target.value);
|
|
89
|
+
}, onBlur: rememberPicked })] })] })) : null] }));
|
|
90
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const TEXT_COLOR_PRESET_KEY = "kb-text-color-presets";
|
|
2
|
+
export declare const TEXT_COLOR_PRESET_LIMIT = 16;
|
|
3
|
+
export declare const normalizeTextColor: (color: string) => string | null;
|
|
4
|
+
/** 记住自定义色:去重后追加,超过上限丢掉最早的;内置色不写入 */
|
|
5
|
+
export declare const rememberCustomTextColor: (current: readonly string[], color: string, builtin: readonly string[]) => string[];
|
|
6
|
+
export declare const removeCustomTextColor: (current: readonly string[], color: string) => string[];
|
|
7
|
+
export declare const readCustomTextColors: () => string[];
|
|
8
|
+
export declare const writeCustomTextColors: (colors: readonly string[]) => void;
|
|
9
|
+
//# sourceMappingURL=text-color-presets.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-color-presets.d.ts","sourceRoot":"","sources":["../../../src/editor/color-menu/text-color-presets.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAC7D,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAI1C,eAAO,MAAM,kBAAkB,GAAI,OAAO,MAAM,kBAG/C,CAAC;AAEF,oCAAoC;AACpC,eAAO,MAAM,uBAAuB,GAAI,SAAS,SAAS,MAAM,EAAE,EAAE,OAAO,MAAM,EAAE,SAAS,SAAS,MAAM,EAAE,aAc5G,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,SAAS,SAAS,MAAM,EAAE,EAAE,OAAO,MAAM,aAM9E,CAAC;AAEF,eAAO,MAAM,oBAAoB,gBAYhC,CAAC;AAEF,eAAO,MAAM,qBAAqB,GAAI,QAAQ,SAAS,MAAM,EAAE,SAE9D,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const TEXT_COLOR_PRESET_KEY = "kb-text-color-presets";
|
|
2
|
+
export const TEXT_COLOR_PRESET_LIMIT = 16;
|
|
3
|
+
const HEX = /^#[0-9a-f]{6}$/;
|
|
4
|
+
export const normalizeTextColor = (color) => {
|
|
5
|
+
const value = color.trim().toLowerCase();
|
|
6
|
+
return HEX.test(value) ? value : null;
|
|
7
|
+
};
|
|
8
|
+
/** 记住自定义色:去重后追加,超过上限丢掉最早的;内置色不写入 */
|
|
9
|
+
export const rememberCustomTextColor = (current, color, builtin) => {
|
|
10
|
+
const normalized = normalizeTextColor(color);
|
|
11
|
+
if (!normalized) {
|
|
12
|
+
return [...current];
|
|
13
|
+
}
|
|
14
|
+
if (builtin.some((item) => item.toLowerCase() === normalized)) {
|
|
15
|
+
return [...current];
|
|
16
|
+
}
|
|
17
|
+
const next = current.filter((item) => item !== normalized);
|
|
18
|
+
next.push(normalized);
|
|
19
|
+
if (next.length <= TEXT_COLOR_PRESET_LIMIT) {
|
|
20
|
+
return next;
|
|
21
|
+
}
|
|
22
|
+
return next.slice(next.length - TEXT_COLOR_PRESET_LIMIT);
|
|
23
|
+
};
|
|
24
|
+
export const removeCustomTextColor = (current, color) => {
|
|
25
|
+
const normalized = normalizeTextColor(color);
|
|
26
|
+
if (!normalized) {
|
|
27
|
+
return [...current];
|
|
28
|
+
}
|
|
29
|
+
return current.filter((item) => item !== normalized);
|
|
30
|
+
};
|
|
31
|
+
export const readCustomTextColors = () => {
|
|
32
|
+
const raw = localStorage.getItem(TEXT_COLOR_PRESET_KEY);
|
|
33
|
+
if (!raw) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
const parsed = JSON.parse(raw);
|
|
37
|
+
if (!Array.isArray(parsed)) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
return parsed
|
|
41
|
+
.filter((item) => typeof item === "string" && HEX.test(item))
|
|
42
|
+
.slice(-TEXT_COLOR_PRESET_LIMIT);
|
|
43
|
+
};
|
|
44
|
+
export const writeCustomTextColors = (colors) => {
|
|
45
|
+
localStorage.setItem(TEXT_COLOR_PRESET_KEY, JSON.stringify(colors));
|
|
46
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type FC } from "react";
|
|
2
|
+
import { type ToolbarItem, type UploadFile } from "./toolbar/items";
|
|
3
|
+
interface EditorProps {
|
|
4
|
+
content: string;
|
|
5
|
+
onChange: (markdown: string) => void;
|
|
6
|
+
onError: (message: string) => void;
|
|
7
|
+
uploadFile: UploadFile;
|
|
8
|
+
toolbar?: readonly ToolbarItem[];
|
|
9
|
+
}
|
|
10
|
+
export declare const Editor: FC<EditorProps>;
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/editor/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,EAA4D,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAM1F,OAAO,EAAmB,KAAK,WAAW,EAAE,KAAK,UAAU,EAAuB,MAAM,iBAAiB,CAAC;AAI1G,UAAU,WAAW;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,UAAU,EAAE,UAAU,CAAC;IACvB,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;CAClC;AA4ID,eAAO,MAAM,MAAM,EAAE,EAAE,CAAC,WAAW,CAyPlC,CAAC"}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import Color from "@tiptap/extension-text-style/color";
|
|
4
|
+
import { FontFamily } from "@tiptap/extension-text-style/font-family";
|
|
5
|
+
import { FontSize } from "@tiptap/extension-text-style/font-size";
|
|
6
|
+
import Highlight from "@tiptap/extension-highlight";
|
|
7
|
+
import Image from "@tiptap/extension-image";
|
|
8
|
+
import Link from "@tiptap/extension-link";
|
|
9
|
+
import Placeholder from "@tiptap/extension-placeholder";
|
|
10
|
+
import { TableKit } from "@tiptap/extension-table";
|
|
11
|
+
import TaskItem from "@tiptap/extension-task-item";
|
|
12
|
+
import TaskList from "@tiptap/extension-task-list";
|
|
13
|
+
import TextAlign from "@tiptap/extension-text-align";
|
|
14
|
+
import { LineHeight } from "@tiptap/extension-text-style/line-height";
|
|
15
|
+
import { TextStyle } from "@tiptap/extension-text-style";
|
|
16
|
+
import { EditorContent, useEditor } from "@tiptap/react";
|
|
17
|
+
import StarterKit from "@tiptap/starter-kit";
|
|
18
|
+
import { NodeSelection } from "@tiptap/pm/state";
|
|
19
|
+
import { GripVertical } from "lucide-react";
|
|
20
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
21
|
+
import { Markdown } from "tiptap-markdown";
|
|
22
|
+
import { LetterSpacing } from "./letter-spacing";
|
|
23
|
+
import { createSlashCommand } from "./slash-menu";
|
|
24
|
+
import { Toolbar } from "./toolbar";
|
|
25
|
+
import { DEFAULT_TOOLBAR } from "./toolbar/items";
|
|
26
|
+
import { VideoEmbed } from "./video-embed";
|
|
27
|
+
import { parseVideoUrl } from "./video-embed/video-url";
|
|
28
|
+
const mediaFilesFrom = (data) => {
|
|
29
|
+
const files = data ? Array.from(data.files) : [];
|
|
30
|
+
return {
|
|
31
|
+
images: files.filter((file) => file.type.startsWith("image/")),
|
|
32
|
+
videos: files.filter((file) => file.type === "video/mp4" || file.type === "video/webm"),
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
const hasFilePayload = (data) => {
|
|
36
|
+
return Boolean(data?.types.includes("Files"));
|
|
37
|
+
};
|
|
38
|
+
/** 行首图片后紧跟内容或单换行时补成段落分隔,避免序列化后粘连 */
|
|
39
|
+
const normalizeStandaloneImages = (markdown) => {
|
|
40
|
+
let current = markdown.replace(/\r\n/g, "\n");
|
|
41
|
+
let next = current.replace(/^(\s*)(!\[[^\]]*\]\([^)]*\))[ \t]*(?:\n(?!\n)|(?=\S))/gm, "$1$2\n\n");
|
|
42
|
+
while (next !== current) {
|
|
43
|
+
current = next;
|
|
44
|
+
next = current.replace(/^(\s*)(!\[[^\]]*\]\([^)]*\))[ \t]*(?:\n(?!\n)|(?=\S))/gm, "$1$2\n\n");
|
|
45
|
+
}
|
|
46
|
+
return next;
|
|
47
|
+
};
|
|
48
|
+
const uploadError = (type) => {
|
|
49
|
+
if (type === "video") {
|
|
50
|
+
return "上传视频失败";
|
|
51
|
+
}
|
|
52
|
+
if (type === "image") {
|
|
53
|
+
return "上传图片失败";
|
|
54
|
+
}
|
|
55
|
+
return "上传失败";
|
|
56
|
+
};
|
|
57
|
+
const BlockDragHandle = ({ editorDom, getEditor }) => {
|
|
58
|
+
const handleRef = useRef(null);
|
|
59
|
+
const nodePosRef = useRef(null);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
const handle = handleRef.current;
|
|
62
|
+
if (!handle) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const hide = () => {
|
|
66
|
+
handle.style.opacity = "0";
|
|
67
|
+
handle.style.pointerEvents = "none";
|
|
68
|
+
nodePosRef.current = null;
|
|
69
|
+
};
|
|
70
|
+
const onMove = (event) => {
|
|
71
|
+
const editor = getEditor();
|
|
72
|
+
if (!editor) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (handle.contains(event.target)) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const pos = editor.view.posAtCoords({ left: event.clientX, top: event.clientY });
|
|
79
|
+
if (!pos) {
|
|
80
|
+
hide();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const $pos = editor.state.doc.resolve(pos.inside >= 0 ? pos.inside : pos.pos);
|
|
84
|
+
if ($pos.depth < 1) {
|
|
85
|
+
hide();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
const nodePos = $pos.before(1);
|
|
89
|
+
const coords = editor.view.coordsAtPos(nodePos);
|
|
90
|
+
const editorRect = editor.view.dom.getBoundingClientRect();
|
|
91
|
+
handle.style.top = `${coords.top - editorRect.top + editor.view.dom.scrollTop}px`;
|
|
92
|
+
handle.style.opacity = "1";
|
|
93
|
+
handle.style.pointerEvents = "auto";
|
|
94
|
+
nodePosRef.current = nodePos;
|
|
95
|
+
};
|
|
96
|
+
const onLeave = (event) => {
|
|
97
|
+
if (handle.contains(event.relatedTarget)) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
hide();
|
|
101
|
+
};
|
|
102
|
+
editorDom.addEventListener("mousemove", onMove);
|
|
103
|
+
editorDom.addEventListener("mouseleave", onLeave);
|
|
104
|
+
return () => {
|
|
105
|
+
editorDom.removeEventListener("mousemove", onMove);
|
|
106
|
+
editorDom.removeEventListener("mouseleave", onLeave);
|
|
107
|
+
};
|
|
108
|
+
}, [editorDom, getEditor]);
|
|
109
|
+
return (_jsx("button", { ref: handleRef, className: "wsmd-drag-handle", type: "button", draggable: true, "aria-label": "\u62D6\u62FD\u5757", title: "\u62D6\u62FD\u5757", onDragStart: (event) => {
|
|
110
|
+
const editor = getEditor();
|
|
111
|
+
const nodePos = nodePosRef.current;
|
|
112
|
+
if (!editor || nodePos === null) {
|
|
113
|
+
event.preventDefault();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const selection = NodeSelection.create(editor.state.doc, nodePos);
|
|
117
|
+
editor.view.dispatch(editor.state.tr.setSelection(selection));
|
|
118
|
+
editor.view.dragging = { slice: selection.content(), move: true };
|
|
119
|
+
event.dataTransfer.setData("text/plain", selection.content().content.textBetween(0, selection.content().size));
|
|
120
|
+
event.dataTransfer.effectAllowed = "move";
|
|
121
|
+
}, onDragEnd: () => {
|
|
122
|
+
const editor = getEditor();
|
|
123
|
+
if (editor) {
|
|
124
|
+
editor.view.dragging = null;
|
|
125
|
+
}
|
|
126
|
+
}, children: _jsx(GripVertical, { "aria-hidden": true }) }));
|
|
127
|
+
};
|
|
128
|
+
export const Editor = ({ content, onChange, onError, uploadFile, toolbar = DEFAULT_TOOLBAR }) => {
|
|
129
|
+
const fileInputRef = useRef(null);
|
|
130
|
+
const videoInputRef = useRef(null);
|
|
131
|
+
const editorRef = useRef(null);
|
|
132
|
+
const uploadFileRef = useRef(uploadFile);
|
|
133
|
+
uploadFileRef.current = uploadFile;
|
|
134
|
+
const [uploading, setUploading] = useState(false);
|
|
135
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
136
|
+
const [editorDom, setEditorDom] = useState(null);
|
|
137
|
+
const [videoOpen, setVideoOpen] = useState(false);
|
|
138
|
+
const [videoError, setVideoError] = useState(null);
|
|
139
|
+
const insertMediaFiles = useCallback(async (files, type, insert) => {
|
|
140
|
+
if (files.length === 0) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
setUploading(true);
|
|
144
|
+
try {
|
|
145
|
+
for (const file of files) {
|
|
146
|
+
try {
|
|
147
|
+
const uploaded = await uploadFileRef.current(file, type);
|
|
148
|
+
if (!uploaded.url) {
|
|
149
|
+
throw new Error(uploadError(type));
|
|
150
|
+
}
|
|
151
|
+
const current = editorRef.current;
|
|
152
|
+
if (!current) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
insert(current, uploaded.url, file);
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
onError(error instanceof Error ? error.message : uploadError(type));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
setUploading(false);
|
|
164
|
+
}
|
|
165
|
+
}, [onError]);
|
|
166
|
+
const insertImageFiles = useCallback((files) => insertMediaFiles(files, "image", (editor, url, file) => {
|
|
167
|
+
editor.chain().focus().setImage({ src: url, alt: file.name }).run();
|
|
168
|
+
}), [insertMediaFiles]);
|
|
169
|
+
const insertVideoFiles = useCallback((files) => insertMediaFiles(files, "video", (editor, url) => {
|
|
170
|
+
editor.chain().focus().setVideoEmbed({ src: url, provider: "file" }).run();
|
|
171
|
+
}), [insertMediaFiles]);
|
|
172
|
+
const openImagePicker = useCallback(() => {
|
|
173
|
+
fileInputRef.current?.click();
|
|
174
|
+
}, []);
|
|
175
|
+
const openVideoMenu = useCallback(() => {
|
|
176
|
+
setVideoError(null);
|
|
177
|
+
setVideoOpen(true);
|
|
178
|
+
}, []);
|
|
179
|
+
const openVideoPicker = useCallback(() => {
|
|
180
|
+
videoInputRef.current?.click();
|
|
181
|
+
}, []);
|
|
182
|
+
const editor = useEditor({
|
|
183
|
+
immediatelyRender: false,
|
|
184
|
+
extensions: [
|
|
185
|
+
StarterKit.configure({
|
|
186
|
+
link: false,
|
|
187
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
188
|
+
}),
|
|
189
|
+
Placeholder.configure({
|
|
190
|
+
placeholder: "输入 / 唤起命令",
|
|
191
|
+
}),
|
|
192
|
+
Image,
|
|
193
|
+
VideoEmbed,
|
|
194
|
+
Link.configure({ openOnClick: false }),
|
|
195
|
+
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
|
196
|
+
TextStyle,
|
|
197
|
+
Color,
|
|
198
|
+
FontFamily,
|
|
199
|
+
FontSize,
|
|
200
|
+
LineHeight,
|
|
201
|
+
LetterSpacing,
|
|
202
|
+
Highlight.configure({ multicolor: true }),
|
|
203
|
+
TableKit.configure({ table: { resizable: false } }),
|
|
204
|
+
TaskList,
|
|
205
|
+
TaskItem.configure({ nested: true }),
|
|
206
|
+
Markdown,
|
|
207
|
+
createSlashCommand(openImagePicker, openVideoMenu),
|
|
208
|
+
],
|
|
209
|
+
content: normalizeStandaloneImages(content),
|
|
210
|
+
editorProps: {
|
|
211
|
+
attributes: { class: "wsmd-tiptap" },
|
|
212
|
+
handleDrop: (_view, event) => {
|
|
213
|
+
const { images, videos } = mediaFilesFrom(event.dataTransfer);
|
|
214
|
+
if (images.length === 0 && videos.length === 0) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
event.preventDefault();
|
|
218
|
+
void (async () => {
|
|
219
|
+
await insertImageFiles(images);
|
|
220
|
+
await insertVideoFiles(videos);
|
|
221
|
+
})();
|
|
222
|
+
return true;
|
|
223
|
+
},
|
|
224
|
+
handlePaste: (_view, event) => {
|
|
225
|
+
const { images, videos } = mediaFilesFrom(event.clipboardData);
|
|
226
|
+
if (images.length === 0 && videos.length === 0) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
event.preventDefault();
|
|
230
|
+
void (async () => {
|
|
231
|
+
await insertImageFiles(images);
|
|
232
|
+
await insertVideoFiles(videos);
|
|
233
|
+
})();
|
|
234
|
+
return true;
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
onCreate: ({ editor: next }) => {
|
|
238
|
+
onChange(normalizeStandaloneImages(next.storage.markdown.getMarkdown()));
|
|
239
|
+
},
|
|
240
|
+
onUpdate: ({ editor: next }) => {
|
|
241
|
+
onChange(normalizeStandaloneImages(next.storage.markdown.getMarkdown()));
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
editorRef.current = editor;
|
|
245
|
+
useEffect(() => {
|
|
246
|
+
if (editor) {
|
|
247
|
+
setEditorDom(editor.view.dom);
|
|
248
|
+
}
|
|
249
|
+
}, [editor]);
|
|
250
|
+
const getEditor = useCallback(() => editorRef.current, []);
|
|
251
|
+
const onEditorDragEnter = (event) => {
|
|
252
|
+
if (!hasFilePayload(event.dataTransfer)) {
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
event.preventDefault();
|
|
256
|
+
setIsDragging(true);
|
|
257
|
+
};
|
|
258
|
+
const onEditorDragOver = (event) => {
|
|
259
|
+
if (!hasFilePayload(event.dataTransfer)) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
event.preventDefault();
|
|
263
|
+
event.dataTransfer.dropEffect = "copy";
|
|
264
|
+
setIsDragging(true);
|
|
265
|
+
};
|
|
266
|
+
const onEditorDragLeave = (event) => {
|
|
267
|
+
if (event.currentTarget.contains(event.relatedTarget)) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
setIsDragging(false);
|
|
271
|
+
};
|
|
272
|
+
const onEditorDrop = (event) => {
|
|
273
|
+
const { images, videos } = mediaFilesFrom(event.dataTransfer);
|
|
274
|
+
setIsDragging(false);
|
|
275
|
+
if (images.length === 0 && videos.length === 0) {
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
event.preventDefault();
|
|
279
|
+
const current = editorRef.current;
|
|
280
|
+
if (current && current.view.dom.contains(event.target)) {
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
void (async () => {
|
|
284
|
+
await insertImageFiles(images);
|
|
285
|
+
await insertVideoFiles(videos);
|
|
286
|
+
})();
|
|
287
|
+
};
|
|
288
|
+
if (!editor) {
|
|
289
|
+
return _jsx("p", { className: "wsmd-markdown-editor__loading", children: "\u7F16\u8F91\u5668\u52A0\u8F7D\u4E2D\u2026" });
|
|
290
|
+
}
|
|
291
|
+
return (_jsxs("div", { className: `wsmd-markdown-editor${isDragging ? " wsmd-markdown-editor--dragging" : ""}`, onDragEnter: onEditorDragEnter, onDragOver: onEditorDragOver, onDragLeave: onEditorDragLeave, onDrop: onEditorDrop, children: [_jsx(Toolbar, { editor: editor, items: toolbar, uploading: uploading, videoOpen: videoOpen, videoError: videoError, onInsertImage: openImagePicker, onVideoOpenChange: setVideoOpen, onUploadVideo: openVideoPicker, onInsertVideoUrl: (url) => {
|
|
292
|
+
const parsed = parseVideoUrl(url);
|
|
293
|
+
if (!parsed) {
|
|
294
|
+
setVideoError("无法识别的视频地址");
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
editor.chain().focus().setVideoEmbed(parsed).run();
|
|
298
|
+
setVideoError(null);
|
|
299
|
+
setVideoOpen(false);
|
|
300
|
+
} }), _jsxs("div", { className: "wsmd-markdown-editor__canvas", children: [editorDom ? _jsx(BlockDragHandle, { editorDom: editorDom, getEditor: getEditor }) : null, _jsx(EditorContent, { editor: editor, className: "wsmd-markdown-editor__content wsmd-markdown-body" })] }), _jsx("input", { ref: fileInputRef, className: "wsmd-markdown-editor__file", type: "file", accept: "image/*", onChange: (event) => {
|
|
301
|
+
const file = event.target.files?.[0];
|
|
302
|
+
event.target.value = "";
|
|
303
|
+
if (file) {
|
|
304
|
+
void insertImageFiles([file]);
|
|
305
|
+
}
|
|
306
|
+
} }), _jsx("input", { ref: videoInputRef, className: "wsmd-markdown-editor__file", type: "file", accept: "video/mp4,video/webm", onChange: (event) => {
|
|
307
|
+
const file = event.target.files?.[0];
|
|
308
|
+
event.target.value = "";
|
|
309
|
+
if (file) {
|
|
310
|
+
setVideoOpen(false);
|
|
311
|
+
void insertVideoFiles([file]);
|
|
312
|
+
}
|
|
313
|
+
} })] }));
|
|
314
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
declare module "@tiptap/core" {
|
|
3
|
+
interface Commands<ReturnType> {
|
|
4
|
+
letterSpacing: {
|
|
5
|
+
setLetterSpacing: (letterSpacing: string) => ReturnType;
|
|
6
|
+
unsetLetterSpacing: () => ReturnType;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
declare module "@tiptap/extension-text-style" {
|
|
11
|
+
interface TextStyleAttributes {
|
|
12
|
+
letterSpacing?: string | null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** 字间距,挂在 textStyle 上以便随 span style 写回 Markdown */
|
|
16
|
+
export declare const LetterSpacing: Extension<any, any>;
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/letter-spacing/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,cAAc,CAAC;AAE3D,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,aAAa,EAAE;YACb,gBAAgB,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,UAAU,CAAC;YACxD,kBAAkB,EAAE,MAAM,UAAU,CAAC;SACtC,CAAC;KACH;CACF;AAED,OAAO,QAAQ,8BAA8B,CAAC;IAC5C,UAAU,mBAAmB;QAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B;CACF;AAED,mDAAmD;AACnD,eAAO,MAAM,aAAa,qBA0CxB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Extension, getStyleProperty } from "@tiptap/core";
|
|
2
|
+
/** 字间距,挂在 textStyle 上以便随 span style 写回 Markdown */
|
|
3
|
+
export const LetterSpacing = Extension.create({
|
|
4
|
+
name: "letterSpacing",
|
|
5
|
+
addOptions() {
|
|
6
|
+
return {
|
|
7
|
+
types: ["textStyle"],
|
|
8
|
+
};
|
|
9
|
+
},
|
|
10
|
+
addGlobalAttributes() {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
types: this.options.types,
|
|
14
|
+
attributes: {
|
|
15
|
+
letterSpacing: {
|
|
16
|
+
default: null,
|
|
17
|
+
parseHTML: (element) => getStyleProperty(element, "letter-spacing") ?? element.style.letterSpacing,
|
|
18
|
+
renderHTML: (attributes) => {
|
|
19
|
+
if (!attributes.letterSpacing) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
return { style: `letter-spacing: ${attributes.letterSpacing}` };
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
},
|
|
29
|
+
addCommands() {
|
|
30
|
+
return {
|
|
31
|
+
setLetterSpacing: (letterSpacing) => ({ chain }) => chain().setMark("textStyle", { letterSpacing }).run(),
|
|
32
|
+
unsetLetterSpacing: () => ({ chain }) => chain().setMark("textStyle", { letterSpacing: null }).removeEmptyTextStyle().run(),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
declare module "@tiptap/core" {
|
|
3
|
+
interface Commands<ReturnType> {
|
|
4
|
+
letterSpacing: {
|
|
5
|
+
setLetterSpacing: (letterSpacing: string) => ReturnType;
|
|
6
|
+
unsetLetterSpacing: () => ReturnType;
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
declare module "@tiptap/extension-text-style" {
|
|
11
|
+
interface TextStyleAttributes {
|
|
12
|
+
letterSpacing?: string | null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** 字间距,挂在 textStyle 上以便随 span style 写回 Markdown */
|
|
16
|
+
export declare const LetterSpacing: Extension<any, any>;
|
|
17
|
+
//# sourceMappingURL=letter-spacing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"letter-spacing.d.ts","sourceRoot":"","sources":["../../src/editor/letter-spacing.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAoB,MAAM,cAAc,CAAC;AAE3D,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,aAAa,EAAE;YACb,gBAAgB,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,UAAU,CAAC;YACxD,kBAAkB,EAAE,MAAM,UAAU,CAAC;SACtC,CAAC;KACH;CACF;AAED,OAAO,QAAQ,8BAA8B,CAAC;IAC5C,UAAU,mBAAmB;QAC3B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B;CACF;AAED,mDAAmD;AACnD,eAAO,MAAM,aAAa,qBA0CxB,CAAC"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Extension, getStyleProperty } from "@tiptap/core";
|
|
2
|
+
/** 字间距,挂在 textStyle 上以便随 span style 写回 Markdown */
|
|
3
|
+
export const LetterSpacing = Extension.create({
|
|
4
|
+
name: "letterSpacing",
|
|
5
|
+
addOptions() {
|
|
6
|
+
return {
|
|
7
|
+
types: ["textStyle"],
|
|
8
|
+
};
|
|
9
|
+
},
|
|
10
|
+
addGlobalAttributes() {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
types: this.options.types,
|
|
14
|
+
attributes: {
|
|
15
|
+
letterSpacing: {
|
|
16
|
+
default: null,
|
|
17
|
+
parseHTML: (element) => getStyleProperty(element, "letter-spacing") ?? element.style.letterSpacing,
|
|
18
|
+
renderHTML: (attributes) => {
|
|
19
|
+
if (!attributes.letterSpacing) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
return { style: `letter-spacing: ${attributes.letterSpacing}` };
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
},
|
|
29
|
+
addCommands() {
|
|
30
|
+
return {
|
|
31
|
+
setLetterSpacing: (letterSpacing) => ({ chain }) => chain().setMark("textStyle", { letterSpacing }).run(),
|
|
32
|
+
unsetLetterSpacing: () => ({ chain }) => chain().setMark("textStyle", { letterSpacing: null }).removeEmptyTextStyle().run(),
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
});
|