Files
wordpress-theme-aksrt/assets/creator-studio/src/components/editor-toolbar.tsx
T
kate522 c5bdbc0457 feat(creator-studio): portal dropdowns, hidden content, two-step block delete
Toolbar
- New ToolMenu renders menus through a portal on body, so they are no longer
  clipped by the horizontally scrolling toolbar and no longer fall back to a
  native <select>. Triggers reuse .jio-creator__tool-btn geometry, fixing the
  heading button's mismatched height/size/position.
- Heading / list / align / quote / hidden-content are all dropdowns.
- Lists: unordered + ordered via InsertOrderedList/InsertUnorderedList.
  The TOC button is removed (it also used the wrong icon).
- Adds left / center / right alignment.
- Adds hidden content: comment / login / VIP / paid, wired to the existing
  [reply], [jio_hide mode="login"], [vip] and [payshow] shortcodes.

Editor content
- Quote blocks now match the live article: full .quote_q structure, four
  colour variables, and an SVG-mask quote mark that does not depend on the
  Font Awesome font being present inside the iframe.
- Block math is centred on a transparent background instead of a full-width
  grey bar.
- Code blocks are plain <pre><code>, identical to the article page; language
  and theme selection is gone and EnlighterJS handles highlighting globally.
  The classic submit editor's code modal follows the same shape.
- Hidden content reuses the .tinymce-hide structure and shows a readable label
  while keeping the shortcode text in the DOM for saving.

Block deletion
- Two-step delete, WPS style: the first click only selects and highlights the
  block, the second click removes it. Clicking elsewhere or Esc cancels.

Contracts updated and extended; 884 widget-system tests pass.
2026-11-09 15:03:50 +08:00

251 lines
9.0 KiB
TypeScript

