import PDFDocument from 'src/api/PDFDocument' ; import PDFPage from 'src/api/PDFPage' ; import PDFFont from 'src/api/PDFFont' ; import PDFField , { FieldAppearanceOptions , assertFieldAppearanceOptions , } from 'src/api/form/PDFField' ; import { AppearanceProviderFor , normalizeAppearance , defaultDropdownAppearanceProvider , } from 'src/api/form/appearances' ; import { rgb } from 'src/api/colors' ; import { degrees } from 'src/api/rotations' ; import { PDFHexString , PDFRef , PDFString , PDFStream , PDFWidgetAnnotation , PDFAcroComboBox , AcroChoiceFlags , } from 'src/core' ; import { assertIs , assertOrUndefined , assertPositive } from 'src/utils' ; /**
 * Represents a dropdown field of a [[PDFForm]].
 *
 * [[PDFDropdown]] fields are interactive text boxes that display a single
 * element (the currently selected value). The purpose of a dropdown is to
 * enable users to select a single option from a set of possible options. Users
 * can click on a dropdown to view the full list of options it provides.
 * Clicking on an option in the list will cause it to be selected and displayed
 * in the dropdown's text box. Some dropdowns allow users to enter text
 * directly into the box from their keyboard, rather than only being allowed to
 * choose an option from the list (see [[PDFDropdown.isEditable]]).
 */ export default class PDFDropdown extends PDFField { param acroComboBox param ref param doc static of = ( acroComboBox , ref , doc ) => new PDFDropdown ( acroComboBox , ref , doc ) ; /** The low-level PDFAcroComboBox wrapped by this dropdown. */ readonly acroField : PDFAcroComboBox ; private constructor ( acroComboBox , ref , doc ) { super ( acroComboBox , ref , doc ) ; assertIs ( acroComboBox , 'acroComboBox' , [ [ PDFAcroComboBox , 'PDFAcroComboBox' ] , ] ) ; this . acroField = acroComboBox ; } returns getOptions ( ) { const rawOptions = this . acroField . getOptions ( ) ; const options = new Array < string > ( rawOptions . length ) ; for ( let idx = 0 , len = options . length ; idx < len ; idx ++ ) { const { display , value } = rawOptions [ idx ] ; options [ idx ] = ( display ?? value ) . decodeText ( ) ; } return options ; } returns getSelected ( ) { const values = this . acroField . getValues ( ) ; const selected = new Array < string > ( values . length ) ; for ( let idx = 0 , len = values . length ; idx < len ; idx ++ ) { selected [ idx ] = values [ idx ] . decodeText ( ) ; } return selected ; } param options setOptions ( options ) { assertIs ( options , 'options' , [ Array ] ) ; const optionObjects = new Array < { value : PDFHexString ; } > ( options . length ) ; for ( let idx = 0 , len = options . length ; idx < len ; idx ++ ) { optionObjects [ idx ] = { value : PDFHexString . fromText ( options [ idx ] ) } ; } this . acroField . setOptions ( optionObjects ) ; } param options addOptions ( options ) { assertIs ( options , 'options' , [ 'string' , Array ] ) ; const optionsArr = Array . isArray ( options ) ? options : [ options ] ; const existingOptions = this . acroField . getOptions ( ) ; const newOptions = new Array < { value : PDFHexString ; } > ( optionsArr . length ) ; for ( let idx = 0 , len = optionsArr . length ; idx < len ; idx ++ ) { newOptions [ idx ] = { value : PDFHexString . fromText ( optionsArr [ idx ] ) } ; } this . acroField . setOptions ( existingOptions . concat ( newOptions ) ) ; } param options param merge select ( options , merge = false ) { assertIs ( options , 'options' , [ 'string' , Array ] ) ; assertIs ( merge , 'merge' , [ 'boolean' ] ) ; const optionsArr = Array . isArray ( options ) ? options : [ options ] ; const validOptions = this . getOptions ( ) ; const hasCustomOption = optionsArr . find ( ( option ) => ! validOptions . includes ( option ) ) ; if ( hasCustomOption ) this . enableEditing ( ) ; this . markAsDirty ( ) ; if ( optionsArr . length > 1 || ( optionsArr . length === 1 && merge ) ) { this . enableMultiselect ( ) ; } const values = new Array < PDFHexString > ( optionsArr . length ) ; for ( let idx = 0 , len = optionsArr . length ; idx < len ; idx ++ ) { values [ idx ] = PDFHexString . fromText ( optionsArr [ idx ] ) ; } if ( merge ) { const existingValues = this . acroField . getValues ( ) ; this . acroField . setValues ( existingValues . concat ( values ) ) ; } else { this . acroField . setValues ( values ) ; } } /**
     * Clear all selected values for this dropdown. This operation is equivalent
     * to selecting an empty list. This method will update the underlying state
     * of the dropdown to indicate that no values have been selected.
     * For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.clear()
     * ```
     * This method will mark this text field as dirty. See [[PDFDropdown.select]]
     * for more details about what this means.
     */ clear ( ) { this . markAsDirty ( ) ; this . acroField . setValues ( [ ] ) ; } param fontSize setFontSize ( fontSize ) { assertPositive ( fontSize , 'fontSize' ) ; this . acroField . setFontSize ( fontSize ) ; this . markAsDirty ( ) ; } returns isEditable ( ) { return this . acroField . hasFlag ( AcroChoiceFlags . Edit ) ; } /**
     * Allow users to edit the selected value of this dropdown in PDF readers
     * with their keyboard. This means that the selected value of this dropdown
     * will not be constrained by the list of available options. However, if this
     * dropdown has any available options, users will still be allowed to select
     * from that list.
     * For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.enableEditing()
     * ```
     */ enableEditing ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . Edit , true ) ; } /**
     * Do not allow users to edit the selected value of this dropdown in PDF
     * readers with their keyboard. This will constrain the selected value of
     * this dropdown to the list of available options. Users will only be able
     * to select an option from that list.
     * For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.disableEditing()
     * ```
     */ disableEditing ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . Edit , false ) ; } returns isSorted ( ) { return this . acroField . hasFlag ( AcroChoiceFlags . Sort ) ; } /**
     * Always display the option list of this dropdown in alphabetical order,
     * irrespective of the order in which the options were added to this dropdown.
     * For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.enableSorting()
     * ```
     */ enableSorting ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . Sort , true ) ; } /**
     * Do not always display the option list of this dropdown in alphabetical
     * order. Instead, display the options in whichever order they were added
     * to the list. For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.disableSorting()
     * ```
     */ disableSorting ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . Sort , false ) ; } returns isMultiselect ( ) { return this . acroField . hasFlag ( AcroChoiceFlags . MultiSelect ) ; } /**
     * Allow users to select more than one option from this dropdown's option
     * list. For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.enableMultiselect()
     * ```
     */ enableMultiselect ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . MultiSelect , true ) ; } /**
     * Do not allow users to select more than one option from this dropdown's
     * option list. For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.disableMultiselect()
     * ```
     */ disableMultiselect ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . MultiSelect , false ) ; } returns isSpellChecked ( ) { return ! this . acroField . hasFlag ( AcroChoiceFlags . DoNotSpellCheck ) ; } /**
     * Allow PDF readers to spell check the selected option of this dropdown.
     * For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.enableSpellChecking()
     * ```
     */ enableSpellChecking ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . DoNotSpellCheck , false ) ; } /**
     * Do not allow PDF readers to spell check the selected option of this
     * dropdown. For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.disableSpellChecking()
     * ```
     */ disableSpellChecking ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . DoNotSpellCheck , true ) ; } returns isSelectOnClick ( ) { return this . acroField . hasFlag ( AcroChoiceFlags . CommitOnSelChange ) ; } /**
     * Store the option selected by a user immediately after the user clicks the
     * option. Do not wait for the user to leave this dropdown field (by clicking
     * outside of it - on another field, for example). For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.enableSelectOnClick()
     * ```
     */ enableSelectOnClick ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . CommitOnSelChange , true ) ; } /**
     * Wait to store the option selected by a user until they leave this dropdown
     * field (by clicking outside of it - on another field, for example).
     * For example:
     * ```js
     * const dropdown = form.getDropdown('some.dropdown.field')
     * dropdown.disableSelectOnClick()
     * ```
     */ disableSelectOnClick ( ) { this . acroField . setFlagTo ( AcroChoiceFlags . CommitOnSelChange , false ) ; } param page param options addToPage ( page , options ? ) { assertIs ( page , 'page' , [ [ PDFPage , 'PDFPage' ] ] ) ; assertFieldAppearanceOptions ( options ) ; if ( ! options ) options = { } ; if ( ! ( 'textColor' in options ) ) options . textColor = rgb ( 0 , 0 , 0 ) ; if ( ! ( 'backgroundColor' in options ) ) options . backgroundColor = rgb ( 1 , 1 , 1 ) ; if ( ! ( 'borderColor' in options ) ) options . borderColor = rgb ( 0 , 0 , 0 ) ; if ( ! ( 'borderWidth' in options ) ) options . borderWidth = 1 ; const widget = this . createWidget ( { x : options . x ?? 0 , y : options . y ?? 0 , width : options . width ?? 200 , height : options . height ?? 50 , textColor : options . textColor , backgroundColor : options . backgroundColor , borderColor : options . borderColor , borderWidth : options . borderWidth ?? 0 , rotate : options . rotate ?? degrees ( 0 ) , hidden : options . hidden , page : page . ref , } ) ; const widgetRef = this . doc . context . register ( widget . dict ) ; this . acroField . addWidget ( widgetRef ) ; const font = options . font ?? this . doc . getForm ( ) . getDefaultFont ( ) ; this . updateWidgetAppearance ( widget , font ) ; page . node . addAnnot ( widgetRef ) ; } returns needsAppearancesUpdate ( ) { if ( this . isDirty ( ) ) return true ; const widgets = this . acroField . getWidgets ( ) ; for ( let idx = 0 , len = widgets . length ; idx < len ; idx ++ ) { const widget = widgets [ idx ] ; const hasAppearances = widget . getAppearances ( ) ?. normal instanceof PDFStream ; if ( ! hasAppearances ) return true ; } return false ; } param font defaultUpdateAppearances ( font ) { assertIs ( font , 'font' , [ [ PDFFont , 'PDFFont' ] ] ) ; this . updateAppearances ( font ) ; } param font param provider updateAppearances ( font , provider ? ) { assertIs ( font , 'font' , [ [ PDFFont , 'PDFFont' ] ] ) ; assertOrUndefined ( provider , 'provider' , [ Function ] ) ; const widgets = this . acroField . getWidgets ( ) ; for ( let idx = 0 , len = widgets . length ; idx < len ; idx ++ ) { const widget = widgets [ idx ] ; this . updateWidgetAppearance ( widget , font , provider ) ; } this . markAsClean ( ) ; } private updateWidgetAppearance ( widget , font , provider ? ) { const apProvider = provider ?? defaultDropdownAppearanceProvider ; const appearances = normalizeAppearance ( apProvider ( this , widget , font ) ) ; this . updateWidgetAppearanceWithFont ( widget , font , appearances ) ; } }