feat(creator-studio): dialog-driven table/math/footnote, theme quote, code language picker
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
7c33482be7
commit
7885f42cff
14 files changed
+1005
-54
No files matched your search
@@ -18,7 +18,19 @@ import { SaveState, type SaveStateValue } from './components/save-state';
|
||||
import { useAutosave } from './hooks/use-autosave';
|
||||
import { useLocalRecovery } from './hooks/use-local-recovery';
|
||||
import { normalizePayForRequest } from './utils/pay';
|
||||
import { codeBlockMarkup, dividerMarkup, footnoteMarkup, listMarkup, mathMarkup, tableMarkup, tocMarkup } from './utils/block-markup';
|
||||
import { EDITOR_CONTENT_STYLE } from './editor-content-style';
|
||||
import {
|
||||
codeBlockMarkup,
|
||||
dividerMarkup,
|
||||
footnoteMarkup,
|
||||
listMarkup,
|
||||
mathMarkup,
|
||||
quoteMarkup,
|
||||
tableMarkup,
|
||||
tocMarkup,
|
||||
type QuoteColor,
|
||||
} from './utils/block-markup';
|
||||
import { BlockDialog, type BlockDialogKind, type BlockDialogResult } from './components/block-dialog';
|
||||
import './styles/studio.scss';
|
||||
|
||||
const bootstrap: Record<string, any> = window['jioCreatorStudio'] || {};
|
||||
@@ -132,6 +144,9 @@ function Studio(): JSX.Element {
|
||||
at: '',
|
||||
}));
|
||||
const [drawerOpen, setDrawerOpen] = useState(false);
|
||||
const [blockDialog, setBlockDialog] = useState<BlockDialogKind | null>(null);
|
||||
const [codeDialog, setCodeDialog] = useState<null | { language: string; code: string }>(null);
|
||||
const codeTargetRef = useRef<any>(null);
|
||||
const [notice, setNotice] = useState<{ tone: 'ok' | 'error'; text: string } | null>(null);
|
||||
const [recovery, setRecovery] = useState<null | { savedAt: string }>(null);
|
||||
const dirtyRef = useRef(false);
|
||||
@@ -384,7 +399,7 @@ function Studio(): JSX.Element {
|
||||
wpautop: true,
|
||||
body_class: 'jio-creator__tinymce',
|
||||
content_css: bootstrap.creator?.contentCss || '',
|
||||
content_style: 'body{margin:0;color:#243330;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans SC","PingFang SC","Microsoft YaHei",sans-serif;font-size:16px;line-height:1.85;letter-spacing:.01em}p{margin:0 0 1.1em}h1,h2,h3,h4,h5,h6{margin:1.5em 0 .65em;line-height:1.4}img,iframe{max-width:100%}img{height:auto}',
|
||||
content_style: EDITOR_CONTENT_STYLE,
|
||||
height: 650,
|
||||
placeholder: String(bootstrap.limits?.contentPlaceholder || ''),
|
||||
setup: (editor: any) => {
|
||||
@@ -400,6 +415,24 @@ function Studio(): JSX.Element {
|
||||
updateFormatValue(editor);
|
||||
});
|
||||
editor.on('NodeChange', () => updateFormatValue(editor));
|
||||
// 代码块右上角的语言标签:点击整块即可换语言,等价于公众号的「高亮」入口。
|
||||
editor.on('click', (event: any) => {
|
||||
const pre = editor.dom.getParent(event.target, 'pre.enlighter-pre');
|
||||
if (!pre) {
|
||||
return;
|
||||
}
|
||||
// 只响应右上角语言标签那一片,光标点进代码正文时保持正常编辑。
|
||||
const rect = pre.getBoundingClientRect();
|
||||
const relativeY = Number(event.clientY) - rect.top;
|
||||
const relativeX = Number(event.clientX) - rect.left;
|
||||
if (relativeY > 40 || relativeX < rect.width - 120) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
const language = String(pre.getAttribute('data-enlighter-language') || 'generic');
|
||||
codeTargetRef.current = pre;
|
||||
setCodeDialog({ language, code: String((pre.textContent || '').trim()) });
|
||||
});
|
||||
editor.on('input change Undo Redo AddUndo', () => {
|
||||
updateContent(editor.getContent());
|
||||
updateHistoryAvailability(editor);
|
||||
@@ -508,22 +541,9 @@ function Studio(): JSX.Element {
|
||||
}
|
||||
}, [focusEditor]);
|
||||
|
||||
const insertBlock = useCallback((kind: string) => {
|
||||
const insertHtml = useCallback((html: string) => {
|
||||
const editor = focusEditor();
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const markup: Record<string, string> = {
|
||||
list: listMarkup(false),
|
||||
toc: tocMarkup(),
|
||||
divider: dividerMarkup(),
|
||||
code: codeBlockMarkup(),
|
||||
footnote: footnoteMarkup(),
|
||||
math: mathMarkup(),
|
||||
table: tableMarkup(),
|
||||
};
|
||||
const html = markup[kind];
|
||||
if (!html) {
|
||||
if (!editor || !html) {
|
||||
return;
|
||||
}
|
||||
editor.insertContent(html);
|
||||
@@ -532,6 +552,60 @@ function Studio(): JSX.Element {
|
||||
updateHistoryAvailability(editor);
|
||||
}, [focusEditor]);
|
||||
|
||||
// 直接插入、无需参数的小块;需要参数的块打开通用弹窗。
|
||||
const insertBlock = useCallback((kind: string) => {
|
||||
rememberSelection();
|
||||
// code 走「语言选择」弹窗,其余三类走通用插入弹窗。
|
||||
if (kind === 'table' || kind === 'math' || kind === 'footnote') {
|
||||
setBlockDialog(kind as BlockDialogKind);
|
||||
return;
|
||||
}
|
||||
if (kind === 'code') {
|
||||
setCodeDialog({ language: 'generic', code: '' });
|
||||
return;
|
||||
}
|
||||
if (kind === 'list') {
|
||||
insertHtml(listMarkup(false));
|
||||
return;
|
||||
}
|
||||
if (kind === 'toc') {
|
||||
insertHtml(tocMarkup());
|
||||
return;
|
||||
}
|
||||
if (kind === 'divider') {
|
||||
insertHtml(dividerMarkup());
|
||||
}
|
||||
}, [insertHtml, rememberSelection]);
|
||||
|
||||
const insertQuote = useCallback((colorClass: QuoteColor) => {
|
||||
const editor = focusEditor();
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
const selected = String(editor.selection.getContent() || '').trim();
|
||||
const existing = editor.dom.getParent(editor.selection.getNode(), '.quote_q');
|
||||
if (existing) {
|
||||
existing.className = `quote_q quote-mce${colorClass ? ` ${colorClass}` : ''}`;
|
||||
editor.nodeChanged();
|
||||
return;
|
||||
}
|
||||
insertHtml(quoteMarkup(selected, colorClass));
|
||||
}, [focusEditor, insertHtml]);
|
||||
|
||||
const applyBlockDialog = useCallback((result: BlockDialogResult) => {
|
||||
setBlockDialog(null);
|
||||
// 弹窗抢走焦点后,插入前把光标放回正文的原选区。
|
||||
if (result.kind === 'table') {
|
||||
insertHtml(tableMarkup(result));
|
||||
} else if (result.kind === 'math') {
|
||||
insertHtml(mathMarkup(result));
|
||||
} else if (result.kind === 'footnote') {
|
||||
insertHtml(footnoteMarkup(result.note));
|
||||
} else {
|
||||
insertHtml(codeBlockMarkup(result.language, result.code));
|
||||
}
|
||||
}, [insertHtml]);
|
||||
|
||||
const openMedia = useCallback((type: 'image' | 'video' | 'file') => {
|
||||
const editor = editorRef.current;
|
||||
const media = (window as any).JioSubmitMedia;
|
||||
@@ -608,6 +682,7 @@ function Studio(): JSX.Element {
|
||||
onOpenLink={openLink}
|
||||
onOpenMedia={openMedia}
|
||||
onInsertBlock={insertBlock}
|
||||
onInsertQuote={insertQuote}
|
||||
/>
|
||||
<div className="jio-creator__bar-actions jio-creator__toolbar-extension" data-toolbar-extension aria-label={__('工具扩展区', 'jio')}>
|
||||
<SaveState value={saveState} />
|
||||
@@ -676,6 +751,43 @@ function Studio(): JSX.Element {
|
||||
onScheduleChange={(enabled, at) => patchDraft({ schedule: { enabled, at } })}
|
||||
/>
|
||||
|
||||
{blockDialog && (
|
||||
<BlockDialog
|
||||
kind={blockDialog}
|
||||
onSubmit={applyBlockDialog}
|
||||
onClose={() => setBlockDialog(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{codeDialog && (
|
||||
<BlockDialog
|
||||
kind="code"
|
||||
initialLanguage={codeDialog.language}
|
||||
initialCode={codeDialog.code}
|
||||
onSubmit={(result) => {
|
||||
setCodeDialog(null);
|
||||
if (result.kind !== 'code') {
|
||||
return;
|
||||
}
|
||||
const previous = codeTargetRef.current;
|
||||
codeTargetRef.current = null;
|
||||
insertHtml(codeBlockMarkup(result.language, result.code));
|
||||
if (previous) {
|
||||
// 换语言时移除旧块,避免留下两份代码。
|
||||
const editor = editorRef.current;
|
||||
if (editor && !editor.removed) {
|
||||
editor.dom.remove(previous);
|
||||
updateContent(editor.getContent());
|
||||
}
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setCodeDialog(null);
|
||||
codeTargetRef.current = null;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{drawerOpen && (
|
||||
<PublishDrawer
|
||||
draft={draft}
|
||||
|
||||
Reference in new issue
Block a user