重构块编辑器弹窗工具栏

This commit is contained in:
jingrow 2026-06-04 04:59:45 +08:00
parent 08f680f92a
commit 0858fb5be9
2 changed files with 136 additions and 22 deletions

View File

@ -20,28 +20,55 @@ const {
isActive,
exec,
headingLevels,
currentHeadingIcon,
setHeading,
setParagraph,
TEXT_COLORS,
HIGHLIGHT_COLORS,
setTextColor,
setHighlightColor,
setTextAlign,
} = useCommands(editorRefWrapper)
function isAlignActive(align: string): boolean {
const editor = editorRefWrapper.value
if (!editor) return false
return editor.isActive({ textAlign: align })
}
//
const bubbleHeadingOpen = ref(false)
const bubbleColorOpen = ref(false)
const bubbleHighlightOpen = ref(false)
const bubbleMoreOpen = ref(false)
const bubbleTurnIntoOpen = ref(false)
function toggleBubbleHeading() {
bubbleHeadingOpen.value = !bubbleHeadingOpen.value
function handleSetTextColor(color: string) {
setTextColor(color)
bubbleColorOpen.value = false
}
function handleSetHighlightColor(color: string) {
setHighlightColor(color)
bubbleHighlightOpen.value = false
}
function execAndCloseMore(cmd: string) {
exec(cmd)
bubbleMoreOpen.value = false
}
/**
* BubbleMenu 每次显示前重置子菜单状态确保不会残留上次的展开状态
* BubbleMenu 每次显示前重置所有子菜单状态确保不会残留上次的展开状态
*/
const bubbleMenuOptions = computed(() => ({
placement: 'top',
offset: 8,
strategy: 'fixed',
onShow: () => {
bubbleHeadingOpen.value = false
bubbleColorOpen.value = false
bubbleHighlightOpen.value = false
bubbleMoreOpen.value = false
bubbleTurnIntoOpen.value = false
},
}))
@ -121,28 +148,92 @@ function shouldShow(props: {
<ToolbarButton icon="code" :title="t('Inline Code')" :active="isActive('code')" @click="exec('code')" />
<ToolbarDivider />
<!-- Bubble heading dropdown -->
<!-- Text Color dropdown -->
<div class="be-tb-dropdown be-bubble-dropdown">
<ToolbarButton :icon="currentHeadingIcon()" :title="t('Heading')" :active="bubbleHeadingOpen" @click="toggleBubbleHeading" />
<div v-if="bubbleHeadingOpen" class="be-tb-dropdown-menu">
<div
v-for="level in headingLevels" :key="level"
class="be-tb-dropdown-item"
:class="{ 'be-tb-dropdown-item-active': isActive('heading', { level }) }"
@mousedown.prevent="setHeading(level)"
><span v-html="renderIcon('h' + level)"></span>{{ t('Heading ' + level) }}</div>
<div class="be-tb-dropdown-divider"></div>
<div
class="be-tb-dropdown-item"
:class="{ 'be-tb-dropdown-item-active': isActive('paragraph') }"
@mousedown.prevent="setParagraph()"
><span v-html="renderIcon('format_paragraph')"></span>{{ t('Paragraph') }}</div>
<ToolbarButton icon="text_color" :title="t('Text Color')" :active="bubbleColorOpen" @click="bubbleColorOpen = !bubbleColorOpen" />
<div v-if="bubbleColorOpen" class="be-tb-dropdown-menu">
<div class="be-tb-color-grid">
<div v-for="c in TEXT_COLORS" :key="c" class="be-tb-color-swatch" :style="{ background: c }" @mousedown.prevent="handleSetTextColor(c)" />
</div>
</div>
</div>
<!-- Highlight dropdown -->
<div class="be-tb-dropdown be-bubble-dropdown">
<ToolbarButton icon="highlight" :title="t('Highlight')" :active="bubbleHighlightOpen" @click="bubbleHighlightOpen = !bubbleHighlightOpen" />
<div v-if="bubbleHighlightOpen" class="be-tb-dropdown-menu">
<div class="be-tb-color-grid">
<div
v-for="c in HIGHLIGHT_COLORS" :key="c"
class="be-tb-color-swatch"
:class="{ 'be-tb-color-swatch-transparent': c === 'transparent' }"
:style="{ background: c === 'transparent' ? '#fff' : c }"
@mousedown.prevent="handleSetHighlightColor(c)"
/>
</div>
</div>
</div>
<ToolbarDivider />
<ToolbarButton icon="link" :title="t('Link')" :active="isActive('link')" @click="openLinkPanel" />
<ToolbarButton icon="quote" :title="t('Blockquote')" :active="isActive('blockquote')" @click="exec('quote')" />
<!-- More menu (竖向三点 转换成子菜单 + 对齐) -->
<div class="be-tb-dropdown be-bubble-dropdown">
<ToolbarButton icon="more_vert" :title="t('More')" :active="bubbleMoreOpen" @click="bubbleMoreOpen = !bubbleMoreOpen" />
<div v-if="bubbleMoreOpen" class="be-tb-dropdown-menu">
<!-- Turn into submenu (块类型转换) -->
<div class="be-tb-dropdown-item be-tb-dropdown-item-submenu" @click.stop="bubbleTurnIntoOpen = !bubbleTurnIntoOpen">
<span v-html="renderIcon('format_paragraph')"></span>
<span>{{ t('Turn Into') }}</span>
<span class="be-tb-submenu-arrow"></span>
<div v-if="bubbleTurnIntoOpen" class="be-tb-submenu">
<div
class="be-tb-dropdown-item"
:class="{ 'be-tb-dropdown-item-active': isActive('paragraph') }"
@mousedown.prevent="setParagraph(); bubbleTurnIntoOpen = false; bubbleMoreOpen = false"
><span v-html="renderIcon('format_paragraph')"></span>{{ t('Paragraph') }}</div>
<div
v-for="level in headingLevels" :key="level"
class="be-tb-dropdown-item"
:class="{ 'be-tb-dropdown-item-active': isActive('heading', { level }) }"
@mousedown.prevent="setHeading(level); bubbleTurnIntoOpen = false; bubbleMoreOpen = false"
><span v-html="renderIcon('h' + level)"></span>{{ t('Heading ' + level) }}</div>
<div class="be-tb-dropdown-divider"></div>
<div class="be-tb-dropdown-item" @mousedown.prevent="exec('quote'); bubbleTurnIntoOpen = false; bubbleMoreOpen = false">
<span v-html="renderIcon('quote')"></span>{{ t('Blockquote') }}
</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="exec('codeBlock'); bubbleTurnIntoOpen = false; bubbleMoreOpen = false">
<span v-html="renderIcon('code_block')"></span>{{ t('Code Block') }}
</div>
<div class="be-tb-dropdown-divider"></div>
<div class="be-tb-dropdown-item" @mousedown.prevent="exec('bulletList'); bubbleTurnIntoOpen = false; bubbleMoreOpen = false">
<span v-html="renderIcon('ulist')"></span>{{ t('Bullet List') }}
</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="exec('orderedList'); bubbleTurnIntoOpen = false; bubbleMoreOpen = false">
<span v-html="renderIcon('olist')"></span>{{ t('Ordered List') }}
</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="exec('taskList'); bubbleTurnIntoOpen = false; bubbleMoreOpen = false">
<span v-html="renderIcon('task_list')"></span>{{ t('Task List') }}
</div>
</div>
</div>
<div class="be-tb-dropdown-divider"></div>
<!-- Alignment -->
<div class="be-tb-dropdown-item" :class="{ 'be-tb-dropdown-item-active': isAlignActive('left') }" @mousedown.prevent="setTextAlign('left'); bubbleMoreOpen = false">
<span v-html="renderIcon('align_left')"></span>{{ t('Align Left') }}
</div>
<div class="be-tb-dropdown-item" :class="{ 'be-tb-dropdown-item-active': isAlignActive('center') }" @mousedown.prevent="setTextAlign('center'); bubbleMoreOpen = false">
<span v-html="renderIcon('align_center')"></span>{{ t('Align Center') }}
</div>
<div class="be-tb-dropdown-item" :class="{ 'be-tb-dropdown-item-active': isAlignActive('right') }" @mousedown.prevent="setTextAlign('right'); bubbleMoreOpen = false">
<span v-html="renderIcon('align_right')"></span>{{ t('Align Right') }}
</div>
<div class="be-tb-dropdown-divider"></div>
<!-- Inline marks -->
<div class="be-tb-dropdown-item" @mousedown.prevent="execAndCloseMore('subscript')"><span v-html="renderIcon('subscript')"></span>{{ t('Subscript') }}</div>
<div class="be-tb-dropdown-item" @mousedown.prevent="execAndCloseMore('superscript')"><span v-html="renderIcon('superscript')"></span>{{ t('Superscript') }}</div>
</div>
</div>
</BubbleMenu>
</template>
@ -152,4 +243,26 @@ body.be-bubble-dragging .blockeditor-bubble-menu {
visibility: hidden !important;
pointer-events: none !important;
}
/* ── 三点菜单中的「转换成」子菜单 ── */
.be-tb-dropdown-item-submenu {
position: relative;
}
.be-tb-submenu-arrow {
margin-left: auto;
font-size: 12px;
color: #9ca3af;
}
.be-tb-submenu {
position: absolute;
left: 100%;
top: -4px;
min-width: 140px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 8px;
box-shadow: 0 6px 20px rgba(0,0,0,0.12);
padding: 4px;
z-index: 50;
}
</style>

View File

@ -47,6 +47,7 @@ const ICONS = {
external_link: '<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>',
trash: '<path d="M3 6h18m-2 0v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2m-6 5v6m4-6v6"/>',
x: '<path d="M18 6L6 18M6 6l12 12"/>',
more_vert: '<circle cx="12" cy="5" r="2" fill="currentColor" stroke="none"/><circle cx="12" cy="12" r="2" fill="currentColor" stroke="none"/><circle cx="12" cy="19" r="2" fill="currentColor" stroke="none"/>',
} as const
/**