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

PSPDFKit自定义注解渲染后丢失文本选择功能的解决咨询

PSPDFKit注解区域文本选择失效问题的解决方案

问题背景

我们在React项目中用PSPDFKit做PDF阅读器,给TextAnnotation、MarkupAnnotation、LinkAnnotation、HighlightAnnotation、UnderlineAnnotation这几类注解加了自定义逻辑:鼠标停留在注解上,停止移动后弹出带指定信息的弹窗。现在弹窗功能正常,但在已有注解的区域里,PSPDFKit原生的文本选择功能用不了了,推测是自定义的DOM节点和事件监听导致的。

问题根源

  1. 自定义div节点设置了pointer-events: all;,完全覆盖在注解区域上方,拦截了原本应该传递给PSPDFKit的文本选择相关事件。
  2. mouseoverListener和mouseLeaveListener里调用了e.preventDefault()和e.stopPropagation(),阻止了事件向PSPDFKit原生组件传递,导致文本选择的触发逻辑被中断。
  3. 弹窗触发的定时器逻辑没有区分“单纯停留”和“拖拽选择文本”两种操作,误把文本选择的拖拽动作当成了停留,干扰了原生功能。

解决方案与优化思路

1. 动态控制pointer-events,减少事件拦截

把自定义div的默认pointer-events设为none,只在需要监听鼠标停留的时候临时开启,弹窗触发后再关闭,让文本选择事件能正常透传。

2. 移除不必要的事件阻止操作

去掉mouseoverListener和mouseLeaveListener里的e.preventDefault()和e.stopPropagation(),让事件正常传递给PSPDFKit的原生组件,保证文本选择事件能被触发。

3. 区分鼠标停留和文本选择操作

增加拖拽状态检测,当用户按下鼠标左键(文本选择的起始动作)时,取消弹窗定时器,避免干扰原生文本选择流程。

4. 优化事件监听的生命周期

在注解销毁时清理所有自定义事件监听,避免内存泄漏和残留的事件拦截。

修改后的完整代码示例

customRenderers: {
    Annotation: ({annotation}) =>{
        if (
            annotation instanceof PSPDFKit.Annotations.TextAnnotation ||
            annotation instanceof PSPDFKit.Annotations.MarkupAnnotation ||
            annotation instanceof PSPDFKit.Annotations.LinkAnnotation ||
            annotation instanceof PSPDFKit.Annotations.HighlightAnnotation ||
            annotation instanceof PSPDFKit.Annotations.UnderlineAnnotation
        ) {
            const node = instance.contentDocument.createElement("div");
            node.classList.add('myAnnotationCustom')
            node.dataset.annotationId = annotation.id;
            // 默认关闭pointer-events,允许事件透传
            node.style.cssText = "position: absolute; width: 100%; height: 100%;pointer-events: none;";
            node.innerHTML = `<div data-title="${annotation.id}"></div>`;

            let isDragging = false;

            const mouseoverListener = async function (e) {
                if(openedAnnotationId.current !== null && openedAnnotationId.current !== annotation.id) return
                if(isPopupOpened.current) return
                if (annotation) {
                    mouseStopActionHasAnnotation.current = true;
                    // 临时开启pointer-events以监听鼠标动作
                    node.style.pointerEvents = 'all';
                }
            };

            const mouseLeaveListener = (e) => {
                if(isPopupOpened.current) return
                mouseStopActionHasAnnotation.current = false;
                // 恢复pointer-events,允许文本选择
                node.style.pointerEvents = 'none';
            }

            const handleOpenPopup = () => {
                setCurrentAnnotation(annotation);
                openedAnnotationId.current = annotation.id;
                isPopupOpened.current = true;
                setShowPopup(true);
                // 弹窗开启后关闭pointer-events,避免拦截后续操作
                node.style.pointerEvents = 'none';
            }

            const handleCancelPopup = () => {
                if(!isPopupOpened.current) {
                    setCurrentAnnotation(null);
                }
            }

            const handleMouseMove = (event) => {
                if(isPopupOpened.current || isDragging) return
                clearTimeout(timer);
                timer = setTimeout(() => {
                    if(annotation && mouseStopActionHasAnnotation.current) {
                        const mousePosition = {
                            event: event,
                            x: event.pageX - 80,
                            y: event.pageY - 120
                        }
                        setMousePosition({
                            top: mousePosition.y,
                            left: mousePosition.x
                        })
                        handleOpenPopup()
                    } else {
                        handleCancelPopup();
                    }
                    node.removeEventListener("mousemove", handleMouseMove);
                }, 500);
            }

            // 监听鼠标左键按下,标记拖拽状态
            const handleMouseDown = (e) => {
                if (e.button === 0) {
                    isDragging = true;
                    clearTimeout(timer);
                    mouseStopActionHasAnnotation.current = false;
                }
            }

            // 监听鼠标左键抬起,重置拖拽状态
            const handleMouseUp = () => {
                isDragging = false;
            }

            node.addEventListener("mouseenter", mouseoverListener, {once: true});
            node.addEventListener("mouseleave", mouseLeaveListener, {once: true});
            node.addEventListener('mousemove', handleMouseMove);
            node.addEventListener('mousedown', handleMouseDown);
            node.addEventListener('mouseup', handleMouseUp);

            // 注解销毁时清理事件监听
            const cleanupListener = (event) => {
                if (event.annotation.id === annotation.id) {
                    node.removeEventListener("mouseenter", mouseoverListener);
                    node.removeEventListener("mouseleave", mouseLeaveListener);
                    node.removeEventListener('mousemove', handleMouseMove);
                    node.removeEventListener('mousedown', handleMouseDown);
                    node.removeEventListener('mouseup', handleMouseUp);
                    instance.removeEventListener('annotationDestroyed', cleanupListener);
                }
            }
            instance.addEventListener('annotationDestroyed', cleanupListener);

            return {
                node,
                append: true,
            };
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 00:14:52