# 自定义表单域右键菜单
Foxit PDF SDK for Web 提供了两种灵活的方式帮助开发者实现自定义表单域的右键菜单,以增强用户的交互体验。
# 方式一:方法重写(Method Override)
通过 ViewerAnnotManager.registerMatchRule (opens new window) 方法注册 mixin,继承 WidgetAnnot 类并重写以下关键方法:
- createFillerModeContextMenu:自定义表单填写模式下的右键菜单
- createDesignModeContextMenu:自定义表单设计模式下的右键菜单
- showContextMenu: 自定义右键菜单的显示逻辑
此方案适用于需要深度自定义菜单项和交互逻辑的场景。无论是基于 UIExtension 还是基于 PDFViewCtrl 开发的用户,都可以支持这种方案。
# 方式二:UI Fragments 配置
通过使用 Foxit PDF SDK for Web 提供的 UI Fragments 机制,开发者可以实现以下操作:
- 替换现有的右键菜单
- 扩展现有菜单项
- 调整菜单项的顺序和分组
此方案依赖于 UIExtension 实现,且仅支持替换或扩展已有的菜单项。
# 示例
# 方法重写
首先,创建一个继承自 IContextMenu (opens new window) 的右键菜单实现类。该类定义了右键菜单的显示、隐藏、禁用、启用和销毁的行为。
class CustomContextMenu extends PDFViewCtrl.viewerui.IContextMenu {
    constructor(widgetAnnotComponent) {
        super();
        this.element = document.createElement('div');
        this.element.className = 'custom-context-menu';
        this.element.innerHTML = `
            <div class="item" action="properties">Properties</div>
            <div class="item" action="delete">Delete</div>
        `;
        this.widgetAnnotComponent = widgetAnnotComponent;
        this.element.addEventListener('click', e => {
            const action = e.target.getAttribute('action');
            if (action == 'properties') {
                // show properties
                pdfui.getStateHandlerManager().then(shm => {
                    shm.switchTo(PDFViewCtrl.STATE_HANDLER_NAMES.STATE_HANDLER_SELECT_ANNOTATION)
                    pdfui.activateElement(widgetAnnotComponent);
                    pdfui.getComponentByName('fv--form-designer-widget-properties-dialog').then(component => {
                        component.show();
                    })
                });
            } else if (action == 'delete') {
                const annot = this.widgetAnnotComponent.annot;
                const page = annot.page;
                page.removeAnnotByObjectNumber(annot.getObjectNumber());
            }
        });
        
    }
    showAt(x, y) {
        super.showAt(x, y);
        document.body.append(this.element);
        this.element.style.left = x + 'px';
        this.element.style.top = y + 'px';
        document.addEventListener('mouseup', async e => {
            await new Promise(resolve => setTimeout(resolve, 100));
            this.element.remove();
        }, { once: true });
    }
    disable() {
        super.disable();
        this.element.classList.add('disabled');
    }
    enable() {
        super.enable();
        this.element.classList.remove('disabled');
    }
    destroy() {
        super.destroy();
        this.element.remove();
    }
}
注意: Foxit PDF SDK for Web 不会主动隐藏 IContextMenu 菜单,开发者需要在 CustomContextMenu 中自行处理隐藏逻辑。通常是通过监听点击菜单项或点击菜单以外的位置来实现隐藏菜单。
在完成自定义菜单实现类后,您可以注册一个 mixin 函数,以重写 createFillerModeContextMenu 或 createDesignModeContextMenu 方法:
// 注册自定义实现
pdfViewer.getAnnotManager().registerMatchRule(function(annot, AnnotClass) {
    if(annot.getType() === 'widget') {
        return class CustomWidgetAnnot extends AnnotClass {
            async createFillerModeContextMenu() {
                return new CustomContextMenu();
            }
            async createDesignModeContextMenu() {
                return new CustomContextMenu();
            }
        }
    }
});
如果是签名表单域,开发者还需要注意其签名状态。根据签名表单域的不同状态,需要显示不同的菜单或菜单项:
class CustomSignatureContextMenu extends PDFViewCtrl.viewerui.IContextMenu {
    // ……
}
class CustomSignedSignatureContextMenu extends PDFViewCtrl.viewerui.IContextMenu {
    // ……
}
pdfViewer.getAnnotManager().registerMatchRule(function(annot, AnnotClass) {
    if(annot.getType() === 'widget') {
        return class CustomWidgetAnnot extends AnnotClass {
            async createFillerModeContextMenu() {
                const field = annot.getField();
                const isSignature = field.getType() === FieldType.Sign;
                if(isSignature) {
                    const isSigned = await field.isSigned();
                    // isSigned 可以控制显示不同的菜单或菜单项。其具体行为最终由应用层来决定。
                    if (isSigned) {
                        return new CustomSignedSignatureContextMenu();
                    } else {
                        return new CustomSignatureContextMenu();
                    }
                }
                return new CustomContextMenu();
            }
        }
    }
});
当然,您也可以选择不通过 createFillerModeContextMenu 和 createDesignModeContextMenu 来实现创建逻辑。您可以直接通过重写 showContextMenu 方法来创建和显示右键菜单:
pdfViewer.getAnnotManager().registerMatchRule(function(annot, AnnotClass) {
    if(annot.getType() === 'widget') {
        return class CustomWidgetAnnot extends AnnotClass {
            async showContextMenu(x, y) {
                if(this.isDesignMode) { // 表示设计模式下显示内置的右键菜单
                    return super.showContextMenu(x, y);
                }
                const field = annot.getField();
                const isSignature = field.getType() === FieldType.Sign;
                if(isSignature) {
                    const isSigned = await field.isSigned();
                    if (isSigned) {
                        // TODO: 显示填表模式且已签名的签名域右键菜单
                    } else {
                        // TODO: 显示填表模式且未签名的签名域右键菜单
                    }
                } else {
                    // TODO: 显示填表模式且非签名的表单域右键菜单
                }
            }
        }
    }
});
# UI Fragments 配置
以下是目前 SDK 中内置的表单域右键菜单组件:
- <form:signature-contextmenu @lazy></form:signature-contextmenu>: 填表模式下的签名表单域右键菜单。
- <form-designer-v2:widget-contextmenu @lazy=""></form-designer-v2:widget-contextmenu>: 设计模式下的表单域右键菜单。
# 填表模式下的签名表单域右键菜单
以下是其实现模板:
<contextmenu name="fv--field-signature-contextmenu">
    <contextmenu-item name="fv--contextmenu-item-signature-sign" @controller="form:SignatureSignDocController" @form:if-signed.hide visible="false">signDocument.sign</contextmenu-item>
    <contextmenu-item name="fv--contextmenu-item-signature-verify" @controller="form:SignatureVerifyController" @form:if-signed.show visible="false">verifySign.verify</contextmenu-item>
    <contextmenu-separator @form:if-signed.show visible="false"></contextmenu-separator>
    <contextmenu-item name="fv--contextmenu-item-signature-properties" @controller="form:SignaturePropertiesController" @form:if-signed.show visible="false">signDocument.showSignProperty</contextmenu-item>
