# 自定义表单域右键菜单

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 函数,以重写 createFillerModeContextMenucreateDesignModeContextMenu 方法:

// 注册自定义实现
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();
            }
        }
    }
});

当然,您也可以选择不通过 createFillerModeContextMenucreateDesignModeContextMenu 来实现创建逻辑。您可以直接通过重写 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,
    //...
});