# # PDF 表单属性

本章节详细介绍了如何使用 Foxit PDF SDK for Web 实现 PDF 表单属性操作的技术方案，包括动态修改属性、变更监听机制，以及基于 form-designer 扩展的自定义属性组件开发。在阅读本章节之前，建议您先了解以下内容：

1. 核心概念：
   - PDF 表单的概念
   - 表单交互模式
2. UI 定制开发基础：
   - UI Fragments

## # 表单控件属性

PDF 表单控件属性主要分为两大类：视觉呈现属性（如旋转角度、颜色方案）和 行为控制属性（如可编辑状态、验证规则）。本节将深入解析 Foxit PDF SDK for Web 提供的属性操作接口以及其实时监听机制。

### # 获取表单控件

Foxit PDF SDK for Web 提供了两种模式，用于获取表单控件实例。

### # 主动检索模式

1. 坐标定位【PDFForm.getWidgetAtPoint
   (opens new window)】：
   - 输入： 页面索引、坐标点、可选表单域类型过滤器
   - 输出： 匹配指定位置及类型的表单控件实例
2. 对象标识检索【PDFPage.getAnnotsByObjectNumArray
   (opens new window)】：
   - 输入： 预定义的 objectNumber 数组
   - 输出： PDF 标注对象集合（需要进行二次类型筛选）
3. 全量遍历 【PDFPage.getAnnots
   (opens new window)】：
   - 输出： 当前页所有标注对象（需筛选 Annot_Type.widget 类型）
4. 表单域关联检索 【PDFFormField.getWidget
   (opens new window)】：
   - 前置条件： 需结合 getWidgetsCount
     (opens new window) 获取有效索引范围
   - 输出： 指定表单域的关联表单控件集合

    // 坐标定位示例
    const widget = await form.getWidgetAtPoint(0, { x: 100, y: 100 });
    const pushButtonWidget = await form.getWidgetAtPoint(0, { x: 100, y: 100 }, PDF.form.FieldType.PushButton);

    // 对象标识检索示例
    const [widget] = await page.getAnnotsByObjNumArray([12345]);

    // 遍历页面中的所有注释，根据类型筛选表单控件
    const annots = await page.getAnnots();
    const filteredWidgets = annots.filter(it => {
        return it.getType() === PDF.annots.constant.Annot_Type.widget;
    });

    // 表单域关联检索示例
    const field = await form.getField("Field name");
    const widgetCount = await field.getWidgetsCount();
    const widgets = await Promise.all(
        Array.from({ length: widgetCount }, (_, i) => {
            return field.getWidget(i);
        })
    );

### # 事件驱动模式

通过注册数据事件监听器，实现动态获取:

- `DataEvents.annotationAdded` 标注创建事件: 当添加标注或表单控件时触发，需要根据类型筛选表单控件。
- `DataEvents.annotationUpdated` 标注属性更新事件: 当标注或表单控件的属性发生变更时触发，同样需要根据类型进行筛选。
- `DataEvents.annotationRemoved` 标注移除事件: 当标注或表单控件被删除时触发，并且被删除的对象仅保留 `getObjectNumber` 和 `getAnnotId` 可用。

### # 获取和设置表单控件属性

通过直接调用 Widget
(opens new window) 接口，可以实现属性的修改和获取操作。以下是一个简单示例：

    // 获取所有标注
    const annots = await page.getAnnots();
    // 筛选表单控件
    const widgets = annots.filter(it => {
        return it.getType() === PDF.annots.constant.Annot_Type.widget;
    });

    // 设置所有表单控件旋转90°
    await Promise.all(
        widgets.map(widget => {
            return widget.setRotation(PDF.form.FormWidgetRotation.ROTATION_90)
        })
    );
    // 获取第一个表单控件的旋转角度
    const rotation = await widgets[0].getRotation();

### # 监听表单控件属性变更事件

通过监听 `DataEvents.annotationUpdated` 事件，可实现对表单控件属性变更的监控。

    pdfui.addPDFViewerEventListener(DataEvents.annotationUpdated, (annots, page, updateType) => {

    })

