You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Tiptap结合Tippy实现固定气泡菜单异常问题求助

问题

我想用Tippy给ProseMirror节点实现固定气泡菜单。最初尝试用<BubbleMenu>扩展,但发现光标在菜单内移动时菜单会随之移位,于是改为将<NodeView>包裹在<Tippy>组件中。但新问题出现:点击按钮边缘功能正常,但点击按钮中间的图标时,按钮不会高亮激活,且光标位置会发生异常变动。无论节点下方是否有段落,按钮激活失败的问题都存在,且下方有段落时光标位置错乱的情况更明显。

相关代码

function PanelBubbleMenuContent({ editor }: { editor: any }) {
    return (
        <span>
            <div role="menu">
                <div className="mw-panel-bubble-menu d-flex ai-center">
                    <button
                        type="button"
                        className="flex--item s-btn mr4"
                        title=""
                        onClick={() => {
                            editor
                                .chain()
                                .focus()
                                .updateAttributes("panel", {
                                    panelType: PanelType.INFO,
                                })
                                .run();
                        }}
                    >
                        <InfoIcon color={getIconColor(PanelType.INFO)} />
                    </button>

                    <button
                        type="button"
                        className="flex--item s-btn mr4"
                        title=""
                        onClick={() => {
                            editor
                                .chain()
                                .focus()
                                .updateAttributes("panel", {
                                    panelType: PanelType.NOTE,
                                })
                                .run();
                        }}
                    >
                        {" "}
                        <NoteIcon color={getIconColor(PanelType.NOTE)} />
                    </button>
                    <button
                        type="button"
                        className="flex--item s-btn mr4"
                        title=""
                        onClick={() => {
                            editor
                                .chain()
                                // .focus()
                                .updateAttributes("panel", {
                                    panelType: PanelType.SUCCESS,
                                })
                                .run();
                        }}
                    >
                        <SuccessIcon color={getIconColor(PanelType.SUCCESS)} />
                    </button>

                    <button
                        type="button"
                        className="flex--item s-btn mr4"
                        title=""
                        onClick={() => {
                            editor
                                .chain()
                                // .focus()
                                .updateAttributes("panel", {
                                    panelType: PanelType.WARNING,
                                })
                                .run();
                        }}
                    >
                        <WarningIcon color={getIconColor(PanelType.WARNING)} />
                    </button>

                    <button
                        type="button"
                        className="flex--item s-btn mr4"
                        title=""
                        onClick={() => {
                            editor
                                .chain()
                                // .focus()
                                .updateAttributes("panel", {
                                    panelType: PanelType.ERROR,
                                })
                                .run();
                        }}
                    >
                        <ErrorIcon color={getIconColor(PanelType.ERROR)} />
                    </button>

                    <button
                        type="button"
                        className="flex--item s-btn mr4"
                        title=""
                        onClick={() => {
                            editor.chain().deleteNode("panel").run();
                        }}
                    >
                        <TrashIcon />
                    </button>
                </div>
            </div>
        </span>
    );
}

export function PanelNodeView({
    editor,
    node,
    updateAttributes,
    deleteNode,
}: NodeViewProps) {
    return (
        <Tippy
            className="mw-popover"
            trigger="click"
            animation="shift-toward-subtle"
            hideOnClick={false}
            content={<PanelBubbleMenuContent editor={editor} />}
            interactive={true}
            placement="bottom"
            onClickOutside={(instance) => {
                instance.hide();
            }}
        >
            <NodeViewWrapper
                as="div"
                className="mw-panel"
                data-panel-type={node.attrs.panelType}
                style={{ backgroundColor: getColor(node.attrs.panelType) }}
            >
                <div
                    className="mw-panel--icon"
                    contentEditable={false}
                    style={{ color: getIconColor(node.attrs.panelType) }}
                >
                    {getIcon(node.attrs.panelType)}
                </div>
                <NodeViewContent className="mw-panel--content" />
            </NodeViewWrapper>
        </Tippy>
    );
}

解决方案

1. 让图标点击穿透到按钮

图标默认会捕获点击事件,导致按钮无法触发激活状态。给所有图标添加pointer-events: none样式,让点击直接穿透到父级按钮:

.mw-panel-bubble-menu button > svg {
    pointer-events: none;
}

如果是自定义Icon组件,也可以直接在组件内部设置:

function InfoIcon({ color }) {
    return (
        <svg style={{ pointerEvents: 'none', color }}>
            {/* 图标内容 */}
        </svg>
    );
}

2. 阻止按钮mousedown事件的默认行为

点击按钮时,ProseMirror的mousedown事件会自动修改编辑器选区并移动光标。给所有按钮添加mousedown处理,阻止默认行为:

<button
    type="button"
    className="flex--item s-btn mr4"
    title=""
    onMouseDown={(e) => e.preventDefault()}
    onClick={() => {
        editor
            .chain()
            .focus()
            .updateAttributes("panel", {
                panelType: PanelType.INFO,
            })
            .run();
    }}
>
    <InfoIcon color={getIconColor(PanelType.INFO)} />
</button>

3. 恢复所有按钮的.focus()调用

之前部分按钮注释掉了.focus(),会导致编辑器状态不一致。恢复所有按钮的editor.chain().focus()调用,确保操作时编辑器处于正确的聚焦状态。

完成以上修改后,点击图标会触发按钮的激活状态,同时光标位置不会再异常变动。

内容的提问来源于stack exchange,提问作者Mercer

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 18:45:12