重构块编辑器弹窗工具栏
This commit is contained in:
parent
08f680f92a
commit
0858fb5be9
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user