事件回调的参数：

- `annots`：发生属性变更的标注（或表单控件）对象数组，需要根据类型筛选表单控件；
- `page`：发生属性变更的标注（或表单控件）所在的页面对象；
- `updateType`：变更的类型，可以是 `PDF.constant.AnnotUpdatedType`
  (opens new window) 中的一个值。

通过 `updateType` 参数可以确定具体发生变更的属性。详细信息可参考 AnnotUpdatedType
(opens new window)。

## # 表单域属性

### # 获取表单域

获取表单域的实例可以通过以下方式:

- PDFForm.getField
  (opens new window) 方法：
  - 根据表单域名称获取表单域对象。
- PDFForm.getFieldAtPosition
  (opens new window) 方法：
  - 根据页面索引和点坐标获取表单域对象。
- Widget.getField
  (opens new window) 方法：
  - 通过表单控件获取表单域。

示例：

    // 通过表单域名称获取表单域
    const field = await form.getField("Field name");
    // 通过页面索引和点坐标获取表单域
    const field = await form.getFieldAtPosition(0, { x: 100, y: 100 });
    // 通过表单控件获取表单域
    const field = widget.getField();

### # 获取和设置表单域属性

表单域属性的获取和设置可参考 PDFFormField
(opens new window) API 文档。以下是示例代码：

    const field = await form.getField("Field name");
    // 获取表单域的值
    const value = await field.getValue();
    // 更新表单域的值
    await field.setValue(value);

### # 监听表单域属性变更事件

表单域属性变更事件是在表单域属性发生变化时触发的。可以通过监听 `DataEvents.formFieldPropertyUpdated` 事件来获取表单域属性变更。

    pdfui.addPDFViewerEventListener(DataEvents.formFieldPropertyUpdated, (doc, fieldName, propertyName) => {

    })

在表单域属性变更事件的回调中，可以获取以下三个参数：

- `doc`：发生属性变更表单域所属的文档对象；
- `fieldName`：发生属性变更的表单域名称；
- `propertyName`：变更的属性名称，参考 `PDF.form.FormFieldPropertyName`
  (opens new window)。

## # 自定义属性编辑组件

form-designer Addon 为 "表单设计" 提供了强大的功能和组件。从 11.0.0 版本开始，该插件支持自定义属性编辑组件，进一步提升了表单设计的灵活性和可扩展性。

### # PDFFormProperty 用法

PDFFormProperty
(opens new window) 是 form-designer Addon 专门为自定义属性编辑组件提供支持的工具类。其提供了以下功能：

- 可以合并选中的多个表单域属性值。如果属性值不同且无法合并，则属性值会自动置为空，并将状态设置为不可用；
- 可以根据选中的表单域类型，判断是否显示对应的属性编辑组件；
- 可以根据选中的表单域类型和属性值，判断是否启用或禁用对应的属性编辑组件。

form-designer 已根据支持的表单属性预先构造了一系列 PDFFormProperty 实例，开发者可以通过 PDFFormPropertiesService
(opens new window) 直接获取这些实例。

    const formDesigner =await pdfui.getAddonInstance('FormDesigner')
    const propertiesService = formDesigner.getPDFFormPropertiesService();

    const fieldNameProperty = propertiesService.getFieldName();

    fieldNameProperty.onChange(() => {
        const {
            // 表示属性是否有值。当用户未选中表单域，或选中多个表单域但属性值无法合并时，为 false；其他情况为 true。
            hasValue,
            // 属性值
            value,
            // 表示属性是否可用。当选中的表单域不支持该属性，或不支持同时编辑选中的多个表单域的属性时，为 false；其他情况为 true。
            available,
            // 表示属性编辑组件是否可见。当选中的表单域不支持该属性，或不支持同时编辑选中的多个表单域的属性时，为 false；其他情况为 true。
            visible
        } = fieldNameProperty;
    })

    const exportValueProperty = propertiesService.getExportValueProperty()

