From f56925863364d9ba0b1550bd75667022faf16cdd Mon Sep 17 00:00:00 2001 From: xfy Date: Thu, 23 Jul 2026 14:00:42 +0800 Subject: [PATCH] =?UTF-8?q?fix(editor):=20=E9=9A=90=E8=97=8F=E6=97=A0?= =?UTF-8?q?=E8=AF=AD=E8=A8=80=E6=A0=87=E8=AF=86=E4=BB=A3=E7=A0=81=E5=9D=97?= =?UTF-8?q?=E7=9A=84=E7=A9=BA=E5=B7=A5=E5=85=B7=E6=A0=8F=E9=A1=B6=E6=A0=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/__tests__/code-block-view.test.ts | 37 ++++++++++++++++++- libs/tiptap-editor/src/code-block-view.ts | 28 +++++++++++++- libs/tiptap-editor/src/style.css | 6 ++- 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/libs/tiptap-editor/src/__tests__/code-block-view.test.ts b/libs/tiptap-editor/src/__tests__/code-block-view.test.ts index 8e59d67..37a9980 100644 --- a/libs/tiptap-editor/src/__tests__/code-block-view.test.ts +++ b/libs/tiptap-editor/src/__tests__/code-block-view.test.ts @@ -39,6 +39,10 @@ describe('CodeBlockNodeView', () => { } as any); expect(view.dom.querySelector('.tiptap-codeblock-lang')?.textContent).toBe('python'); expect(view.dom.querySelector('.tiptap-codeblock-run')).not.toBeNull(); + expect(view.dom.querySelector('.tiptap-codeblock-toolbar')?.style.display).toBe( + '', + ); + expect(view.dom.classList.contains('has-toolbar')).toBe(true); }); it('普通块(python):显示语言标签,无运行按钮', () => { @@ -48,16 +52,47 @@ describe('CodeBlockNodeView', () => { } as any); expect(view.dom.querySelector('.tiptap-codeblock-lang')?.textContent).toBe('python'); expect(view.dom.querySelector('.tiptap-codeblock-run')).toBeNull(); + expect(view.dom.querySelector('.tiptap-codeblock-toolbar')?.style.display).toBe( + '', + ); + expect(view.dom.classList.contains('has-toolbar')).toBe(true); }); - it('无 language 的块:标签为空或占位,无运行按钮', () => { + it('无 language 的块:隐藏 toolbar,无运行按钮', () => { const view = new CodeBlockNodeView({ node: mockNode(''), editor: mockEditor(), } as any); expect(view.dom.querySelector('.tiptap-codeblock-run')).toBeNull(); + expect(view.dom.querySelector('.tiptap-codeblock-toolbar')?.style.display).toBe( + 'none', + ); + expect(view.dom.classList.contains('has-toolbar')).toBe(false); }); + it('node 语言由空更新为 python 时 update() 显示 toolbar', () => { + const view = new CodeBlockNodeView({ + node: mockNode(''), + editor: mockEditor(), + } as any); + expect(view.dom.querySelector('.tiptap-codeblock-toolbar')?.style.display).toBe( + 'none', + ); + expect(view.dom.classList.contains('has-toolbar')).toBe(false); + + view.update(mockNode('python')); + expect(view.dom.querySelector('.tiptap-codeblock-lang')?.textContent).toBe('python'); + expect(view.dom.querySelector('.tiptap-codeblock-toolbar')?.style.display).toBe( + '', + ); + expect(view.dom.classList.contains('has-toolbar')).toBe(true); + + view.update(mockNode('')); + expect(view.dom.querySelector('.tiptap-codeblock-toolbar')?.style.display).toBe( + 'none', + ); + expect(view.dom.classList.contains('has-toolbar')).toBe(false); + }); it('node 语言变化时 update() 刷新标签', () => { const view = new CodeBlockNodeView({ node: mockNode('python'), diff --git a/libs/tiptap-editor/src/code-block-view.ts b/libs/tiptap-editor/src/code-block-view.ts index 60057ae..2f1455e 100644 --- a/libs/tiptap-editor/src/code-block-view.ts +++ b/libs/tiptap-editor/src/code-block-view.ts @@ -66,10 +66,10 @@ export class CodeBlockNodeView { this.langBadge.classList.add('tiptap-codeblock-lang'); this.langBadge.textContent = extractLang((this.node.attrs.language as string) ?? ''); // runnable 块的语言标签可点击,触发编辑模态框(改语言/overrides) + this.langBadge.addEventListener('click', () => this.openEditModal()); if (isRunnable(this.node)) { this.langBadge.classList.add('tiptap-codeblock-lang-editable'); this.langBadge.title = '点击修改语言与运行配置'; - this.langBadge.addEventListener('click', () => this.openEditModal()); } this.toolbar.appendChild(this.langBadge); @@ -93,6 +93,9 @@ export class CodeBlockNodeView { } this.pre.appendChild(this.code); this.container.appendChild(this.pre); + + // 校验并设置工具栏显隐(无语言且非 runnable 时隐藏) + this.updateToolbarVisibility(); } get dom(): HTMLElement { @@ -114,12 +117,22 @@ export class CodeBlockNodeView { this.node = node; if (oldLang !== newLang) { this.langBadge.textContent = extractLang(newLang); + const runnable = isRunnable(node); + this.langBadge.classList.toggle('tiptap-codeblock-lang-editable', runnable); + if (runnable) { + this.langBadge.title = '点击修改语言与运行配置'; + } else { + this.langBadge.removeAttribute('title'); + } + // 更新 的 language class(低亮按新语言重算) this.code.className = ''; const langClass = extractLang(newLang); if (langClass) this.code.classList.add(`language-${langClass}`); // runnable 状态变化时重建按钮(简化:不细粒度增删,整体重建工具栏按钮区) this.refreshRunButton(); + // 刷新工具栏显隐状态(无语言且非 runnable 时隐藏) + this.updateToolbarVisibility(); } return true; } @@ -137,6 +150,18 @@ export class CodeBlockNodeView { } } + /** + * 刷新工具栏显隐与容器类名:无语言且非 runnable 时隐藏工具栏。 + */ + private updateToolbarVisibility(): void { + const lang = extractLang((this.node.attrs.language as string) ?? ''); + const runnable = isRunnable(this.node); + const show = lang !== '' || runnable; + + this.toolbar.style.display = show ? '' : 'none'; + this.container.classList.toggle('has-toolbar', show); + } + /** * 判断 DOM mutation 是否应被 ProseMirror 忽略。 * @@ -178,6 +203,7 @@ export class CodeBlockNodeView { /** 点击运行:调 editor.storage.__onRunCode,结果填入结果区。 */ /** 点击语言标签:打开编辑模态框,修改当前 runnable 块的语言/overrides。 */ private openEditModal(): void { + if (!isRunnable(this.node)) return; const pos = this.getPos?.(); const currentInfo = (this.node.attrs.language as string) ?? ''; if (pos === undefined) return; diff --git a/libs/tiptap-editor/src/style.css b/libs/tiptap-editor/src/style.css index e07753f..e33ab1b 100644 --- a/libs/tiptap-editor/src/style.css +++ b/libs/tiptap-editor/src/style.css @@ -960,9 +960,13 @@ -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; } -/* pre 紧贴 toolbar 下方,合并圆角 */ +/* pre 默认全圆角;有 toolbar 时与 toolbar 合并顶部圆角 */ .tiptap-codeblock > pre { margin: 0; + border-radius: 6px; +} + +.tiptap-codeblock.has-toolbar > pre { border-radius: 0 0 6px 6px; }