/**
* 顶栏第二行里的正文工具条。
*
* 每个按钮都是「上图标 + 下文字」,与公众号/知乎的单行工具栏一致;
* 只渲染当前账号真正有权限使用的能力,全部动作由父组件转给 TinyMCE。
*
* 需要多选项的工具(标题级别、列表、引用配色、隐藏内容)统一走 ToolMenu,
* 避免原生 <select> 的风格断裂和滚动容器裁剪。
*
* @package Jio
*/
import { __ } from '@wordpress/i18n';
import { ToolMenu, type ToolMenuItem } from './tool-menu';
type Capabilities = Record<string, boolean>;
type Props = {
ready: boolean;
canUndo: boolean;
canRedo: boolean;
formatValue: string;
onFormatChange: (value: string) => void;
alignValue: string;
blockValue: string;
capabilities: Capabilities;
onCommand: (name: string, value?: string) => void;
onRememberSelection: () => void;
onOpenLink: () => void;
onOpenMedia: (type: 'image' | 'video' | 'file') => void;
onInsertBlock: (kind: string) => void;
onInsertQuote: (color: '' | 'qe_wzk_c-red' | 'qe_wzk_lan' | 'qe_wzk_lv') => void;
onInsertHidden: (kind: 'reply' | 'login' | 'vip' | 'payshow') => void;
};
type ToolButtonProps = {
label: string;
icon: string;
ready: boolean;
onClick: () => void;
title?: string;
active?: boolean;
};
function ToolButton({ label, icon, ready, onClick, title, active = false }: ToolButtonProps) {
return (
<button
type="button"
className={`jio-creator__tool-btn${active ? ' is-active' : ''}`}
disabled={!ready}
onClick={onClick}
title={title || label}
aria-label={title || label}
aria-pressed={active || undefined}
>
<i className={icon} aria-hidden="true" />
<span className="jio-creator__tool-label">{label}</span>
</button>
);
}
/** 引用配色:菜单项自带色块预览,和正文里的效果对得上。 */
const QUOTE_OPTIONS = [
{ key: '' as const, label: __('灰色引用', 'jio'), swatch: '#555555' },
{ key: 'qe_wzk_c-red' as const, label: __('红色引用', 'jio'), swatch: '#dc3f3f' },
{ key: 'qe_wzk_lan' as const, label: __('蓝色引用', 'jio'), swatch: '#0092ff' },
{ key: 'qe_wzk_lv' as const, label: __('绿色引用', 'jio'), swatch: '#23790e' },
];
/** 隐藏内容的四档可见性,对应服务端的 [reply] / [jio_hide] / [vip] / [payshow]。 */
const HIDE_OPTIONS = [
{ key: 'reply' as const, label: __('评论后可查看', 'jio'), icon: 'fa fas fa-comment-dots' },
{ key: 'login' as const, label: __('登录后可查看', 'jio'), icon: 'fa fas fa-sign-in-alt' },
{ key: 'vip' as const, label: __('会员可查看', 'jio'), icon: 'fa fas fa-crown' },
{ key: 'payshow' as const, label: __('付费后可查看', 'jio'), icon: 'fa fas fa-coins' },
];
export function EditorToolbar({
ready,
canUndo,
canRedo,
formatValue,
onFormatChange,
alignValue,
blockValue,
capabilities,
onCommand,
onRememberSelection,
onOpenLink,
onOpenMedia,
onInsertBlock,
onInsertQuote,
onInsertHidden,
}: Props) {
const headingItems: ToolMenuItem[] = [
{ key: 'p', label: __('正文', 'jio'), icon: 'fa fas fa-paragraph' },
{ key: 'h2', label: __('标题 2', 'jio'), icon: 'fa fas fa-heading' },
{ key: 'h3', label: __('标题 3', 'jio'), icon: 'fa fas fa-heading' },
{ key: 'h4', label: __('标题 4', 'jio'), icon: 'fa fas fa-heading' },
].map((item) => ({
...item,
active: formatValue === item.key,
onSelect: () => {
onFormatChange(item.key);
onCommand('format', item.key);
},
}));
const listItems: ToolMenuItem[] = [
{ key: 'ul', label: __('无序列表', 'jio'), icon: 'fa fas fa-list-ul' },
{ key: 'ol', label: __('有序列表', 'jio'), icon: 'fa fas fa-list-ol' },
].map((item) => ({
...item,
active: blockValue === item.key,
onSelect: () => onInsertBlock(item.key),
}));
const alignItems: ToolMenuItem[] = [
{ key: 'left', label: __('居左对齐', 'jio'), icon: 'fa fas fa-align-left' },
{ key: 'center', label: __('居中对齐', 'jio'), icon: 'fa fas fa-align-center' },
{ key: 'right', label: __('居右对齐', 'jio'), icon: 'fa fas fa-align-right' },
].map((item) => ({
...item,
active: alignValue === item.key,
onSelect: () => onCommand('align', item.key),
}));
const quoteItems: ToolMenuItem[] = QUOTE_OPTIONS.map((option) => ({
key: option.key || 'default',
label: option.label,
swatch: option.swatch,
active: blockValue === (option.key || 'quote'),
onSelect: () => onInsertQuote(option.key),
}));
const hideItems: ToolMenuItem[] = HIDE_OPTIONS.map((option) => ({
key: option.key,
label: option.label,
icon: option.icon,
onSelect: () => onInsertHidden(option.key),
}));
// 触发器图标跟随当前状态(对齐方式 / 列表类型),一眼能看出光标所在段落。
const alignIcon =
alignValue === 'center'
? 'fa fas fa-align-center'
: alignValue === 'right'
? 'fa fas fa-align-right'
: 'fa fas fa-align-left';
const listIcon = blockValue === 'ol' ? 'fa fas fa-list-ol' : 'fa fas fa-list-ul';
return (
<div
className="jio-creator__toolbar-tools"
role="toolbar"
aria-label={__('正文编辑工具', 'jio')}
onMouseDown={(event) => {
onRememberSelection();
if ((event.target as HTMLElement).closest('button')) event.preventDefault();
}}
>
<div className="jio-creator__toolbar-group" role="group" aria-label={__('撤销和重做', 'jio')}>
<ToolButton label={__('撤销', 'jio')} icon="fa fas fa-undo" ready={canUndo} onClick={() => onCommand('undo')} />
<ToolButton label={__('重做', 'jio')} icon="fa fas fa-redo" ready={canRedo} onClick={() => onCommand('redo')} />
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('段落样式', 'jio')}>
<ToolMenu
label={__('标题', 'jio')}
icon="fa fas fa-heading"
items={headingItems}
ready={ready}
width={168}
onRememberSelection={onRememberSelection}
/>
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('文字格式', 'jio')}>
<ToolButton label={__('加粗', 'jio')} icon="fa fas fa-bold" ready={ready} onClick={() => onCommand('Bold')} />
<ToolButton label={__('斜体', 'jio')} icon="fa fas fa-italic" ready={ready} onClick={() => onCommand('Italic')} />
<ToolButton label={__('下划线', 'jio')} icon="fa fas fa-underline" ready={ready} onClick={() => onCommand('Underline')} />
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('段落块', 'jio')}>
<ToolMenu
label={__('列表', 'jio')}
icon={listIcon}
items={listItems}
ready={ready}
width={168}
onRememberSelection={onRememberSelection}
active={blockValue === 'ul' || blockValue === 'ol'}
/>
<ToolMenu
label={__('对齐', 'jio')}
icon={alignIcon}
items={alignItems}
ready={ready}
width={168}
onRememberSelection={onRememberSelection}
active={alignValue !== 'left'}
title={__('对齐方式', 'jio')}
/>
<ToolMenu
label={__('引用', 'jio')}
icon="fa fas fa-quote-left"
items={quoteItems}
ready={ready}
width={168}
onRememberSelection={onRememberSelection}
active={blockValue === 'quote' || QUOTE_OPTIONS.some((option) => option.key === blockValue)}
/>
{capabilities.canHide && (
<ToolMenu
label={__('隐藏', 'jio')}
icon="fa fas fa-eye-slash"
items={hideItems}
ready={ready}
width={190}
onRememberSelection={onRememberSelection}
title={__('隐藏内容', 'jio')}
/>
)}
<ToolButton label={__('分割线', 'jio')} icon="fa fas fa-minus" ready={ready} onClick={() => onInsertBlock('divider')} />
<ToolButton label={__('代码块', 'jio')} icon="fa fas fa-code" ready={ready} onClick={() => onInsertBlock('code')} />
<ToolButton label={__('注释', 'jio')} icon="fa fas fa-comment-dots" ready={ready} onClick={() => onInsertBlock('footnote')} />
<ToolButton label={__('公式', 'jio')} icon="fa fas fa-square-root-alt" ready={ready} onClick={() => onInsertBlock('math')} />
<ToolButton label={__('表格', 'jio')} icon="fa fas fa-table" ready={ready} onClick={() => onInsertBlock('table')} />
</div>
<span className="jio-creator__toolbar-divider" aria-hidden="true" />
<div className="jio-creator__toolbar-group" role="group" aria-label={__('媒体和链接', 'jio')}>
{capabilities.canUploadImage && (
<ToolButton label={__('图片', 'jio')} icon="fa far fa-image" ready={ready} onClick={() => onOpenMedia('image')} />
)}
{(capabilities.canUploadVideo || capabilities.canEmbed) && (
<ToolButton label={__('视频', 'jio')} icon="fa fas fa-video" ready={ready} onClick={() => onOpenMedia('video')} />
)}
<ToolButton label={__('链接', 'jio')} icon="fa fas fa-link" ready={ready} onClick={onOpenLink} />
{capabilities.canUploadFile && (
<ToolButton label={__('附件', 'jio')} icon="fa fas fa-paperclip" ready={ready} onClick={() => onOpenMedia('file')} />
)}
</div>
</div>
);
}