下面是几个典型的场景，用于解释 PDFFormProperty 的状态：

1. 用户只选中一个 PushButton 表单域，对于 `fieldNameProperty`， 其状态值如下：

   - `hasValue`: true
   - `value`: 'PushButton 0'
   - `available`: true
   - `visible`: true 此时，fieldName 属性编辑组件上显示的值为 'PushButton 0', 组件可见且可编辑。

2. 用户选中了一个 PushButton 和一个 ListBox 表单域，对于 `fieldNameProperty`， 其状态值如下：

   - `hasValue`: false
   - `value`: undefined
   - `available`: false
   - `visible`: true 此时，fieldName 属性编辑组件上显示为空白，组件可见但不可编辑，因为无法同时设置两个表单域名字为相同。

3. 用户选中了一个 PushButton, 对于 `exportValueProperty`， 其状态值如下：

   - `hasValue`: false
   - `value`: undefined
   - `available`: false
   - `visible`: false 此时，`ExportValue` 属性编辑组件不可见且不可用，因为 PushButton 不支持该属性。

`PDFFormProperty.onChange` 方法：

该方法用于监听属性变更，当用户选中表单域或表单域的属性值发生变更时，会触发变更事件回调。以下以 React 组件代码作为示例演示：

1. 首先，构造 hook 用于获取 PDFFormProperty 实例：

       function useFormDesignerAddonInstance() {
           const context = useContext(PDFUIContext); // PDFUIContext，用于共享 PDFUI 对象
           const pdfui = context.current;
           const [instance, setInstance] = useState();
           if (pdfui && instance) {
               pdfui.getAddonInstance('FormDesigner').then(instance => {
                   setInstance(instance);
               });
           }
           return instance;
       }
       function useFormPropertiesService() {
           const formDesigner = useFormDesignerAddonInstance();
           return formDesigner?.getPDFFormPropertiesService();
       }
       function usePDFFormProperty(pdfFormPropertyFactory) {
           const formPropertiesService = useFormPropertiesService();
           const [property, setProperty] = useState();
           useEffect(() => {
               if(property) {
                   return;
               }
               if(!formPropertiesService) {
                   return;
               }
               const property = pdfFormPropertyFactory(formPropertiesService);
               setProperty(property);
           }, [property, formPropertiesService])

           useEffect(() => {
               if(!property) {
                   return;
               }
               return property.onChange(() => {
                   setProperty(property);
               });
           }, [property])
           return property;
       }

2. 在 React 组件中使用：

       function FieldNameEditor() {
           const formPropertiesService = useFormPropertiesService();
           const fieldNameProperty = usePDFFormProperty(pdfFormPropertyService => {
               return pdfFormPropertyService.getFieldName();
           })
           const [value, setValue] = useState()
           // 由于 fieldNameProperty.value 是一个只读属性，直接在 <input> 上使用会导致用户无法编辑。因此，需要构造一个新的 state
           useEffect(() => {
               if(!fieldNameProperty?.hasValue) {
                   setValue('');
               } else {
                   setValue(fieldNameProperty.value)
               }
           }, [fieldNameProperty?.hasValue, fieldNameProperty?.value])
           return <input
               readonly={!fieldNameProperty?.available}
               className={fieldNameProperty?.visible ? '' : 'hide'}
               value={value}
               onChange={(event) => {
                   const newFieldName = event.target.value;
                   setValue(newFieldName);
                   const fields = formPropertiesService.getSelectedFields();
                   // 当用户修改内容后，需要将更新的数据设置到表单域中
                   fields.forEach(field => {
                       field.setName(newFieldName);
                   });
               }}
           ></input>
       }

注意： 以上示例仅用于演示用法，实际开发时需根据具体场景进行调整和实现。

### # 内置表单属性编辑组件

内置表单属性编辑组件可以参考 内置表单属性编辑组件 文档。

### # 自定义表单属性对话框

自定义表单属性对话框可参考 自定义表单属性对话框 文档。