</contextmenu>
菜单项说明:
- @form:if-signed指令:可以根据当前右键的目标签名域是否已签名来控制组件是否显示。比如,- @form:if-signed.hide表示如果目标签名域已签名,则隐藏该组件。相对应的,- @form:if-signed.show则表示如果目标签名域已签名,则显示该组件。
- fv--contextmenu-item-signature-sign: 触发签名功能,在未签名的签名域上右键时显示。
- fv--contextmenu-item-signature-verify: 触发验签功能,在已签名的签名域上右键时显示。
- fv--contextmenu-item-signature-properties: 显示签名属性,在已签名的签名域上右键时显示。
# 示例1:插入菜单项
首先,实现一个自定义菜单项组件:
const customModule = UIExtension.modular.module('custom', []);
class CustomContextMenuItem extends UIExtension.SeniorComponentFactory.createSuperClass({
    template: `<contextmenu-item @on.click="$component.onClick()"></contextmenu-item>`
}) {
    static getName() {
        return 'custom-signature-contextmenu';
    }
    onClick() {
        const currentWidget = this.parent.getCurrentTarget();
        console.log(currentWidget);
    }
}
const FRAGMENT_ACTION = UIExtension.UIConsts.FRAGMENT_ACTION;
const CustomAppearance = UIExtension.appearances.AdaptiveAppearance.extend({
    getDefaultFragments: function() {
        const isMobile = PDFViewCtrl.DeviceInfo.isMobile;
        if(isMobile) {
            return [];
        } else {
            return [{
                target: 'fv--field-signature-contextmenu',
                action: FRAGMENT_ACTION.APPEND,
                template: `<custom:custom-signature-contextmenu></custom:custom-signature-contextmenu>`
            }];
        }
    }
});
const pdfui = new UIExtension.PDFUI({
    appearance: CustomAppearance,
    //...
});
# 示例2:替换菜单项
const FRAGMENT_ACTION = UIExtension.UIConsts.FRAGMENT_ACTION;
const CustomAppearance = UIExtension.appearances.AdaptiveAppearance.extend({
    getDefaultFragments: function() {
        const isMobile = PDFViewCtrl.DeviceInfo.isMobile;
        if(isMobile) {
            return [];
        } else {
            return [{
                target: 'fv--contextmenu-item-signature-properties',
                action: FRAGMENT_ACTION.REPLACE,
                template: `<custom:custom-signature-contextmenu></custom:custom-signature-contextmenu>`
            }];
        }
    }
});
const pdfui = new UIExtension.PDFUI({
    appearance: CustomAppearance,
    //...
});