# # 自定义表单域右键菜单

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,
        //...
    });