feat: 品牌裁剪弹窗/Markdown 行内短代码与详情视频块等
新增: - BrandCropModal 品牌图裁剪弹窗 + cropImage 工具 - Markdown 行内短代码(inlineShortcodes + remark 插件) - MdDetails 折叠块、MdVideo 视频块组件 - directiveBlocks/inlineShortcodes 测试 其它: - 聊天后端(handler/service chat)调整 - Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
This commit is contained in:
@@ -34,6 +34,11 @@ import {
|
||||
Ellipsis,
|
||||
Smile,
|
||||
LayoutList,
|
||||
Highlighter,
|
||||
Keyboard,
|
||||
Palette,
|
||||
MonitorPlay,
|
||||
Pipette,
|
||||
} from "lucide-react";
|
||||
import { apiSearchPosts, apiUploadPostImage, type PostListItem } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
@@ -42,7 +47,8 @@ import ImageInsertModal from "@/components/ImageInsertModal";
|
||||
import TimelineEditor from "@/components/TimelineEditor";
|
||||
import { ENTRIES_TEMPLATE, ENTRIES_TEMPLATE_AUTO } from "@/lib/entryBlocks";
|
||||
import EmojiPicker from "@/components/EmojiPicker";
|
||||
import { resolveTimelineBlockForEdit } from "@/lib/directiveBlocks";
|
||||
import { resolveTimelineBlockForEdit, parseVideoInput } from "@/lib/directiveBlocks";
|
||||
import { isValidColorValue } from "@/lib/inlineShortcodes";
|
||||
import { SHIKI_LANG_IDS } from "@/components/markdown/shikiHighlighter";
|
||||
import { postPath } from "@/lib/urlStyle";
|
||||
import { useUrlStyle } from "@/components/SiteBrandProvider";
|
||||
@@ -96,7 +102,61 @@ type WrapOpts = {
|
||||
linePrefix?: string;
|
||||
};
|
||||
|
||||
type PopoverKind = "link" | "code" | "hide" | "heading" | "more" | "emoji" | null;
|
||||
type PopoverKind = "link" | "code" | "hide" | "heading" | "more" | "emoji" | "color" | "video" | null;
|
||||
|
||||
/** 文字颜色弹窗预设色板(均为合法短代码颜色值) */
|
||||
const COLOR_FG_PRESETS = [
|
||||
"#e03131", "#e8590c", "#f08c00", "#2f9e44", "#0c8599",
|
||||
"#1971c2", "#6741d9", "#c2255c", "#1f2328", "#5f6b7a",
|
||||
];
|
||||
const COLOR_BG_PRESETS = [
|
||||
"#fff3bf", "#ffe3e3", "#d3f9d8", "#d0ebff",
|
||||
"#e5dbff", "#ffd8a8", "#ffff8f", "#e9ecef",
|
||||
];
|
||||
|
||||
type HSV = { h: number; s: number; v: number };
|
||||
|
||||
/** #rgb/#rrggbb → HSV;非 hex(如颜色名)返回 null */
|
||||
function hexToHsv(hex: string): HSV | null {
|
||||
const m = /^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.exec(hex.trim());
|
||||
if (!m) return null;
|
||||
let x = m[1];
|
||||
if (x.length === 3) x = x.split("").map((c) => c + c).join("");
|
||||
const r = parseInt(x.slice(0, 2), 16) / 255;
|
||||
const g = parseInt(x.slice(2, 4), 16) / 255;
|
||||
const b = parseInt(x.slice(4, 6), 16) / 255;
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
const d = max - min;
|
||||
let h = 0;
|
||||
if (d !== 0) {
|
||||
if (max === r) h = ((g - b) / d) % 6;
|
||||
else if (max === g) h = (b - r) / d + 2;
|
||||
else h = (r - g) / d + 4;
|
||||
h *= 60;
|
||||
if (h < 0) h += 360;
|
||||
}
|
||||
return { h, s: max === 0 ? 0 : d / max, v: max };
|
||||
}
|
||||
|
||||
/** HSV → #rrggbb */
|
||||
function hsvToHex({ h, s, v }: HSV): string {
|
||||
const c = v * s;
|
||||
const x = c * (1 - Math.abs(((h / 60) % 2) - 1));
|
||||
const m = v - c;
|
||||
let r = 0, g = 0, b = 0;
|
||||
if (h < 60) [r, g, b] = [c, x, 0];
|
||||
else if (h < 120) [r, g, b] = [x, c, 0];
|
||||
else if (h < 180) [r, g, b] = [0, c, x];
|
||||
else if (h < 240) [r, g, b] = [0, x, c];
|
||||
else if (h < 300) [r, g, b] = [x, 0, c];
|
||||
else [r, g, b] = [c, 0, x];
|
||||
const to = (n: number) =>
|
||||
Math.max(0, Math.min(255, Math.round((n + m) * 255)))
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
return `#${to(r)}${to(g)}${to(b)}`;
|
||||
}
|
||||
|
||||
const CODE_LANGS = ["plaintext", "aardio", ...SHIKI_LANG_IDS.filter((l) => l !== "plaintext")] as const;
|
||||
|
||||
@@ -190,6 +250,16 @@ export default function MarkdownEditor({
|
||||
const [hidePoints, setHidePoints] = useState(10);
|
||||
const [hidePassword, setHidePassword] = useState("");
|
||||
const [hideStep, setHideStep] = useState<"menu" | "points" | "password">("menu");
|
||||
/** 颜色弹窗:前景/背景独立选择,可同时设置;null=不设置该侧 */
|
||||
const [colorFg, setColorFg] = useState<string | null>("#e03131");
|
||||
const [colorBg, setColorBg] = useState<string | null>(null);
|
||||
/** 色轮/自定义输入框的当前值(始终 #hex,供 HSV 取色器使用) */
|
||||
const [colorFgWheel, setColorFgWheel] = useState("#e03131");
|
||||
const [colorBgWheel, setColorBgWheel] = useState("#fff3bf");
|
||||
/** 内联 HSV 取色器展开侧;打开期间 colorHsv 为该侧当前色 */
|
||||
const [activeWheel, setActiveWheel] = useState<"fg" | "bg" | null>(null);
|
||||
const [colorHsv, setColorHsv] = useState<HSV>({ h: 0, s: 1, v: 1 });
|
||||
const [videoInput, setVideoInput] = useState("");
|
||||
const [toolNarrow, setToolNarrow] = useState(false);
|
||||
const [uploadError, setUploadError] = useState("");
|
||||
const [modKey, setModKey] = useState("Ctrl");
|
||||
@@ -504,6 +574,14 @@ export default function MarkdownEditor({
|
||||
setLinkAdvanced(false);
|
||||
}
|
||||
if (kind === "code") setCodeQuery("");
|
||||
if (kind === "color") {
|
||||
setColorFg("#e03131");
|
||||
setColorBg(null);
|
||||
setColorFgWheel("#e03131");
|
||||
setColorBgWheel("#fff3bf");
|
||||
setActiveWheel(null);
|
||||
}
|
||||
if (kind === "video") setVideoInput("");
|
||||
if (kind === "hide") {
|
||||
setHideStep("menu");
|
||||
setHidePoints(10);
|
||||
@@ -614,6 +692,176 @@ export default function MarkdownEditor({
|
||||
setPopover(null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 颜色短代码插入:前景/背景可独立或组合。
|
||||
* 两侧都有 → [color=前景 bg=背景]…[/color];仅一侧 → [color] / [bg]。
|
||||
*/
|
||||
const applyColor = () => {
|
||||
const fg = colorFg?.trim();
|
||||
const bg = colorBg?.trim();
|
||||
if (!fg && !bg) {
|
||||
toast("请至少选择前景色或背景色", "error");
|
||||
return;
|
||||
}
|
||||
if ((fg && !isValidColorValue(fg)) || (bg && !isValidColorValue(bg))) {
|
||||
toast("颜色仅支持 #RGB/#RRGGBB/#RRGGBBAA 或英文颜色名", "error");
|
||||
return;
|
||||
}
|
||||
restoreSel();
|
||||
const el = taRef.current;
|
||||
const start = el?.selectionStart ?? 0;
|
||||
const end = el?.selectionEnd ?? 0;
|
||||
const selected = value.slice(start, end);
|
||||
const body = selected || "彩色文字";
|
||||
let md: string;
|
||||
if (fg && bg) md = `[color=${fg} bg=${bg}]${body}[/color]`;
|
||||
else if (fg) md = `[color=${fg}]${body}[/color]`;
|
||||
else md = `[bg=${bg}]${body}[/bg]`;
|
||||
const next = value.slice(0, start) + md + value.slice(end);
|
||||
onChange(next);
|
||||
requestAnimationFrame(() => {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const caret = start + md.length;
|
||||
el.setSelectionRange(caret, caret);
|
||||
});
|
||||
setPopover(null);
|
||||
};
|
||||
|
||||
/** 内联 HSV 取色器开关:打开时以该侧当前 hex(或上次色轮值)初始化 */
|
||||
const toggleColorWheel = (side: "fg" | "bg") => {
|
||||
if (activeWheel === side) {
|
||||
setActiveWheel(null);
|
||||
return;
|
||||
}
|
||||
const wheelHex = side === "fg" ? colorFgWheel : colorBgWheel;
|
||||
setColorHsv(hexToHsv(wheelHex) ?? { h: 0, s: 1, v: 1 });
|
||||
setActiveWheel(side);
|
||||
};
|
||||
|
||||
/** HSV 拖动 → 同步该侧 wheel hex 与选中色 */
|
||||
const commitHsv = (next: HSV) => {
|
||||
setColorHsv(next);
|
||||
const hex = hsvToHex(next);
|
||||
if (activeWheel === "fg") {
|
||||
setColorFgWheel(hex);
|
||||
setColorFg(hex);
|
||||
} else if (activeWheel === "bg") {
|
||||
setColorBgWheel(hex);
|
||||
setColorBg(hex);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 饱和度/明度面板拖拽:坐标以真实渲染元素 rect 映射(pointer capture 保证移出仍跟手)。
|
||||
* 注意:必须在同步阶段取出 currentTarget 存入 el——React 在事件派发结束后会把
|
||||
* e.currentTarget 置空,延迟回调里再读会拿到 null 导致拖拽中断。
|
||||
*/
|
||||
const onSvPointer = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const el = e.currentTarget;
|
||||
el.setPointerCapture(e.pointerId);
|
||||
const update = (clientX: number, clientY: number) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const s = Math.max(0, Math.min(1, (clientX - rect.left) / rect.width));
|
||||
const v = 1 - Math.max(0, Math.min(1, (clientY - rect.top) / rect.height));
|
||||
setColorHsv((prev) => {
|
||||
const next = { ...prev, s, v };
|
||||
const hex = hsvToHex(next);
|
||||
if (activeWheel === "fg") {
|
||||
setColorFgWheel(hex);
|
||||
setColorFg(hex);
|
||||
} else if (activeWheel === "bg") {
|
||||
setColorBgWheel(hex);
|
||||
setColorBg(hex);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
update(e.clientX, e.clientY);
|
||||
const onMove = (ev: PointerEvent) => update(ev.clientX, ev.clientY);
|
||||
const cleanup = () => {
|
||||
el.removeEventListener("pointermove", onMove);
|
||||
el.removeEventListener("pointerup", cleanup);
|
||||
el.removeEventListener("pointercancel", cleanup);
|
||||
};
|
||||
el.addEventListener("pointermove", onMove);
|
||||
el.addEventListener("pointerup", cleanup);
|
||||
el.addEventListener("pointercancel", cleanup);
|
||||
};
|
||||
|
||||
/** 色相条拖拽(水平);同样在同步阶段缓存元素 */
|
||||
const onHuePointer = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
const el = e.currentTarget;
|
||||
el.setPointerCapture(e.pointerId);
|
||||
const update = (clientX: number) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const h = Math.max(0, Math.min(360, ((clientX - rect.left) / rect.width) * 360));
|
||||
commitHsv({ ...colorHsv, h });
|
||||
};
|
||||
update(e.clientX);
|
||||
const onMove = (ev: PointerEvent) => update(ev.clientX);
|
||||
const cleanup = () => {
|
||||
el.removeEventListener("pointermove", onMove);
|
||||
el.removeEventListener("pointerup", cleanup);
|
||||
el.removeEventListener("pointercancel", cleanup);
|
||||
};
|
||||
el.addEventListener("pointermove", onMove);
|
||||
el.addEventListener("pointerup", cleanup);
|
||||
el.addEventListener("pointercancel", cleanup);
|
||||
};
|
||||
|
||||
/** [bilibili]/[youtube] 整行插入:表单裸输入统一走 parseVideoInput */
|
||||
const applyVideo = () => {
|
||||
const parsed = parseVideoInput(videoInput);
|
||||
if (!parsed) {
|
||||
toast("请输入 B站 BV 号/链接 或 YouTube 视频 ID/链接", "error");
|
||||
return;
|
||||
}
|
||||
restoreSel();
|
||||
const el = taRef.current;
|
||||
const start = el?.selectionStart ?? 0;
|
||||
const end = el?.selectionEnd ?? 0;
|
||||
const tag = `[${parsed.provider}]${parsed.id}[/${parsed.provider}]`;
|
||||
const padBefore = start > 0 && value[start - 1] !== "\n" ? "\n" : "";
|
||||
const padAfter = end < value.length && value[end] !== "\n" ? "\n" : "";
|
||||
const inserted = padBefore + tag + padAfter;
|
||||
const next = value.slice(0, start) + inserted + value.slice(end);
|
||||
onChange(next);
|
||||
requestAnimationFrame(() => {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const caret = start + inserted.length;
|
||||
el.setSelectionRange(caret, caret);
|
||||
});
|
||||
setPopover(null);
|
||||
};
|
||||
|
||||
/** [details 标题]…[/details] 折叠面板:包住选区或占位正文 */
|
||||
const insertDetails = () => {
|
||||
restoreSel();
|
||||
const el = taRef.current;
|
||||
const start = el?.selectionStart ?? 0;
|
||||
const end = el?.selectionEnd ?? 0;
|
||||
const selected = value.slice(start, end);
|
||||
const body = selected || "折叠内容";
|
||||
const open = "[details 点击展开]";
|
||||
const block = `${open}\n${body}\n[/details]`;
|
||||
const padBefore = start > 0 && value[start - 1] !== "\n" ? "\n" : "";
|
||||
const padAfter = end < value.length && value[end] !== "\n" ? "\n" : "";
|
||||
const inserted = padBefore + block + padAfter;
|
||||
const next = value.slice(0, start) + inserted + value.slice(end);
|
||||
onChange(next);
|
||||
requestAnimationFrame(() => {
|
||||
if (!el) return;
|
||||
el.focus();
|
||||
const bodyStart = start + padBefore.length + open.length + 1;
|
||||
el.setSelectionRange(bodyStart, bodyStart + body.length);
|
||||
});
|
||||
setPopover(null);
|
||||
};
|
||||
|
||||
const openTimelineEditor = () => {
|
||||
rememberSel();
|
||||
const el = taRef.current;
|
||||
@@ -923,6 +1171,164 @@ export default function MarkdownEditor({
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// 文字颜色弹层:前景/背景两行独立选择,可同时设置;每行含预设、内联 HSV 取色器与自定义输入。
|
||||
// 用普通函数调用而非 <ColorPickRow/> 组件:避免内联组件每次渲染重挂载导致输入框失焦。
|
||||
const renderColorRow = (
|
||||
side: "fg" | "bg",
|
||||
label: string,
|
||||
presets: readonly string[],
|
||||
picked: string | null
|
||||
) => {
|
||||
const setPicked = (v: string | null) =>
|
||||
side === "fg" ? setColorFg(v) : setColorBg(v);
|
||||
const wheelOpen = activeWheel === side;
|
||||
return (
|
||||
<div className="j13-md-color-row">
|
||||
<span className="j13-md-color-row-label">{label}</span>
|
||||
<div className="j13-md-color-row-items">
|
||||
<button
|
||||
type="button"
|
||||
className={`j13-md-color-none${picked === null ? " is-active" : ""}`}
|
||||
onClick={() => setPicked(null)}
|
||||
>
|
||||
无
|
||||
</button>
|
||||
{presets.map((c) => (
|
||||
<button
|
||||
key={c}
|
||||
type="button"
|
||||
className={`j13-md-color-dot${picked?.toLowerCase() === c.toLowerCase() ? " is-active" : ""}`}
|
||||
style={{ background: c }}
|
||||
data-tip={c}
|
||||
aria-label={`使用 ${c}`}
|
||||
onClick={() => setPicked(c)}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
className={`j13-md-color-wheel${wheelOpen ? " is-active" : ""}`}
|
||||
data-tip="自定义颜色"
|
||||
aria-label="自定义颜色"
|
||||
aria-expanded={wheelOpen}
|
||||
onClick={() => toggleColorWheel(side)}
|
||||
>
|
||||
<Pipette size={12} />
|
||||
</button>
|
||||
<input
|
||||
className="j13-md-color-hex j13-md-popover-input"
|
||||
value={picked ?? ""}
|
||||
placeholder="#e03131 或 tomato"
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
setPicked(v);
|
||||
if (/^#[0-9a-fA-F]{6}$/.test(v)) {
|
||||
if (side === "fg") setColorFgWheel(v);
|
||||
else setColorBgWheel(v);
|
||||
if (wheelOpen) setColorHsv(hexToHsv(v) ?? colorHsv);
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
applyColor();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{wheelOpen && (
|
||||
<div className="j13-md-hsv">
|
||||
<div
|
||||
className="j13-md-hsv-sv"
|
||||
style={{
|
||||
background: `linear-gradient(to top, #000, rgba(0,0,0,0)), linear-gradient(to right, #fff, ${hsvToHex({ h: colorHsv.h, s: 1, v: 1 })})`,
|
||||
}}
|
||||
onPointerDown={onSvPointer}
|
||||
>
|
||||
<span
|
||||
className="j13-md-hsv-sv-thumb"
|
||||
style={{
|
||||
left: `${colorHsv.s * 100}%`,
|
||||
top: `${(1 - colorHsv.v) * 100}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="j13-md-hsv-hue" onPointerDown={onHuePointer}>
|
||||
<span
|
||||
className="j13-md-hsv-hue-thumb"
|
||||
style={{ left: `${(colorHsv.h / 360) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const colorPopover =
|
||||
popover === "color" ? (
|
||||
<div
|
||||
className={`j13-md-popover j13-md-popover-form j13-md-color-popover${toolNarrow ? " j13-md-popover-center" : ""}`}
|
||||
role="dialog"
|
||||
aria-label="文字颜色"
|
||||
>
|
||||
<div className="j13-md-color-preview">
|
||||
<span
|
||||
style={{
|
||||
color: colorFg ?? "inherit",
|
||||
backgroundColor: colorBg ?? "transparent",
|
||||
}}
|
||||
>
|
||||
Aa 示例文字
|
||||
</span>
|
||||
</div>
|
||||
{renderColorRow("fg", "前景", COLOR_FG_PRESETS, colorFg)}
|
||||
{renderColorRow("bg", "背景", COLOR_BG_PRESETS, colorBg)}
|
||||
<div className="j13-md-popover-footer">
|
||||
<button type="button" className="btn btn-line btn-sm" onClick={() => setPopover(null)}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={() => applyColor()}>
|
||||
插入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
// 视频嵌入弹层:裸输入统一交给 parseVideoInput 归一化
|
||||
const videoPopover =
|
||||
popover === "video" ? (
|
||||
<div
|
||||
className={`j13-md-popover j13-md-popover-form${toolNarrow ? " j13-md-popover-center" : ""}`}
|
||||
role="dialog"
|
||||
aria-label="插入视频"
|
||||
>
|
||||
<label className="j13-md-field">
|
||||
<span>B站 / YouTube</span>
|
||||
<input
|
||||
autoFocus
|
||||
value={videoInput}
|
||||
onChange={(e) => setVideoInput(e.target.value)}
|
||||
className="j13-md-popover-input"
|
||||
placeholder="BV1… / 视频链接 / YouTube ID"
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
applyVideo();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<div className="j13-md-popover-footer">
|
||||
<button type="button" className="btn btn-line btn-sm" onClick={() => setPopover(null)}>
|
||||
取消
|
||||
</button>
|
||||
<button type="button" className="btn btn-primary btn-sm" onClick={applyVideo}>
|
||||
插入
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const ToolBtn = ({
|
||||
label,
|
||||
icon,
|
||||
@@ -981,6 +1387,31 @@ export default function MarkdownEditor({
|
||||
onClick={() => runWrap({ before: "~~", after: "~~", placeholder: "删除线" })}
|
||||
/>
|
||||
)}
|
||||
{!isComment && !toolNarrow && (
|
||||
<ToolBtn
|
||||
label="高亮 =="
|
||||
icon={<Highlighter size={15} />}
|
||||
onClick={() => runWrap({ before: "==", after: "==", placeholder: "高亮" })}
|
||||
/>
|
||||
)}
|
||||
{!isComment && !toolNarrow && (
|
||||
<ToolBtn
|
||||
label="键帽"
|
||||
icon={<Keyboard size={15} />}
|
||||
onClick={() => runWrap({ before: "[kbd]", after: "[/kbd]", placeholder: "Ctrl" })}
|
||||
/>
|
||||
)}
|
||||
{!toolNarrow && (
|
||||
<div className="j13-md-popover-anchor relative">
|
||||
<ToolBtn
|
||||
label="文字颜色"
|
||||
icon={<Palette size={15} />}
|
||||
active={popover === "color"}
|
||||
onClick={() => openPopover("color")}
|
||||
/>
|
||||
{popover === "color" && colorPopover}
|
||||
</div>
|
||||
)}
|
||||
{!isComment && !toolNarrow && <Sep />}
|
||||
{!isComment && (
|
||||
<div className="j13-md-popover-anchor relative">
|
||||
@@ -1217,6 +1648,9 @@ export default function MarkdownEditor({
|
||||
)}
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { runWrap({ before: "*", after: "*", placeholder: "斜体" }); setPopover(null); }}>斜体</button>
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { runWrap({ before: "~~", after: "~~", placeholder: "删除线" }); setPopover(null); }}>删除线</button>
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { runWrap({ before: "==", after: "==", placeholder: "高亮" }); setPopover(null); }}>高亮</button>
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { runWrap({ before: "[kbd]", after: "[/kbd]", placeholder: "Ctrl" }); setPopover(null); }}>键帽</button>
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => openPopover("color")}>颜色…</button>
|
||||
{!isComment && (
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => openPopover("emoji")}>表情</button>
|
||||
)}
|
||||
@@ -1230,12 +1664,20 @@ export default function MarkdownEditor({
|
||||
)}
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { setPopover(null); openTimelineEditor(); }}>时间线</button>
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { insertAtCursor(entriesAuto ? ENTRIES_TEMPLATE_AUTO : ENTRIES_TEMPLATE); setPopover(null); }}>条目卡</button>
|
||||
{!isComment && (
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => insertDetails()}>折叠面板</button>
|
||||
)}
|
||||
{!isComment && (
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => openPopover("video")}>视频…</button>
|
||||
)}
|
||||
{toolNarrow && (
|
||||
<button type="button" role="menuitem" className="j13-md-menu-item" onMouseDown={(e) => e.preventDefault()} onClick={() => { setPopover(null); toggleFullscreen(); }}>{fullscreen ? "退出全屏" : "全屏写作"}</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{toolNarrow && codePopover}
|
||||
{toolNarrow && popover === "color" && colorPopover}
|
||||
{toolNarrow && popover === "video" && videoPopover}
|
||||
{toolNarrow && popover === "emoji" && !isComment && (
|
||||
<div className="j13-md-popover j13-md-emoji-popover j13-md-popover-center" role="dialog" aria-label="插入表情">
|
||||
<EmojiPicker onPick={pickEmoji} />
|
||||
@@ -1259,6 +1701,17 @@ export default function MarkdownEditor({
|
||||
onClick={() => insertAtCursor(entriesAuto ? ENTRIES_TEMPLATE_AUTO : ENTRIES_TEMPLATE)}
|
||||
/>
|
||||
)}
|
||||
{!isComment && !toolNarrow && (
|
||||
<div className="j13-md-popover-anchor relative">
|
||||
<ToolBtn
|
||||
label="视频"
|
||||
icon={<MonitorPlay size={15} />}
|
||||
active={popover === "video"}
|
||||
onClick={() => openPopover("video")}
|
||||
/>
|
||||
{popover === "video" && videoPopover}
|
||||
</div>
|
||||
)}
|
||||
{allowHide && !toolNarrow ? (
|
||||
<div className="j13-md-popover-anchor relative">
|
||||
<ToolBtn
|
||||
|
||||
Reference in New Issue
Block a user