@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,25 @@
|
|
|
1
|
+
import { Extension, type Editor, type Range } from "@tiptap/react";
|
|
2
|
+
import { type SuggestionKeyDownProps, type SuggestionProps } from "@tiptap/suggestion";
|
|
3
|
+
import { Heading1 } from "lucide-react";
|
|
4
|
+
export type SlashCommandContext = {
|
|
5
|
+
editor: Editor;
|
|
6
|
+
range: Range;
|
|
7
|
+
onInsertImage: () => void;
|
|
8
|
+
onInsertVideo: () => void;
|
|
9
|
+
};
|
|
10
|
+
export type SlashItem = {
|
|
11
|
+
title: string;
|
|
12
|
+
icon: typeof Heading1;
|
|
13
|
+
command: (ctx: SlashCommandContext) => void;
|
|
14
|
+
};
|
|
15
|
+
export declare const SLASH_ITEMS: SlashItem[];
|
|
16
|
+
interface SlashMenuProps extends SuggestionProps<SlashItem, SlashItem> {
|
|
17
|
+
}
|
|
18
|
+
export interface SlashMenuHandle {
|
|
19
|
+
onKeyDown: (props: SuggestionKeyDownProps) => boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const SlashMenu: import("react").ForwardRefExoticComponent<SlashMenuProps & import("react").RefAttributes<SlashMenuHandle>>;
|
|
22
|
+
/** 输入 / 唤起块类型菜单 */
|
|
23
|
+
export declare const createSlashCommand: (onInsertImage: () => void, onInsertVideo: () => void) => Extension<any, any>;
|
|
24
|
+
export {};
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/slash-menu/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAiB,KAAK,MAAM,EAAE,KAAK,KAAK,EAAE,MAAM,eAAe,CAAC;AAClF,OAAmB,EAAE,KAAK,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACnG,OAAO,EAEL,QAAQ,EAcT,MAAM,cAAc,CAAC;AAGtB,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,KAAK,CAAC;IACb,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,QAAQ,CAAC;IACtB,OAAO,EAAE,CAAC,GAAG,EAAE,mBAAmB,KAAK,IAAI,CAAC;CAC7C,CAAC;AAqBF,eAAO,MAAM,WAAW,EAAE,SAAS,EAwElC,CAAC;AAEF,UAAU,cAAe,SAAQ,eAAe,CAAC,SAAS,EAAE,SAAS,CAAC;CAAG;AAEzE,MAAM,WAAW,eAAe;IAC9B,SAAS,EAAE,CAAC,KAAK,EAAE,sBAAsB,KAAK,OAAO,CAAC;CACvD;AAED,eAAO,MAAM,SAAS,4GAwDpB,CAAC;AAEH,mBAAmB;AACnB,eAAO,MAAM,kBAAkB,GAAI,eAAe,MAAM,IAAI,EAAE,eAAe,MAAM,IAAI,wBA+CtF,CAAC"}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Extension, ReactRenderer } from "@tiptap/react";
|
|
4
|
+
import Suggestion from "@tiptap/suggestion";
|
|
5
|
+
import { Code, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, ImageIcon, List, ListOrdered, ListTodo, Minus, Quote, Table, Video, } from "lucide-react";
|
|
6
|
+
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
|
|
7
|
+
const HEADING_ICONS = {
|
|
8
|
+
1: Heading1,
|
|
9
|
+
2: Heading2,
|
|
10
|
+
3: Heading3,
|
|
11
|
+
4: Heading4,
|
|
12
|
+
5: Heading5,
|
|
13
|
+
6: Heading6,
|
|
14
|
+
};
|
|
15
|
+
const headingItem = (level) => {
|
|
16
|
+
return {
|
|
17
|
+
title: `标题 ${level}`,
|
|
18
|
+
icon: HEADING_ICONS[level],
|
|
19
|
+
command: ({ editor, range }) => {
|
|
20
|
+
editor.chain().focus().deleteRange(range).setHeading({ level }).run();
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
export const SLASH_ITEMS = [
|
|
25
|
+
headingItem(1),
|
|
26
|
+
headingItem(2),
|
|
27
|
+
headingItem(3),
|
|
28
|
+
headingItem(4),
|
|
29
|
+
headingItem(5),
|
|
30
|
+
headingItem(6),
|
|
31
|
+
{
|
|
32
|
+
title: "无序列表",
|
|
33
|
+
icon: List,
|
|
34
|
+
command: ({ editor, range }) => {
|
|
35
|
+
editor.chain().focus().deleteRange(range).toggleBulletList().run();
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
title: "有序列表",
|
|
40
|
+
icon: ListOrdered,
|
|
41
|
+
command: ({ editor, range }) => {
|
|
42
|
+
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
title: "待办",
|
|
47
|
+
icon: ListTodo,
|
|
48
|
+
command: ({ editor, range }) => {
|
|
49
|
+
editor.chain().focus().deleteRange(range).toggleTaskList().run();
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
title: "代码块",
|
|
54
|
+
icon: Code,
|
|
55
|
+
command: ({ editor, range }) => {
|
|
56
|
+
editor.chain().focus().deleteRange(range).toggleCodeBlock().run();
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: "引用",
|
|
61
|
+
icon: Quote,
|
|
62
|
+
command: ({ editor, range }) => {
|
|
63
|
+
editor.chain().focus().deleteRange(range).toggleBlockquote().run();
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
title: "表格",
|
|
68
|
+
icon: Table,
|
|
69
|
+
command: ({ editor, range }) => {
|
|
70
|
+
editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
title: "分割线",
|
|
75
|
+
icon: Minus,
|
|
76
|
+
command: ({ editor, range }) => {
|
|
77
|
+
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
title: "图片",
|
|
82
|
+
icon: ImageIcon,
|
|
83
|
+
command: ({ editor, range, onInsertImage }) => {
|
|
84
|
+
editor.chain().focus().deleteRange(range).run();
|
|
85
|
+
onInsertImage();
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
title: "视频",
|
|
90
|
+
icon: Video,
|
|
91
|
+
command: ({ editor, range, onInsertVideo }) => {
|
|
92
|
+
editor.chain().focus().deleteRange(range).run();
|
|
93
|
+
onInsertVideo();
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
export const SlashMenu = forwardRef(({ items, command }, ref) => {
|
|
98
|
+
const [selected, setSelected] = useState(0);
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
setSelected(0);
|
|
101
|
+
}, [items]);
|
|
102
|
+
useImperativeHandle(ref, () => ({
|
|
103
|
+
onKeyDown: ({ event }) => {
|
|
104
|
+
if (items.length === 0) {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
if (event.key === "ArrowUp") {
|
|
108
|
+
setSelected((index) => (index + items.length - 1) % items.length);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
if (event.key === "ArrowDown") {
|
|
112
|
+
setSelected((index) => (index + 1) % items.length);
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (event.key === "Enter") {
|
|
116
|
+
const item = items[selected];
|
|
117
|
+
if (item) {
|
|
118
|
+
command(item);
|
|
119
|
+
}
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
},
|
|
124
|
+
}));
|
|
125
|
+
if (items.length === 0) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
return (_jsx("div", { className: "wsmd-slash-menu", role: "listbox", children: items.map((item, index) => {
|
|
129
|
+
const Icon = item.icon;
|
|
130
|
+
return (_jsxs("button", { className: `wsmd-slash-menu__item${index === selected ? " wsmd-slash-menu__item--active" : ""}`, type: "button", role: "option", "aria-selected": index === selected, onMouseEnter: () => setSelected(index), onClick: () => command(item), children: [_jsx(Icon, { "aria-hidden": true }), _jsx("span", { children: item.title })] }, item.title));
|
|
131
|
+
}) }));
|
|
132
|
+
});
|
|
133
|
+
/** 输入 / 唤起块类型菜单 */
|
|
134
|
+
export const createSlashCommand = (onInsertImage, onInsertVideo) => {
|
|
135
|
+
return Extension.create({
|
|
136
|
+
name: "slashCommand",
|
|
137
|
+
addProseMirrorPlugins() {
|
|
138
|
+
return [
|
|
139
|
+
Suggestion({
|
|
140
|
+
editor: this.editor,
|
|
141
|
+
char: "/",
|
|
142
|
+
items: ({ query }) => SLASH_ITEMS.filter((item) => item.title.toLowerCase().includes(query.toLowerCase())),
|
|
143
|
+
command: ({ editor, range, props }) => {
|
|
144
|
+
props.command({ editor, range, onInsertImage, onInsertVideo });
|
|
145
|
+
},
|
|
146
|
+
render: () => {
|
|
147
|
+
let component = null;
|
|
148
|
+
let unmount = null;
|
|
149
|
+
return {
|
|
150
|
+
onStart: (props) => {
|
|
151
|
+
component = new ReactRenderer(SlashMenu, {
|
|
152
|
+
editor: props.editor,
|
|
153
|
+
props,
|
|
154
|
+
});
|
|
155
|
+
unmount = props.mount(component.element);
|
|
156
|
+
},
|
|
157
|
+
onUpdate: (props) => {
|
|
158
|
+
component?.updateProps(props);
|
|
159
|
+
},
|
|
160
|
+
onKeyDown: (props) => {
|
|
161
|
+
if (props.event.key === "Escape") {
|
|
162
|
+
unmount?.();
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
return component?.ref?.onKeyDown(props) ?? false;
|
|
166
|
+
},
|
|
167
|
+
onExit: () => {
|
|
168
|
+
unmount?.();
|
|
169
|
+
component?.destroy();
|
|
170
|
+
unmount = null;
|
|
171
|
+
component = null;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
},
|
|
175
|
+
}),
|
|
176
|
+
];
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
};
|
|
@@ -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 function normalizeTextColor(color: string): string | null;
|
|
4
|
+
/** 记住自定义色:去重后追加,超过上限丢掉最早的;内置色不写入 */
|
|
5
|
+
export declare function rememberCustomTextColor(current: readonly string[], color: string, builtin: readonly string[]): string[];
|
|
6
|
+
export declare function removeCustomTextColor(current: readonly string[], color: string): string[];
|
|
7
|
+
export declare function readCustomTextColors(): string[];
|
|
8
|
+
export declare function 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/text-color-presets.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,qBAAqB,0BAA0B,CAAC;AAC7D,eAAO,MAAM,uBAAuB,KAAK,CAAC;AAI1C,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,iBAG/C;AAED,oCAAoC;AACpC,wBAAgB,uBAAuB,CACrC,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,SAAS,MAAM,EAAE,YAe3B;AAED,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,EAAE,KAAK,EAAE,MAAM,YAM9E;AAED,wBAAgB,oBAAoB,aAYnC;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,QAE9D"}
|
|
@@ -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 function normalizeTextColor(color) {
|
|
5
|
+
const value = color.trim().toLowerCase();
|
|
6
|
+
return HEX.test(value) ? value : null;
|
|
7
|
+
}
|
|
8
|
+
/** 记住自定义色:去重后追加,超过上限丢掉最早的;内置色不写入 */
|
|
9
|
+
export function 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 function 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 function 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 function writeCustomTextColors(colors) {
|
|
45
|
+
localStorage.setItem(TEXT_COLOR_PRESET_KEY, JSON.stringify(colors));
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type FC } from "react";
|
|
2
|
+
export declare const FONT_FAMILIES: readonly [{
|
|
3
|
+
readonly label: "宋体";
|
|
4
|
+
readonly value: "SimSun, Songti SC, serif";
|
|
5
|
+
}, {
|
|
6
|
+
readonly label: "黑体";
|
|
7
|
+
readonly value: "SimHei, Heiti SC, sans-serif";
|
|
8
|
+
}, {
|
|
9
|
+
readonly label: "楷体";
|
|
10
|
+
readonly value: "KaiTi, Kaiti SC, serif";
|
|
11
|
+
}, {
|
|
12
|
+
readonly label: "微软雅黑";
|
|
13
|
+
readonly value: "\"Microsoft YaHei\", \"PingFang SC\", sans-serif";
|
|
14
|
+
}, {
|
|
15
|
+
readonly label: "苹方";
|
|
16
|
+
readonly value: "\"PingFang SC\", \"Microsoft YaHei\", sans-serif";
|
|
17
|
+
}, {
|
|
18
|
+
readonly label: "Arial";
|
|
19
|
+
readonly value: "Arial, sans-serif";
|
|
20
|
+
}, {
|
|
21
|
+
readonly label: "Georgia";
|
|
22
|
+
readonly value: "Georgia, serif";
|
|
23
|
+
}, {
|
|
24
|
+
readonly label: "Times New Roman";
|
|
25
|
+
readonly value: "\"Times New Roman\", Times, serif";
|
|
26
|
+
}, {
|
|
27
|
+
readonly label: "Consolas";
|
|
28
|
+
readonly value: "Consolas, monospace";
|
|
29
|
+
}];
|
|
30
|
+
export declare const FONT_SIZES: readonly ["12px", "14px", "16px", "18px", "20px", "24px", "28px", "32px", "36px"];
|
|
31
|
+
export declare const LETTER_SPACINGS: readonly ["-1px", "-0.5px", "0px", "0.5px", "1px", "1.5px", "2px", "3px", "4px", "6px", "8px"];
|
|
32
|
+
export declare const LINE_HEIGHTS: readonly ["1", "1.15", "1.25", "1.5", "1.75", "2", "2.25", "2.5", "2.75", "3"];
|
|
33
|
+
interface TextStyleMenuProps {
|
|
34
|
+
fontFamily: string | null;
|
|
35
|
+
fontSize: string | null;
|
|
36
|
+
letterSpacing: string | null;
|
|
37
|
+
lineHeight: string | null;
|
|
38
|
+
onFontFamily: (value: string | null) => void;
|
|
39
|
+
onFontSize: (value: string | null) => void;
|
|
40
|
+
onLetterSpacing: (value: string | null) => void;
|
|
41
|
+
onLineHeight: (value: string | null) => void;
|
|
42
|
+
}
|
|
43
|
+
/** 工具栏文本样式:字体、字号、字间距、行距 */
|
|
44
|
+
export declare const TextStyleMenu: FC<TextStyleMenuProps>;
|
|
45
|
+
export {};
|
|
46
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/text-style-menu/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEpE,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;EAUhB,CAAC;AAEX,eAAO,MAAM,UAAU,mFAAoF,CAAC;AAE5G,eAAO,MAAM,eAAe,gGAYlB,CAAC;AAEX,eAAO,MAAM,YAAY,gFAAiF,CAAC;AAS3G,UAAU,kBAAkB;IAC1B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,UAAU,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C,eAAe,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IAChD,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;CAC9C;AAwBD,2BAA2B;AAC3B,eAAO,MAAM,aAAa,EAAE,EAAE,CAAC,kBAAkB,CA0EhD,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Type } from "lucide-react";
|
|
4
|
+
import { useEffect, useId, useRef, useState } from "react";
|
|
5
|
+
export const FONT_FAMILIES = [
|
|
6
|
+
{ label: "宋体", value: "SimSun, Songti SC, serif" },
|
|
7
|
+
{ label: "黑体", value: "SimHei, Heiti SC, sans-serif" },
|
|
8
|
+
{ label: "楷体", value: "KaiTi, Kaiti SC, serif" },
|
|
9
|
+
{ label: "微软雅黑", value: '"Microsoft YaHei", "PingFang SC", sans-serif' },
|
|
10
|
+
{ label: "苹方", value: '"PingFang SC", "Microsoft YaHei", sans-serif' },
|
|
11
|
+
{ label: "Arial", value: "Arial, sans-serif" },
|
|
12
|
+
{ label: "Georgia", value: "Georgia, serif" },
|
|
13
|
+
{ label: "Times New Roman", value: '"Times New Roman", Times, serif' },
|
|
14
|
+
{ label: "Consolas", value: "Consolas, monospace" },
|
|
15
|
+
];
|
|
16
|
+
export const FONT_SIZES = ["12px", "14px", "16px", "18px", "20px", "24px", "28px", "32px", "36px"];
|
|
17
|
+
export const LETTER_SPACINGS = [
|
|
18
|
+
"-1px",
|
|
19
|
+
"-0.5px",
|
|
20
|
+
"0px",
|
|
21
|
+
"0.5px",
|
|
22
|
+
"1px",
|
|
23
|
+
"1.5px",
|
|
24
|
+
"2px",
|
|
25
|
+
"3px",
|
|
26
|
+
"4px",
|
|
27
|
+
"6px",
|
|
28
|
+
"8px",
|
|
29
|
+
];
|
|
30
|
+
export const LINE_HEIGHTS = ["1", "1.15", "1.25", "1.5", "1.75", "2", "2.25", "2.5", "2.75", "3"];
|
|
31
|
+
const StyleRow = ({ label, value, options, onChange }) => {
|
|
32
|
+
const id = useId();
|
|
33
|
+
return (_jsxs("label", { className: "wsmd-text-style-menu__row", htmlFor: id, children: [_jsx("span", { children: label }), _jsxs("select", { id: id, className: "wsmd-text-style-menu__select", value: value ?? "", onChange: (event) => onChange(event.target.value || null), children: [_jsx("option", { value: "", children: "\u9ED8\u8BA4" }), options.map((option) => (_jsx("option", { value: option.value, children: option.label }, option.value)))] })] }));
|
|
34
|
+
};
|
|
35
|
+
/** 工具栏文本样式:字体、字号、字间距、行距 */
|
|
36
|
+
export const TextStyleMenu = ({ fontFamily, fontSize, letterSpacing, lineHeight, onFontFamily, onFontSize, onLetterSpacing, onLineHeight }) => {
|
|
37
|
+
const [open, setOpen] = useState(false);
|
|
38
|
+
const rootRef = useRef(null);
|
|
39
|
+
const active = Boolean(fontFamily || fontSize || letterSpacing || lineHeight);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const onPointerDown = (event) => {
|
|
45
|
+
if (!rootRef.current?.contains(event.target)) {
|
|
46
|
+
setOpen(false);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const onKeyDown = (event) => {
|
|
50
|
+
if (event.key === "Escape") {
|
|
51
|
+
setOpen(false);
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
55
|
+
document.addEventListener("keydown", onKeyDown);
|
|
56
|
+
return () => {
|
|
57
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
58
|
+
document.removeEventListener("keydown", onKeyDown);
|
|
59
|
+
};
|
|
60
|
+
}, [open]);
|
|
61
|
+
return (_jsxs("div", { className: "wsmd-text-style-menu", ref: rootRef, children: [_jsx("button", { className: `wsmd-icon-btn wsmd-icon-btn--ghost wsmd-toolbar__btn${active ? " wsmd-toolbar__btn--active" : ""}`, type: "button", title: "\u6587\u672C", "aria-label": "\u6587\u672C", "aria-expanded": open, "aria-haspopup": "dialog", "aria-pressed": active, onClick: () => setOpen((value) => !value), children: _jsx(Type, { "aria-hidden": true }) }), open ? (_jsxs("div", { className: "wsmd-text-style-menu__panel", role: "dialog", "aria-label": "\u6587\u672C", children: [_jsx(StyleRow, { label: "\u5B57\u4F53", value: fontFamily, options: FONT_FAMILIES, onChange: onFontFamily }), _jsx(StyleRow, { label: "\u5B57\u53F7", value: fontSize, options: FONT_SIZES.map((size) => ({ label: size, value: size })), onChange: onFontSize }), _jsx(StyleRow, { label: "\u5B57\u95F4\u8DDD", value: letterSpacing, options: LETTER_SPACINGS.map((spacing) => ({
|
|
62
|
+
label: spacing === "0px" ? "0" : spacing,
|
|
63
|
+
value: spacing,
|
|
64
|
+
})), onChange: onLetterSpacing }), _jsx(StyleRow, { label: "\u884C\u8DDD", value: lineHeight, options: LINE_HEIGHTS.map((height) => ({ label: height, value: height })), onChange: onLineHeight })] })) : null] }));
|
|
65
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare const THEME_STORAGE_KEY = "kb-theme";
|
|
2
|
+
export type ColorTheme = "light" | "dark";
|
|
3
|
+
export type ThemePreference = ColorTheme | "system";
|
|
4
|
+
export declare function isColorTheme(value: string | null): value is ColorTheme;
|
|
5
|
+
/** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
|
|
6
|
+
export declare function applyThemePreference(preference: ThemePreference): void;
|
|
7
|
+
export declare function resolveColorTheme(preference: ThemePreference): ColorTheme;
|
|
8
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../src/editor/theme.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,iBAAiB,aAAa,CAAC;AAE5C,MAAM,MAAM,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAC1C,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,QAAQ,CAAC;AAEpD,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,KAAK,IAAI,UAAU,CAEtE;AAED,oDAAoD;AACpD,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,eAAe,QAM/D;AAED,wBAAgB,iBAAiB,CAAC,UAAU,EAAE,eAAe,GAAG,UAAU,CAKzE"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const THEME_STORAGE_KEY = "kb-theme";
|
|
2
|
+
export function isColorTheme(value) {
|
|
3
|
+
return value === "light" || value === "dark";
|
|
4
|
+
}
|
|
5
|
+
/** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
|
|
6
|
+
export function applyThemePreference(preference) {
|
|
7
|
+
if (preference === "system") {
|
|
8
|
+
document.documentElement.removeAttribute("data-theme");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
document.documentElement.dataset.theme = preference;
|
|
12
|
+
}
|
|
13
|
+
export function resolveColorTheme(preference) {
|
|
14
|
+
if (preference !== "system") {
|
|
15
|
+
return preference;
|
|
16
|
+
}
|
|
17
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
18
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/react";
|
|
2
|
+
import { type FC } from "react";
|
|
3
|
+
import { type ToolbarItem } from "./items";
|
|
4
|
+
interface ToolbarProps {
|
|
5
|
+
editor: Editor;
|
|
6
|
+
items: readonly ToolbarItem[];
|
|
7
|
+
uploading: boolean;
|
|
8
|
+
videoOpen: boolean;
|
|
9
|
+
videoError: string | null;
|
|
10
|
+
onInsertImage: () => void;
|
|
11
|
+
onVideoOpenChange: (open: boolean) => void;
|
|
12
|
+
onUploadVideo: () => void;
|
|
13
|
+
onInsertVideoUrl: (url: string) => void;
|
|
14
|
+
}
|
|
15
|
+
export declare const Toolbar: FC<ToolbarProps>;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/toolbar/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAgC5C,OAAO,EAAuB,KAAK,EAAE,EAAkB,MAAM,OAAO,CAAC;AAUrE,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAqB5D,UAAU,YAAY;IACpB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9B,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,iBAAiB,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IAC3C,aAAa,EAAE,MAAM,IAAI,CAAC;IAC1B,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACzC;AAuED,eAAO,MAAM,OAAO,EAAE,EAAE,CAAC,YAAY,CAmXpC,CAAC"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEditorState } from "@tiptap/react";
|
|
4
|
+
import { AlignCenter, AlignJustify, AlignLeft, AlignRight, Baseline, Bold, Code, Heading1, Heading2, Heading3, Heading4, Heading5, Heading6, Highlighter, ImageIcon, Italic, Link as LinkIcon, List, ListOrdered, Loader2, Moon, Pilcrow, Quote, Redo2, Strikethrough, Sun, Table, Undo2, } from "lucide-react";
|
|
5
|
+
import { useEffect, useState } from "react";
|
|
6
|
+
import { ColorMenu, PRESET_HIGHLIGHT_COLORS, PRESET_TEXT_COLORS } from "../color-menu";
|
|
7
|
+
import { readCustomTextColors, rememberCustomTextColor, removeCustomTextColor, writeCustomTextColors, } from "../color-menu/text-color-presets";
|
|
8
|
+
import { TextStyleMenu } from "../text-style-menu";
|
|
9
|
+
import { VideoMenu } from "../video-menu";
|
|
10
|
+
import { DEFAULT_TOOLBAR } from "./items";
|
|
11
|
+
import { applyThemePreference, isColorTheme, resolveColorTheme, THEME_STORAGE_KEY, } from "./theme";
|
|
12
|
+
const HEADING_ICONS = {
|
|
13
|
+
1: Heading1,
|
|
14
|
+
2: Heading2,
|
|
15
|
+
3: Heading3,
|
|
16
|
+
4: Heading4,
|
|
17
|
+
5: Heading5,
|
|
18
|
+
6: Heading6,
|
|
19
|
+
};
|
|
20
|
+
const ToolbarButton = ({ title, active, disabled, onClick, children }) => {
|
|
21
|
+
return (_jsx("button", { className: `wsmd-icon-btn wsmd-icon-btn--ghost wsmd-toolbar__btn${active ? " wsmd-toolbar__btn--active" : ""}`, type: "button", title: title, "aria-label": title, "aria-pressed": active, disabled: disabled, onClick: onClick, children: children }));
|
|
22
|
+
};
|
|
23
|
+
const useToolbarTheme = () => {
|
|
24
|
+
const [preference, setPreference] = useState("system");
|
|
25
|
+
const [resolved, setResolved] = useState("light");
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const stored = localStorage.getItem(THEME_STORAGE_KEY);
|
|
28
|
+
const next = isColorTheme(stored) ? stored : "system";
|
|
29
|
+
setPreference(next);
|
|
30
|
+
setResolved(resolveColorTheme(next));
|
|
31
|
+
applyThemePreference(next);
|
|
32
|
+
}, []);
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
35
|
+
const sync = () => setResolved(resolveColorTheme(preference));
|
|
36
|
+
media.addEventListener("change", sync);
|
|
37
|
+
return () => media.removeEventListener("change", sync);
|
|
38
|
+
}, [preference]);
|
|
39
|
+
const toggle = () => {
|
|
40
|
+
const next = resolved === "dark" ? "light" : "dark";
|
|
41
|
+
localStorage.setItem(THEME_STORAGE_KEY, next);
|
|
42
|
+
setPreference(next);
|
|
43
|
+
setResolved(next);
|
|
44
|
+
applyThemePreference(next);
|
|
45
|
+
};
|
|
46
|
+
return { resolved, toggle };
|
|
47
|
+
};
|
|
48
|
+
const textStyleValue = (value) => {
|
|
49
|
+
return typeof value === "string" && value ? value : null;
|
|
50
|
+
};
|
|
51
|
+
const headingLevel = (item) => {
|
|
52
|
+
if (!item.startsWith("h")) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const level = Number(item.slice(1));
|
|
56
|
+
if (level < 1 || level > 6) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return level;
|
|
60
|
+
};
|
|
61
|
+
export const Toolbar = ({ editor, items = DEFAULT_TOOLBAR, uploading, videoOpen, videoError, onInsertImage, onVideoOpenChange, onUploadVideo, onInsertVideoUrl }) => {
|
|
62
|
+
const { resolved, toggle } = useToolbarTheme();
|
|
63
|
+
const [customTextColors, setCustomTextColors] = useState([]);
|
|
64
|
+
const state = useEditorState({
|
|
65
|
+
editor,
|
|
66
|
+
selector: (ctx) => ({
|
|
67
|
+
canUndo: ctx.editor.can().undo(),
|
|
68
|
+
canRedo: ctx.editor.can().redo(),
|
|
69
|
+
isParagraph: ctx.editor.isActive("paragraph"),
|
|
70
|
+
headingLevels: [1, 2, 3, 4, 5, 6].map((level) => ctx.editor.isActive("heading", { level })),
|
|
71
|
+
isBold: ctx.editor.isActive("bold"),
|
|
72
|
+
isItalic: ctx.editor.isActive("italic"),
|
|
73
|
+
isStrike: ctx.editor.isActive("strike"),
|
|
74
|
+
isHighlight: ctx.editor.isActive("highlight"),
|
|
75
|
+
highlightColor: ctx.editor.getAttributes("highlight").color ?? null,
|
|
76
|
+
textColor: textStyleValue(ctx.editor.getAttributes("textStyle").color),
|
|
77
|
+
fontFamily: textStyleValue(ctx.editor.getAttributes("textStyle").fontFamily),
|
|
78
|
+
fontSize: textStyleValue(ctx.editor.getAttributes("textStyle").fontSize),
|
|
79
|
+
letterSpacing: textStyleValue(ctx.editor.getAttributes("textStyle").letterSpacing),
|
|
80
|
+
lineHeight: textStyleValue(ctx.editor.getAttributes("textStyle").lineHeight),
|
|
81
|
+
isBulletList: ctx.editor.isActive("bulletList"),
|
|
82
|
+
isOrderedList: ctx.editor.isActive("orderedList"),
|
|
83
|
+
isBlockquote: ctx.editor.isActive("blockquote"),
|
|
84
|
+
isCodeBlock: ctx.editor.isActive("codeBlock"),
|
|
85
|
+
isAlignLeft: ctx.editor.isActive({ textAlign: "left" }),
|
|
86
|
+
isAlignCenter: ctx.editor.isActive({ textAlign: "center" }),
|
|
87
|
+
isAlignRight: ctx.editor.isActive({ textAlign: "right" }),
|
|
88
|
+
isAlignJustify: ctx.editor.isActive({ textAlign: "justify" }),
|
|
89
|
+
isLink: ctx.editor.isActive("link"),
|
|
90
|
+
}),
|
|
91
|
+
});
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
setCustomTextColors(readCustomTextColors());
|
|
94
|
+
}, []);
|
|
95
|
+
const setLink = () => {
|
|
96
|
+
const url = window.prompt("链接地址", editor.getAttributes("link").href ?? "");
|
|
97
|
+
if (url === null) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (url === "") {
|
|
101
|
+
editor.chain().focus().unsetLink().run();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
editor.chain().focus().extendMarkRange("link").setLink({ href: url }).run();
|
|
105
|
+
};
|
|
106
|
+
const renderItem = (item, index) => {
|
|
107
|
+
const level = headingLevel(item);
|
|
108
|
+
if (level) {
|
|
109
|
+
const Icon = HEADING_ICONS[level];
|
|
110
|
+
return (_jsx(ToolbarButton, { title: `标题 ${level}`, active: state.headingLevels[level - 1], onClick: () => editor.chain().focus().toggleHeading({ level }).run(), children: _jsx(Icon, { "aria-hidden": true }) }, `${item}-${index}`));
|
|
111
|
+
}
|
|
112
|
+
switch (item) {
|
|
113
|
+
case "divider":
|
|
114
|
+
return _jsx("span", { className: "wsmd-toolbar__divider" }, `${item}-${index}`);
|
|
115
|
+
case "spacer":
|
|
116
|
+
return _jsx("span", { className: "wsmd-toolbar__spacer" }, `${item}-${index}`);
|
|
117
|
+
case "paragraph":
|
|
118
|
+
return (_jsx(ToolbarButton, { title: "\u6BB5\u843D", active: state.isParagraph, onClick: () => editor.chain().focus().setParagraph().run(), children: _jsx(Pilcrow, { "aria-hidden": true }) }, item));
|
|
119
|
+
case "bold":
|
|
120
|
+
return (_jsx(ToolbarButton, { title: "\u7C97\u4F53", active: state.isBold, onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { "aria-hidden": true }) }, item));
|
|
121
|
+
case "italic":
|
|
122
|
+
return (_jsx(ToolbarButton, { title: "\u659C\u4F53", active: state.isItalic, onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { "aria-hidden": true }) }, item));
|
|
123
|
+
case "strike":
|
|
124
|
+
return (_jsx(ToolbarButton, { title: "\u5220\u9664\u7EBF", active: state.isStrike, onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { "aria-hidden": true }) }, item));
|
|
125
|
+
case "highlight":
|
|
126
|
+
return (_jsx(ColorMenu, { label: "\u9AD8\u4EAE", active: state.isHighlight, currentColor: state.highlightColor, presets: PRESET_HIGHLIGHT_COLORS, onPick: (color) => editor.chain().focus().setHighlight({ color }).run(), onClear: () => editor.chain().focus().unsetHighlight().run(), children: _jsx(Highlighter, { "aria-hidden": true }) }, item));
|
|
127
|
+
case "color":
|
|
128
|
+
return (_jsx(ColorMenu, { label: "\u6587\u5B57\u989C\u8272", active: Boolean(state.textColor), currentColor: state.textColor, presets: PRESET_TEXT_COLORS, customPresets: customTextColors, onPick: (color) => editor.chain().focus().setColor(color).run(), onRemember: (color) => {
|
|
129
|
+
setCustomTextColors((current) => {
|
|
130
|
+
const next = rememberCustomTextColor(current, color, PRESET_TEXT_COLORS);
|
|
131
|
+
writeCustomTextColors(next);
|
|
132
|
+
return next;
|
|
133
|
+
});
|
|
134
|
+
}, onRemoveCustom: (color) => {
|
|
135
|
+
setCustomTextColors((current) => {
|
|
136
|
+
const next = removeCustomTextColor(current, color);
|
|
137
|
+
writeCustomTextColors(next);
|
|
138
|
+
return next;
|
|
139
|
+
});
|
|
140
|
+
}, onClear: () => editor.chain().focus().unsetColor().run(), children: _jsx(Baseline, { "aria-hidden": true }) }, item));
|
|
141
|
+
case "textStyle":
|
|
142
|
+
return (_jsx(TextStyleMenu, { fontFamily: state.fontFamily, fontSize: state.fontSize, letterSpacing: state.letterSpacing, lineHeight: state.lineHeight, onFontFamily: (value) => {
|
|
143
|
+
const chain = editor.chain().focus();
|
|
144
|
+
if (value) {
|
|
145
|
+
chain.setFontFamily(value).run();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
chain.unsetFontFamily().run();
|
|
149
|
+
}, onFontSize: (value) => {
|
|
150
|
+
const chain = editor.chain().focus();
|
|
151
|
+
if (value) {
|
|
152
|
+
chain.setFontSize(value).run();
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
chain.unsetFontSize().run();
|
|
156
|
+
}, onLetterSpacing: (value) => {
|
|
157
|
+
const chain = editor.chain().focus();
|
|
158
|
+
if (value) {
|
|
159
|
+
chain.setLetterSpacing(value).run();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
chain.unsetLetterSpacing().run();
|
|
163
|
+
}, onLineHeight: (value) => {
|
|
164
|
+
const chain = editor.chain().focus();
|
|
165
|
+
if (value) {
|
|
166
|
+
chain.setLineHeight(value).run();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
chain.unsetLineHeight().run();
|
|
170
|
+
} }, item));
|
|
171
|
+
case "list:bullet":
|
|
172
|
+
return (_jsx(ToolbarButton, { title: "\u65E0\u5E8F\u5217\u8868", active: state.isBulletList, onClick: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, { "aria-hidden": true }) }, item));
|
|
173
|
+
case "list:ordered":
|
|
174
|
+
return (_jsx(ToolbarButton, { title: "\u6709\u5E8F\u5217\u8868", active: state.isOrderedList, onClick: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, { "aria-hidden": true }) }, item));
|
|
175
|
+
case "quote":
|
|
176
|
+
return (_jsx(ToolbarButton, { title: "\u5F15\u7528", active: state.isBlockquote, onClick: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, { "aria-hidden": true }) }, item));
|
|
177
|
+
case "code":
|
|
178
|
+
return (_jsx(ToolbarButton, { title: "\u4EE3\u7801\u5757", active: state.isCodeBlock, onClick: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(Code, { "aria-hidden": true }) }, item));
|
|
179
|
+
case "align:left":
|
|
180
|
+
return (_jsx(ToolbarButton, { title: "\u5DE6\u5BF9\u9F50", active: state.isAlignLeft, onClick: () => editor.chain().focus().setTextAlign("left").run(), children: _jsx(AlignLeft, { "aria-hidden": true }) }, item));
|
|
181
|
+
case "align:center":
|
|
182
|
+
return (_jsx(ToolbarButton, { title: "\u5C45\u4E2D", active: state.isAlignCenter, onClick: () => editor.chain().focus().setTextAlign("center").run(), children: _jsx(AlignCenter, { "aria-hidden": true }) }, item));
|
|
183
|
+
case "align:right":
|
|
184
|
+
return (_jsx(ToolbarButton, { title: "\u53F3\u5BF9\u9F50", active: state.isAlignRight, onClick: () => editor.chain().focus().setTextAlign("right").run(), children: _jsx(AlignRight, { "aria-hidden": true }) }, item));
|
|
185
|
+
case "align:justify":
|
|
186
|
+
return (_jsx(ToolbarButton, { title: "\u4E24\u7AEF\u5BF9\u9F50", active: state.isAlignJustify, onClick: () => editor.chain().focus().setTextAlign("justify").run(), children: _jsx(AlignJustify, { "aria-hidden": true }) }, item));
|
|
187
|
+
case "link":
|
|
188
|
+
return (_jsx(ToolbarButton, { title: "\u94FE\u63A5", active: state.isLink, onClick: setLink, children: _jsx(LinkIcon, { "aria-hidden": true }) }, item));
|
|
189
|
+
case "table":
|
|
190
|
+
return (_jsx(ToolbarButton, { title: "\u8868\u683C", onClick: () => editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run(), children: _jsx(Table, { "aria-hidden": true }) }, item));
|
|
191
|
+
case "image":
|
|
192
|
+
return (_jsx(ToolbarButton, { title: uploading ? "上传中" : "图片", disabled: uploading, onClick: onInsertImage, children: uploading ? _jsx(Loader2, { className: "wsmd-icon-btn__spin", "aria-hidden": true }) : _jsx(ImageIcon, { "aria-hidden": true }) }, item));
|
|
193
|
+
case "video":
|
|
194
|
+
return (_jsx(VideoMenu, { open: videoOpen, uploading: uploading, error: videoError, onOpenChange: onVideoOpenChange, onUpload: onUploadVideo, onSubmitUrl: onInsertVideoUrl }, item));
|
|
195
|
+
case "undo":
|
|
196
|
+
return (_jsx(ToolbarButton, { title: "\u64A4\u9500", disabled: !state.canUndo, onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { "aria-hidden": true }) }, item));
|
|
197
|
+
case "redo":
|
|
198
|
+
return (_jsx(ToolbarButton, { title: "\u6062\u590D", disabled: !state.canRedo, onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { "aria-hidden": true }) }, item));
|
|
199
|
+
case "theme":
|
|
200
|
+
return (_jsx(ToolbarButton, { title: resolved === "dark" ? "切换浅色" : "切换深色", onClick: toggle, children: resolved === "dark" ? _jsx(Sun, { "aria-hidden": true }) : _jsx(Moon, { "aria-hidden": true }) }, item));
|
|
201
|
+
default:
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
return (_jsx("div", { className: "wsmd-toolbar", role: "toolbar", "aria-label": "\u683C\u5F0F", children: items.map((item, index) => renderItem(item, index)) }));
|
|
206
|
+
};
|