@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,10 @@
|
|
|
1
|
+
export declare const DEFAULT_TOOLBAR: readonly ["paragraph", "h1", "h2", "h3", "h4", "h5", "h6", "divider", "bold", "italic", "strike", "highlight", "color", "textStyle", "divider", "list:bullet", "list:ordered", "quote", "code", "divider", "align:left", "align:center", "align:right", "align:justify", "divider", "link", "table", "image", "video", "spacer", "undo", "redo"];
|
|
2
|
+
export type ToolbarItem = (typeof DEFAULT_TOOLBAR)[number] | "theme";
|
|
3
|
+
export type UploadFileType = "image" | "video" | "file";
|
|
4
|
+
export type UploadFileResult = {
|
|
5
|
+
id: number | string;
|
|
6
|
+
url: string;
|
|
7
|
+
extra: Record<string, any>;
|
|
8
|
+
};
|
|
9
|
+
export type UploadFile = (file: File, type: UploadFileType) => Promise<UploadFileResult>;
|
|
10
|
+
//# sourceMappingURL=items.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"items.d.ts","sourceRoot":"","sources":["../../../src/editor/toolbar/items.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,kVAiClB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;AAErE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const DEFAULT_TOOLBAR = [
|
|
2
|
+
"paragraph",
|
|
3
|
+
"h1",
|
|
4
|
+
"h2",
|
|
5
|
+
"h3",
|
|
6
|
+
"h4",
|
|
7
|
+
"h5",
|
|
8
|
+
"h6",
|
|
9
|
+
"divider",
|
|
10
|
+
"bold",
|
|
11
|
+
"italic",
|
|
12
|
+
"strike",
|
|
13
|
+
"highlight",
|
|
14
|
+
"color",
|
|
15
|
+
"textStyle",
|
|
16
|
+
"divider",
|
|
17
|
+
"list:bullet",
|
|
18
|
+
"list:ordered",
|
|
19
|
+
"quote",
|
|
20
|
+
"code",
|
|
21
|
+
"divider",
|
|
22
|
+
"align:left",
|
|
23
|
+
"align:center",
|
|
24
|
+
"align:right",
|
|
25
|
+
"align:justify",
|
|
26
|
+
"divider",
|
|
27
|
+
"link",
|
|
28
|
+
"table",
|
|
29
|
+
"image",
|
|
30
|
+
"video",
|
|
31
|
+
"spacer",
|
|
32
|
+
"undo",
|
|
33
|
+
"redo",
|
|
34
|
+
];
|
|
@@ -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 const isColorTheme: (value: string | null) => value is ColorTheme;
|
|
5
|
+
/** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
|
|
6
|
+
export declare const applyThemePreference: (preference: ThemePreference) => void;
|
|
7
|
+
export declare const resolveColorTheme: (preference: ThemePreference) => ColorTheme;
|
|
8
|
+
//# sourceMappingURL=theme.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"theme.d.ts","sourceRoot":"","sources":["../../../src/editor/toolbar/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,eAAO,MAAM,YAAY,GAAI,OAAO,MAAM,GAAG,IAAI,KAAG,KAAK,IAAI,UAE5D,CAAC;AAEF,oDAAoD;AACpD,eAAO,MAAM,oBAAoB,GAAI,YAAY,eAAe,SAM/D,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,eAAe,KAAG,UAK/D,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const THEME_STORAGE_KEY = "kb-theme";
|
|
2
|
+
export const isColorTheme = (value) => {
|
|
3
|
+
return value === "light" || value === "dark";
|
|
4
|
+
};
|
|
5
|
+
/** 未手动选择时不写 data-theme,交给系统 prefers-color-scheme */
|
|
6
|
+
export const applyThemePreference = (preference) => {
|
|
7
|
+
if (preference === "system") {
|
|
8
|
+
document.documentElement.removeAttribute("data-theme");
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
document.documentElement.dataset.theme = preference;
|
|
12
|
+
};
|
|
13
|
+
export const 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,10 @@
|
|
|
1
|
+
export declare const DEFAULT_TOOLBAR: readonly ["paragraph", "h1", "h2", "h3", "h4", "h5", "h6", "divider", "bold", "italic", "strike", "highlight", "color", "textStyle", "divider", "list:bullet", "list:ordered", "quote", "code", "divider", "align:left", "align:center", "align:right", "align:justify", "divider", "link", "table", "image", "video", "spacer", "undo", "redo"];
|
|
2
|
+
export type ToolbarItem = (typeof DEFAULT_TOOLBAR)[number] | "theme";
|
|
3
|
+
export type UploadFileType = "image" | "video" | "file";
|
|
4
|
+
export type UploadFileResult = {
|
|
5
|
+
id: number | string;
|
|
6
|
+
url: string;
|
|
7
|
+
extra: Record<string, any>;
|
|
8
|
+
};
|
|
9
|
+
export type UploadFile = (file: File, type: UploadFileType) => Promise<UploadFileResult>;
|
|
10
|
+
//# sourceMappingURL=toolbar-items.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"toolbar-items.d.ts","sourceRoot":"","sources":["../../src/editor/toolbar-items.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,eAAe,kVAiClB,CAAC;AAEX,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;AAErE,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,OAAO,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,GAAG,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,cAAc,KAAK,OAAO,CAAC,gBAAgB,CAAC,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const DEFAULT_TOOLBAR = [
|
|
2
|
+
"paragraph",
|
|
3
|
+
"h1",
|
|
4
|
+
"h2",
|
|
5
|
+
"h3",
|
|
6
|
+
"h4",
|
|
7
|
+
"h5",
|
|
8
|
+
"h6",
|
|
9
|
+
"divider",
|
|
10
|
+
"bold",
|
|
11
|
+
"italic",
|
|
12
|
+
"strike",
|
|
13
|
+
"highlight",
|
|
14
|
+
"color",
|
|
15
|
+
"textStyle",
|
|
16
|
+
"divider",
|
|
17
|
+
"list:bullet",
|
|
18
|
+
"list:ordered",
|
|
19
|
+
"quote",
|
|
20
|
+
"code",
|
|
21
|
+
"divider",
|
|
22
|
+
"align:left",
|
|
23
|
+
"align:center",
|
|
24
|
+
"align:right",
|
|
25
|
+
"align:justify",
|
|
26
|
+
"divider",
|
|
27
|
+
"link",
|
|
28
|
+
"table",
|
|
29
|
+
"image",
|
|
30
|
+
"video",
|
|
31
|
+
"spacer",
|
|
32
|
+
"undo",
|
|
33
|
+
"redo",
|
|
34
|
+
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Editor } from "@tiptap/react";
|
|
2
|
+
import { type ToolbarItem } from "./toolbar-items";
|
|
3
|
+
type ToolbarProps = {
|
|
4
|
+
editor: Editor;
|
|
5
|
+
items: readonly ToolbarItem[];
|
|
6
|
+
uploading: boolean;
|
|
7
|
+
videoOpen: boolean;
|
|
8
|
+
videoError: string | null;
|
|
9
|
+
onInsertImage: () => void;
|
|
10
|
+
onVideoOpenChange: (open: boolean) => void;
|
|
11
|
+
onUploadVideo: () => void;
|
|
12
|
+
onInsertVideoUrl: (url: string) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function Toolbar({ editor, items, uploading, videoOpen, videoError, onInsertImage, onVideoOpenChange, onUploadVideo, onInsertVideoUrl, }: ToolbarProps): import("react").JSX.Element;
|
|
15
|
+
export {};
|
|
16
|
+
//# sourceMappingURL=Toolbar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Toolbar.d.ts","sourceRoot":"","sources":["../../src/editor/Toolbar.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAiD5C,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAcpE,KAAK,YAAY,GAAG;IAClB,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,CAAC;AAuEF,wBAAgB,OAAO,CAAC,EACtB,MAAM,EACN,KAAuB,EACvB,SAAS,EACT,SAAS,EACT,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,gBAAgB,GACjB,EAAE,YAAY,+BAmXd"}
|
|
@@ -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 "./ColorMenu";
|
|
7
|
+
import { TextStyleMenu } from "./TextStyleMenu";
|
|
8
|
+
import { readCustomTextColors, rememberCustomTextColor, removeCustomTextColor, writeCustomTextColors, } from "./text-color-presets";
|
|
9
|
+
import { applyThemePreference, isColorTheme, resolveColorTheme, THEME_STORAGE_KEY, } from "./theme";
|
|
10
|
+
import { DEFAULT_TOOLBAR } from "./toolbar-items";
|
|
11
|
+
import { VideoMenu } from "./VideoMenu";
|
|
12
|
+
const HEADING_ICONS = {
|
|
13
|
+
1: Heading1,
|
|
14
|
+
2: Heading2,
|
|
15
|
+
3: Heading3,
|
|
16
|
+
4: Heading4,
|
|
17
|
+
5: Heading5,
|
|
18
|
+
6: Heading6,
|
|
19
|
+
};
|
|
20
|
+
function ToolbarButton({ title, active, disabled, onClick, children }) {
|
|
21
|
+
return (_jsx("button", { className: `icon-btn icon-btn--ghost toolbar__btn${active ? " toolbar__btn--active" : ""}`, type: "button", title: title, "aria-label": title, "aria-pressed": active, disabled: disabled, onClick: onClick, children: children }));
|
|
22
|
+
}
|
|
23
|
+
function 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
|
+
function textStyleValue(value) {
|
|
49
|
+
return typeof value === "string" && value ? value : null;
|
|
50
|
+
}
|
|
51
|
+
function 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 function 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: "toolbar__divider" }, `${item}-${index}`);
|
|
115
|
+
case "spacer":
|
|
116
|
+
return _jsx("span", { className: "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: "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: "toolbar", role: "toolbar", "aria-label": "\u683C\u5F0F", children: items.map((item, index) => renderItem(item, index)) }));
|
|
206
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-dom-id.d.ts","sourceRoot":"","sources":["../../src/editor/use-dom-id.ts"],"names":[],"mappings":"AAIA,wBAAwB;AACxB,wBAAgB,QAAQ,WAOvB"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Node } from "@tiptap/core";
|
|
2
|
+
import type { VideoProvider } from "./video-url";
|
|
3
|
+
declare module "@tiptap/core" {
|
|
4
|
+
interface Commands<ReturnType> {
|
|
5
|
+
videoEmbed: {
|
|
6
|
+
setVideoEmbed: (attrs: {
|
|
7
|
+
src: string;
|
|
8
|
+
provider: VideoProvider;
|
|
9
|
+
}) => ReturnType;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** 块级视频:本地文件用 video,外站用官方播放器 iframe */
|
|
14
|
+
export declare const VideoEmbed: Node<any, any>;
|
|
15
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/video-embed/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAmB,MAAM,cAAc,CAAC;AAErD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,OAAO,QAAQ,cAAc,CAAC;IAC5B,UAAU,QAAQ,CAAC,UAAU;QAC3B,UAAU,EAAE;YACV,aAAa,EAAE,CAAC,KAAK,EAAE;gBAAE,GAAG,EAAE,MAAM,CAAC;gBAAC,QAAQ,EAAE,aAAa,CAAA;aAAE,KAAK,UAAU,CAAC;SAChF,CAAC;KACH;CACF;AAMD,uCAAuC;AACvC,eAAO,MAAM,UAAU,gBA8FrB,CAAC"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Node, mergeAttributes } from "@tiptap/core";
|
|
2
|
+
import { TextSelection } from "@tiptap/pm/state";
|
|
3
|
+
const attrString = (value) => {
|
|
4
|
+
return typeof value === "string" ? value : null;
|
|
5
|
+
};
|
|
6
|
+
/** 块级视频:本地文件用 video,外站用官方播放器 iframe */
|
|
7
|
+
export const VideoEmbed = Node.create({
|
|
8
|
+
name: "videoEmbed",
|
|
9
|
+
group: "block",
|
|
10
|
+
atom: true,
|
|
11
|
+
draggable: true,
|
|
12
|
+
selectable: true,
|
|
13
|
+
addAttributes() {
|
|
14
|
+
return {
|
|
15
|
+
src: { default: null },
|
|
16
|
+
provider: { default: "file" },
|
|
17
|
+
};
|
|
18
|
+
},
|
|
19
|
+
parseHTML() {
|
|
20
|
+
return [
|
|
21
|
+
{
|
|
22
|
+
tag: "video[src]",
|
|
23
|
+
getAttrs: (element) => {
|
|
24
|
+
if (!(element instanceof HTMLElement)) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
const src = element.getAttribute("src");
|
|
28
|
+
if (!src) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return { src, provider: "file" };
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
tag: "iframe[data-video-provider]",
|
|
36
|
+
getAttrs: (element) => {
|
|
37
|
+
if (!(element instanceof HTMLElement)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const provider = element.getAttribute("data-video-provider");
|
|
41
|
+
const src = element.getAttribute("src");
|
|
42
|
+
if ((provider !== "youtube" && provider !== "bilibili") || !src) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
return { src, provider };
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
];
|
|
49
|
+
},
|
|
50
|
+
renderHTML({ node }) {
|
|
51
|
+
const src = attrString(node.attrs.src);
|
|
52
|
+
const provider = node.attrs.provider;
|
|
53
|
+
if (!src) {
|
|
54
|
+
return ["video", { controls: "true", preload: "metadata" }];
|
|
55
|
+
}
|
|
56
|
+
if (provider === "youtube" || provider === "bilibili") {
|
|
57
|
+
return [
|
|
58
|
+
"iframe",
|
|
59
|
+
mergeAttributes({
|
|
60
|
+
src,
|
|
61
|
+
"data-video-provider": provider,
|
|
62
|
+
allowfullscreen: "true",
|
|
63
|
+
frameborder: "0",
|
|
64
|
+
}),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
return [
|
|
68
|
+
"video",
|
|
69
|
+
mergeAttributes({
|
|
70
|
+
src,
|
|
71
|
+
controls: "true",
|
|
72
|
+
preload: "metadata",
|
|
73
|
+
}),
|
|
74
|
+
];
|
|
75
|
+
},
|
|
76
|
+
addCommands() {
|
|
77
|
+
return {
|
|
78
|
+
setVideoEmbed: (attrs) => ({ state, dispatch }) => {
|
|
79
|
+
const type = state.schema.nodes.videoEmbed;
|
|
80
|
+
if (!type) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const node = type.create(attrs);
|
|
84
|
+
const $from = state.doc.resolve(state.selection.from);
|
|
85
|
+
const insertAt = $from.parent.inlineContent ? $from.after() : state.selection.to;
|
|
86
|
+
if (dispatch) {
|
|
87
|
+
const tr = state.tr.insert(insertAt, node);
|
|
88
|
+
const $after = tr.doc.resolve(Math.min(insertAt + node.nodeSize, tr.doc.content.size));
|
|
89
|
+
dispatch(tr.setSelection(TextSelection.near($after)).scrollIntoView());
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type VideoProvider = "file" | "youtube" | "bilibili";
|
|
2
|
+
export type VideoEmbedAttrs = {
|
|
3
|
+
provider: VideoProvider;
|
|
4
|
+
src: string;
|
|
5
|
+
};
|
|
6
|
+
/** 把用户粘贴的地址解析成可嵌入的视频节点属性 */
|
|
7
|
+
export declare const parseVideoUrl: (input: string) => VideoEmbedAttrs | null;
|
|
8
|
+
//# sourceMappingURL=video-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-url.d.ts","sourceRoot":"","sources":["../../../src/editor/video-embed/video-url.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;AAE5D,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,EAAE,aAAa,CAAC;IACxB,GAAG,EAAE,MAAM,CAAC;CACb,CAAC;AA0FF,4BAA4B;AAC5B,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,eAAe,GAAG,IAkB/D,CAAC"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const YOUTUBE_ID = /^[\w-]{11}$/;
|
|
2
|
+
const BVID = /^BV[\w]+$/i;
|
|
3
|
+
const httpUrl = (input) => {
|
|
4
|
+
const trimmed = input.trim();
|
|
5
|
+
if (!trimmed) {
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
let url;
|
|
9
|
+
try {
|
|
10
|
+
url = new URL(trimmed);
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return url;
|
|
19
|
+
};
|
|
20
|
+
const bareHost = (url) => {
|
|
21
|
+
return url.hostname.replace(/^www\./, "").toLowerCase();
|
|
22
|
+
};
|
|
23
|
+
const youtubeId = (url) => {
|
|
24
|
+
const host = bareHost(url);
|
|
25
|
+
if (host === "youtu.be") {
|
|
26
|
+
const id = url.pathname.split("/").filter(Boolean)[0];
|
|
27
|
+
return id && YOUTUBE_ID.test(id) ? id : null;
|
|
28
|
+
}
|
|
29
|
+
if (host !== "youtube.com" &&
|
|
30
|
+
host !== "m.youtube.com" &&
|
|
31
|
+
host !== "music.youtube.com" &&
|
|
32
|
+
host !== "youtube-nocookie.com") {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
if (url.pathname === "/watch") {
|
|
36
|
+
const id = url.searchParams.get("v");
|
|
37
|
+
return id && YOUTUBE_ID.test(id) ? id : null;
|
|
38
|
+
}
|
|
39
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
40
|
+
if ((parts[0] === "embed" || parts[0] === "shorts") && parts[1] && YOUTUBE_ID.test(parts[1])) {
|
|
41
|
+
return parts[1];
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
};
|
|
45
|
+
const bilibiliEmbed = (url) => {
|
|
46
|
+
const host = bareHost(url);
|
|
47
|
+
if (host === "bilibili.com" || host === "m.bilibili.com") {
|
|
48
|
+
const bv = url.pathname.match(/\/video\/(BV[\w]+)/i);
|
|
49
|
+
if (bv?.[1] && BVID.test(bv[1])) {
|
|
50
|
+
return {
|
|
51
|
+
provider: "bilibili",
|
|
52
|
+
src: `https://player.bilibili.com/player.html?bvid=${bv[1]}&autoplay=0`,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const av = url.pathname.match(/\/video\/av(\d+)/i);
|
|
56
|
+
if (av?.[1]) {
|
|
57
|
+
return {
|
|
58
|
+
provider: "bilibili",
|
|
59
|
+
src: `https://player.bilibili.com/player.html?aid=${av[1]}&autoplay=0`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
if (host !== "player.bilibili.com") {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const bvid = url.searchParams.get("bvid");
|
|
68
|
+
if (bvid && BVID.test(bvid)) {
|
|
69
|
+
return {
|
|
70
|
+
provider: "bilibili",
|
|
71
|
+
src: `https://player.bilibili.com/player.html?bvid=${bvid}&autoplay=0`,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
const aid = url.searchParams.get("aid");
|
|
75
|
+
if (aid && /^\d+$/.test(aid)) {
|
|
76
|
+
return {
|
|
77
|
+
provider: "bilibili",
|
|
78
|
+
src: `https://player.bilibili.com/player.html?aid=${aid}&autoplay=0`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
};
|
|
83
|
+
/** 把用户粘贴的地址解析成可嵌入的视频节点属性 */
|
|
84
|
+
export const parseVideoUrl = (input) => {
|
|
85
|
+
const url = httpUrl(input);
|
|
86
|
+
if (!url) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const youtube = youtubeId(url);
|
|
90
|
+
if (youtube) {
|
|
91
|
+
return { provider: "youtube", src: `https://www.youtube.com/embed/${youtube}` };
|
|
92
|
+
}
|
|
93
|
+
const bilibili = bilibiliEmbed(url);
|
|
94
|
+
if (bilibili) {
|
|
95
|
+
return bilibili;
|
|
96
|
+
}
|
|
97
|
+
const path = decodeURIComponent(url.pathname).toLowerCase();
|
|
98
|
+
if (path.endsWith(".mp4") || path.endsWith(".webm")) {
|
|
99
|
+
return { provider: "file", src: url.toString() };
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type FC } from "react";
|
|
2
|
+
interface VideoMenuProps {
|
|
3
|
+
open: boolean;
|
|
4
|
+
uploading: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
onOpenChange: (open: boolean) => void;
|
|
7
|
+
onUpload: () => void;
|
|
8
|
+
onSubmitUrl: (url: string) => void;
|
|
9
|
+
}
|
|
10
|
+
/** 工具栏视频菜单:上传文件或粘贴链接 */
|
|
11
|
+
export declare const VideoMenu: FC<VideoMenuProps>;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/editor/video-menu/index.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAsC,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEpE,UAAU,cAAc;IACtB,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAED,wBAAwB;AACxB,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAyExC,CAAC"}
|