Files
blweb/src/components/admin/MarkdownToolbar.jsx

174 lines
5.5 KiB
React
Raw Normal View History

import { useState, useRef } from 'react';
import styles from './MarkdownToolbar.module.css';
const TOOLS = [
{ label: 'B', title: '加粗', wrap: (s) => `<strong>${s}</strong>` },
{ label: 'I', title: '斜体', wrap: (s) => `<em>${s}</em>` },
{ label: 'H2', title: '二级标题', wrap: (s) => `<h2>${s}</h2>` },
{ label: '❝', title: '引用', wrap: (s) => `<blockquote>${s}</blockquote>` },
{ label: '</>', title: '代码', wrap: (s) => `<code>${s}</code>` },
];
export default function MarkdownToolbar({ textareaRef }) {
const [imgPopover, setImgPopover] = useState(false);
const [imgUrl, setImgUrl] = useState('');
const [imgAlt, setImgAlt] = useState('');
const [uploading, setUploading] = useState(false);
const fileInputRef = useRef(null);
const insertAt = (html) => {
const ta = textareaRef.current;
if (!ta) return;
const pos = ta.selectionEnd;
const before = ta.value.slice(0, pos);
const after = ta.value.slice(pos);
const next = before + '\n' + html + '\n' + after;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(ta, next);
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.focus();
};
const apply = (wrapFn) => {
const ta = textareaRef.current;
if (!ta) return;
const start = ta.selectionStart;
const end = ta.selectionEnd;
const selected = ta.value.slice(start, end) || '内容';
const wrapped = wrapFn(selected);
const before = ta.value.slice(0, start);
const after = ta.value.slice(end);
const next = before + wrapped + after;
const setter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set;
setter.call(ta, next);
ta.dispatchEvent(new Event('input', { bubbles: true }));
ta.focus();
ta.setSelectionRange(start + wrapped.length, start + wrapped.length);
};
const insertImgUrl = () => {
if (!imgUrl.trim()) return;
const alt = imgAlt.trim() || '图片';
insertAt(`<img src="${imgUrl.trim()}" alt="${alt}" />`);
setImgUrl('');
setImgAlt('');
setImgPopover(false);
};
const handleFileChange = (e) => {
const file = e.target.files[0];
if (!file) return;
if (file.size > 2 * 1024 * 1024) {
alert('图片文件不能超过 2MBbase64 会使文章体积变大)');
return;
}
setUploading(true);
const reader = new FileReader();
reader.onload = (ev) => {
const src = ev.target.result;
const alt = file.name.replace(/\.[^.]+$/, '') || '图片';
insertAt(`<img src="${src}" alt="${alt}" />`);
setUploading(false);
setImgPopover(false);
e.target.value = '';
};
reader.readAsDataURL(file);
};
return (
<div className={styles.toolbarWrap}>
<div className={styles.toolbar}>
{TOOLS.map(t => (
<button
key={t.label}
type="button"
className={styles.tool}
title={t.title}
onMouseDown={(e) => { e.preventDefault(); apply(t.wrap); }}
>
{t.label}
</button>
))}
<div className={styles.separator} />
<button
type="button"
className={styles.tool}
title="插入图片"
onMouseDown={(e) => { e.preventDefault(); setImgPopover(v => !v); }}
>
🖼
</button>
</div>
{imgPopover && (
<div className={`${styles.popover} glass-card`}>
<p className={styles.popoverTitle}>插入图片</p>
<div className={styles.popoverSection}>
<label className={styles.popoverLabel}>图片 URL</label>
<div className={styles.urlRow}>
<input
className={styles.popoverInput}
value={imgUrl}
onChange={e => setImgUrl(e.target.value)}
onKeyDown={e => e.key === 'Enter' && insertImgUrl()}
placeholder="https://example.com/image.jpg"
autoFocus
/>
<button
type="button"
className="btn btn--primary"
onClick={insertImgUrl}
disabled={!imgUrl.trim()}
>
插入
</button>
</div>
<input
className={styles.popoverInput}
value={imgAlt}
onChange={e => setImgAlt(e.target.value)}
placeholder="图片描述alt可选"
style={{ marginTop: 6 }}
/>
</div>
<div className={styles.divider}>
<span></span>
</div>
<div className={styles.popoverSection}>
<label className={styles.popoverLabel}>上传本地图片 2MB base64</label>
<input
ref={fileInputRef}
type="file"
accept="image/*"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
type="button"
className="btn"
onClick={() => fileInputRef.current?.click()}
disabled={uploading}
style={{ width: '100%', justifyContent: 'center' }}
>
{uploading ? '处理中…' : '选择图片文件'}
</button>
</div>
<button
type="button"
className={styles.popoverClose}
onClick={() => setImgPopover(false)}
>
取消
</button>
</div>
)}
</div>
);
}