react-native-platform-components 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PlatformComponents.podspec +1 -1
- package/README.md +47 -51
- package/android/src/main/java/com/platformcomponents/PCButtonGroupView.kt +664 -0
- package/android/src/main/java/com/platformcomponents/PCButtonGroupViewManager.kt +241 -0
- package/android/src/main/java/com/platformcomponents/PCButtonSupport.kt +159 -0
- package/android/src/main/java/com/platformcomponents/PCButtonView.kt +527 -0
- package/android/src/main/java/com/platformcomponents/PCButtonViewManager.kt +204 -0
- package/android/src/main/java/com/platformcomponents/PCContextMenuView.kt +41 -151
- package/android/src/main/java/com/platformcomponents/PCContextMenuViewManager.kt +7 -63
- package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +310 -115
- package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +23 -3
- package/android/src/main/java/com/platformcomponents/PCExpressive.kt +112 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingActionButtonView.kt +485 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingActionButtonViewManager.kt +109 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingToolbarView.kt +247 -0
- package/android/src/main/java/com/platformcomponents/PCFloatingToolbarViewManager.kt +73 -0
- package/android/src/main/java/com/platformcomponents/PCHaptics.kt +46 -0
- package/android/src/main/java/com/platformcomponents/PCImageLoader.kt +42 -7
- package/android/src/main/java/com/platformcomponents/PCLiquidGlassContainerView.kt +32 -0
- package/android/src/main/java/com/platformcomponents/PCLiquidGlassContainerViewManager.kt +31 -0
- package/android/src/main/java/com/platformcomponents/PCLiquidGlassView.kt +32 -6
- package/android/src/main/java/com/platformcomponents/PCLiquidGlassViewManager.kt +4 -0
- package/android/src/main/java/com/platformcomponents/PCMenuSupport.kt +280 -0
- package/android/src/main/java/com/platformcomponents/PCNavigationBarSupport.kt +332 -0
- package/android/src/main/java/com/platformcomponents/PCNavigationRailView.kt +448 -0
- package/android/src/main/java/com/platformcomponents/PCNavigationRailViewManager.kt +176 -0
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlView.kt +35 -17
- package/android/src/main/java/com/platformcomponents/PCSegmentedControlViewManager.kt +20 -3
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +371 -39
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +18 -4
- package/android/src/main/java/com/platformcomponents/PCTabBarView.kt +397 -0
- package/android/src/main/java/com/platformcomponents/PCTabBarViewManager.kt +182 -0
- package/android/src/main/java/com/platformcomponents/PCTextFieldView.kt +1393 -0
- package/android/src/main/java/com/platformcomponents/PCTextFieldViewManager.kt +354 -0
- package/android/src/main/java/com/platformcomponents/PCThemeSupport.kt +37 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +8 -0
- package/android/src/main/res/drawable/pc_ic_search.xml +12 -0
- package/android/src/main/res/values/attrs.xml +13 -0
- package/android/src/main/res/values/ids.xml +5 -0
- package/android/src/main/res/values/styles.xml +207 -0
- package/ios/PCButton.h +10 -0
- package/ios/PCButton.mm +311 -0
- package/ios/PCButtonGroup.h +10 -0
- package/ios/PCButtonGroup.mm +323 -0
- package/ios/PCButtonGroupView.swift +473 -0
- package/ios/PCButtonSupport.swift +312 -0
- package/ios/PCButtonView.swift +438 -0
- package/ios/PCConstants.swift +23 -2
- package/ios/PCContextMenu.mm +28 -110
- package/ios/PCContextMenu.swift +106 -167
- package/ios/PCDatePicker.mm +21 -9
- package/ios/PCDatePickerView.swift +94 -20
- package/ios/PCFloatingActionButton.h +10 -0
- package/ios/PCFloatingActionButton.mm +177 -0
- package/ios/PCFloatingActionButtonView.swift +426 -0
- package/ios/PCFloatingToolbar.h +10 -0
- package/ios/PCFloatingToolbar.mm +103 -0
- package/ios/PCFloatingToolbarView.swift +336 -0
- package/ios/PCHaptics.swift +91 -0
- package/ios/PCImageLoader.swift +76 -12
- package/ios/PCLiquidGlass.mm +36 -0
- package/ios/PCLiquidGlass.swift +221 -114
- package/ios/PCLiquidGlassContainer.h +10 -0
- package/ios/PCLiquidGlassContainer.mm +84 -0
- package/ios/PCLiquidGlassContainer.swift +69 -0
- package/ios/PCMenuItems.h +76 -0
- package/ios/PCMenuSupport.swift +350 -0
- package/ios/PCNavigationRail.h +10 -0
- package/ios/PCNavigationRail.mm +270 -0
- package/ios/PCNavigationRailView.swift +415 -0
- package/ios/PCSegmentedControl.mm +17 -2
- package/ios/PCSegmentedControl.swift +57 -7
- package/ios/PCSelectionMenu.mm +36 -8
- package/ios/PCSelectionMenu.swift +224 -48
- package/ios/PCTabBar.h +10 -0
- package/ios/PCTabBar.mm +285 -0
- package/ios/PCTabBarAccessory.h +10 -0
- package/ios/PCTabBarAccessory.mm +93 -0
- package/ios/PCTabBarAccessoryView.swift +96 -0
- package/ios/PCTabBarView.swift +671 -0
- package/ios/PCTextField.h +10 -0
- package/ios/PCTextField.mm +546 -0
- package/ios/PCTextFieldView.swift +1558 -0
- package/lib/commonjs/Button.js +149 -0
- package/lib/commonjs/Button.js.map +1 -0
- package/lib/commonjs/ButtonGroup.js +109 -0
- package/lib/commonjs/ButtonGroup.js.map +1 -0
- package/lib/commonjs/ButtonGroupNativeComponent.ts +191 -0
- package/lib/commonjs/ButtonNativeComponent.ts +192 -0
- package/lib/commonjs/ContextMenu.js +9 -48
- package/lib/commonjs/ContextMenu.js.map +1 -1
- package/lib/commonjs/ContextMenuNativeComponent.ts +47 -49
- package/lib/commonjs/DatePicker.js +98 -2
- package/lib/commonjs/DatePicker.js.map +1 -1
- package/lib/commonjs/DatePickerNativeComponent.ts +19 -2
- package/lib/commonjs/FloatingActionButton.js +60 -0
- package/lib/commonjs/FloatingActionButton.js.map +1 -0
- package/lib/commonjs/FloatingActionButtonNativeComponent.ts +58 -0
- package/lib/commonjs/FloatingToolbar.js +77 -0
- package/lib/commonjs/FloatingToolbar.js.map +1 -0
- package/lib/commonjs/FloatingToolbarNativeComponent.ts +47 -0
- package/lib/commonjs/LiquidGlass.js +7 -1
- package/lib/commonjs/LiquidGlass.js.map +1 -1
- package/lib/commonjs/LiquidGlassContainer.js +32 -0
- package/lib/commonjs/LiquidGlassContainer.js.map +1 -0
- package/lib/commonjs/LiquidGlassContainerNativeComponent.ts +17 -0
- package/lib/commonjs/LiquidGlassNativeComponent.ts +8 -0
- package/lib/commonjs/NavigationRail.js +104 -0
- package/lib/commonjs/NavigationRail.js.map +1 -0
- package/lib/commonjs/NavigationRailNativeComponent.ts +107 -0
- package/lib/commonjs/PlatformComponentsProvider.js +18 -0
- package/lib/commonjs/PlatformComponentsProvider.js.map +1 -0
- package/lib/commonjs/SegmentedControl.js +18 -85
- package/lib/commonjs/SegmentedControl.js.map +1 -1
- package/lib/commonjs/SegmentedControlNativeComponent.ts +14 -0
- package/lib/commonjs/SelectionMenu.js +27 -3
- package/lib/commonjs/SelectionMenu.js.map +1 -1
- package/lib/commonjs/SelectionMenuNativeComponent.ts +26 -4
- package/lib/commonjs/SplitButton.js +98 -0
- package/lib/commonjs/SplitButton.js.map +1 -0
- package/lib/commonjs/TabBar.js +203 -0
- package/lib/commonjs/TabBar.js.map +1 -0
- package/lib/commonjs/TabBarAccessoryNativeComponent.ts +19 -0
- package/lib/commonjs/TabBarNativeComponent.ts +145 -0
- package/lib/commonjs/TextField.js +358 -0
- package/lib/commonjs/TextField.js.map +1 -0
- package/lib/commonjs/TextFieldNativeComponent.ts +324 -0
- package/lib/commonjs/focusRegistry.js +79 -0
- package/lib/commonjs/focusRegistry.js.map +1 -0
- package/lib/commonjs/haptics.js +2 -0
- package/lib/commonjs/haptics.js.map +1 -0
- package/lib/commonjs/icons.js +106 -0
- package/lib/commonjs/icons.js.map +1 -0
- package/lib/commonjs/index.js +143 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +157 -63
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/jest.js +910 -0
- package/lib/commonjs/jest.js.map +1 -0
- package/lib/commonjs/labelStyle.js +27 -0
- package/lib/commonjs/labelStyle.js.map +1 -0
- package/lib/commonjs/menuItems.js +68 -0
- package/lib/commonjs/menuItems.js.map +1 -0
- package/lib/commonjs/reactNativeInternals.d.js +2 -0
- package/lib/commonjs/reactNativeInternals.d.js.map +1 -0
- package/lib/commonjs/selectionIdentifiers.js +18 -0
- package/lib/commonjs/selectionIdentifiers.js.map +1 -0
- package/lib/commonjs/submitBehavior.js +19 -0
- package/lib/commonjs/submitBehavior.js.map +1 -0
- package/lib/commonjs/tabItems.js +49 -0
- package/lib/commonjs/tabItems.js.map +1 -0
- package/lib/commonjs/web/Button.js +170 -0
- package/lib/commonjs/web/Button.js.map +1 -0
- package/lib/commonjs/web/ButtonGroup.js +119 -0
- package/lib/commonjs/web/ButtonGroup.js.map +1 -0
- package/lib/commonjs/web/DatePicker.js +168 -0
- package/lib/commonjs/web/DatePicker.js.map +1 -0
- package/lib/commonjs/web/DateRangePicker.js +137 -0
- package/lib/commonjs/web/DateRangePicker.js.map +1 -0
- package/lib/commonjs/web/Dialog.js +54 -0
- package/lib/commonjs/web/Dialog.js.map +1 -0
- package/lib/commonjs/web/FloatingActionButton.js +98 -0
- package/lib/commonjs/web/FloatingActionButton.js.map +1 -0
- package/lib/commonjs/web/Icon.js +34 -0
- package/lib/commonjs/web/Icon.js.map +1 -0
- package/lib/commonjs/web/NativeTheme.js +28 -0
- package/lib/commonjs/web/NativeTheme.js.map +1 -0
- package/lib/commonjs/web/NavigationRail.js +139 -0
- package/lib/commonjs/web/NavigationRail.js.map +1 -0
- package/lib/commonjs/web/PlatformComponentsProvider.js +51 -0
- package/lib/commonjs/web/PlatformComponentsProvider.js.map +1 -0
- package/lib/commonjs/web/SegmentedControl.js +117 -0
- package/lib/commonjs/web/SegmentedControl.js.map +1 -0
- package/lib/commonjs/web/SelectionMenu.js +142 -0
- package/lib/commonjs/web/SelectionMenu.js.map +1 -0
- package/lib/commonjs/web/SplitButton.js +35 -0
- package/lib/commonjs/web/SplitButton.js.map +1 -0
- package/lib/commonjs/web/TabBar.js +131 -0
- package/lib/commonjs/web/TabBar.js.map +1 -0
- package/lib/commonjs/web/TextField.js +322 -0
- package/lib/commonjs/web/TextField.js.map +1 -0
- package/lib/commonjs/web/components.js +36 -0
- package/lib/commonjs/web/components.js.map +1 -0
- package/lib/commonjs/web/containers.js +121 -0
- package/lib/commonjs/web/containers.js.map +1 -0
- package/lib/commonjs/web/dateInput.js +116 -0
- package/lib/commonjs/web/dateInput.js.map +1 -0
- package/lib/commonjs/web/shared.js +192 -0
- package/lib/commonjs/web/shared.js.map +1 -0
- package/lib/commonjs/webComponents.js +6 -0
- package/lib/commonjs/webComponents.js.map +1 -0
- package/lib/module/Button.js +142 -0
- package/lib/module/Button.js.map +1 -0
- package/lib/module/ButtonGroup.js +102 -0
- package/lib/module/ButtonGroup.js.map +1 -0
- package/lib/module/ButtonGroupNativeComponent.ts +191 -0
- package/lib/module/ButtonNativeComponent.ts +192 -0
- package/lib/module/ContextMenu.js +9 -48
- package/lib/module/ContextMenu.js.map +1 -1
- package/lib/module/ContextMenuNativeComponent.ts +47 -49
- package/lib/module/DatePicker.js +98 -4
- package/lib/module/DatePicker.js.map +1 -1
- package/lib/module/DatePickerNativeComponent.ts +19 -2
- package/lib/module/FloatingActionButton.js +53 -0
- package/lib/module/FloatingActionButton.js.map +1 -0
- package/lib/module/FloatingActionButtonNativeComponent.ts +58 -0
- package/lib/module/FloatingToolbar.js +70 -0
- package/lib/module/FloatingToolbar.js.map +1 -0
- package/lib/module/FloatingToolbarNativeComponent.ts +47 -0
- package/lib/module/LiquidGlass.js +7 -1
- package/lib/module/LiquidGlass.js.map +1 -1
- package/lib/module/LiquidGlassContainer.js +26 -0
- package/lib/module/LiquidGlassContainer.js.map +1 -0
- package/lib/module/LiquidGlassContainerNativeComponent.ts +17 -0
- package/lib/module/LiquidGlassNativeComponent.ts +8 -0
- package/lib/module/NavigationRail.js +99 -0
- package/lib/module/NavigationRail.js.map +1 -0
- package/lib/module/NavigationRailNativeComponent.ts +107 -0
- package/lib/module/PlatformComponentsProvider.js +13 -0
- package/lib/module/PlatformComponentsProvider.js.map +1 -0
- package/lib/module/SegmentedControl.js +18 -85
- package/lib/module/SegmentedControl.js.map +1 -1
- package/lib/module/SegmentedControlNativeComponent.ts +14 -0
- package/lib/module/SelectionMenu.js +27 -3
- package/lib/module/SelectionMenu.js.map +1 -1
- package/lib/module/SelectionMenuNativeComponent.ts +26 -4
- package/lib/module/SplitButton.js +91 -0
- package/lib/module/SplitButton.js.map +1 -0
- package/lib/module/TabBar.js +196 -0
- package/lib/module/TabBar.js.map +1 -0
- package/lib/module/TabBarAccessoryNativeComponent.ts +19 -0
- package/lib/module/TabBarNativeComponent.ts +145 -0
- package/lib/module/TextField.js +352 -0
- package/lib/module/TextField.js.map +1 -0
- package/lib/module/TextFieldNativeComponent.ts +324 -0
- package/lib/module/focusRegistry.js +75 -0
- package/lib/module/focusRegistry.js.map +1 -0
- package/lib/module/haptics.js +2 -0
- package/lib/module/haptics.js.map +1 -0
- package/lib/module/icons.js +101 -0
- package/lib/module/icons.js.map +1 -0
- package/lib/module/index.js +13 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +12 -59
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/jest.js +840 -0
- package/lib/module/jest.js.map +1 -0
- package/lib/module/labelStyle.js +23 -0
- package/lib/module/labelStyle.js.map +1 -0
- package/lib/module/menuItems.js +65 -0
- package/lib/module/menuItems.js.map +1 -0
- package/lib/module/reactNativeInternals.d.js +2 -0
- package/lib/module/reactNativeInternals.d.js.map +1 -0
- package/lib/module/selectionIdentifiers.js +14 -0
- package/lib/module/selectionIdentifiers.js.map +1 -0
- package/lib/module/submitBehavior.js +15 -0
- package/lib/module/submitBehavior.js.map +1 -0
- package/lib/module/tabItems.js +45 -0
- package/lib/module/tabItems.js.map +1 -0
- package/lib/module/web/Button.js +164 -0
- package/lib/module/web/Button.js.map +1 -0
- package/lib/module/web/ButtonGroup.js +113 -0
- package/lib/module/web/ButtonGroup.js.map +1 -0
- package/lib/module/web/DatePicker.js +162 -0
- package/lib/module/web/DatePicker.js.map +1 -0
- package/lib/module/web/DateRangePicker.js +131 -0
- package/lib/module/web/DateRangePicker.js.map +1 -0
- package/lib/module/web/Dialog.js +48 -0
- package/lib/module/web/Dialog.js.map +1 -0
- package/lib/module/web/FloatingActionButton.js +94 -0
- package/lib/module/web/FloatingActionButton.js.map +1 -0
- package/lib/module/web/Icon.js +29 -0
- package/lib/module/web/Icon.js.map +1 -0
- package/lib/module/web/NativeTheme.js +23 -0
- package/lib/module/web/NativeTheme.js.map +1 -0
- package/lib/module/web/NavigationRail.js +133 -0
- package/lib/module/web/NavigationRail.js.map +1 -0
- package/lib/module/web/PlatformComponentsProvider.js +44 -0
- package/lib/module/web/PlatformComponentsProvider.js.map +1 -0
- package/lib/module/web/SegmentedControl.js +112 -0
- package/lib/module/web/SegmentedControl.js.map +1 -0
- package/lib/module/web/SelectionMenu.js +137 -0
- package/lib/module/web/SelectionMenu.js.map +1 -0
- package/lib/module/web/SplitButton.js +30 -0
- package/lib/module/web/SplitButton.js.map +1 -0
- package/lib/module/web/TabBar.js +126 -0
- package/lib/module/web/TabBar.js.map +1 -0
- package/lib/module/web/TextField.js +316 -0
- package/lib/module/web/TextField.js.map +1 -0
- package/lib/module/web/components.js +33 -0
- package/lib/module/web/components.js.map +1 -0
- package/lib/module/web/containers.js +112 -0
- package/lib/module/web/containers.js.map +1 -0
- package/lib/module/web/dateInput.js +109 -0
- package/lib/module/web/dateInput.js.map +1 -0
- package/lib/module/web/shared.js +179 -0
- package/lib/module/web/shared.js.map +1 -0
- package/lib/module/webComponents.js +4 -0
- package/lib/module/webComponents.js.map +1 -0
- package/lib/typescript/commonjs/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/Button.d.ts +184 -0
- package/lib/typescript/commonjs/src/Button.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroup.d.ts +117 -0
- package/lib/typescript/commonjs/src/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts +154 -0
- package/lib/typescript/commonjs/src/ButtonGroupNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts +148 -0
- package/lib/typescript/commonjs/src/ButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/ContextMenu.d.ts +20 -33
- package/lib/typescript/commonjs/src/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts +45 -47
- package/lib/typescript/commonjs/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/DatePicker.d.ts +74 -1
- package/lib/typescript/commonjs/src/DatePicker.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts +16 -1
- package/lib/typescript/commonjs/src/DatePickerNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/FloatingActionButton.d.ts +67 -0
- package/lib/typescript/commonjs/src/FloatingActionButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingActionButtonNativeComponent.d.ts +43 -0
- package/lib/typescript/commonjs/src/FloatingActionButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingToolbar.d.ts +75 -0
- package/lib/typescript/commonjs/src/FloatingToolbar.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts +36 -0
- package/lib/typescript/commonjs/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/LiquidGlass.d.ts +17 -0
- package/lib/typescript/commonjs/src/LiquidGlass.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/LiquidGlassContainer.d.ts +27 -0
- package/lib/typescript/commonjs/src/LiquidGlassContainer.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/LiquidGlassContainerNativeComponent.d.ts +13 -0
- package/lib/typescript/commonjs/src/LiquidGlassContainerNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts +7 -0
- package/lib/typescript/commonjs/src/LiquidGlassNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/NavigationRail.d.ts +86 -0
- package/lib/typescript/commonjs/src/NavigationRail.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/NavigationRailNativeComponent.d.ts +81 -0
- package/lib/typescript/commonjs/src/NavigationRailNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/PlatformComponentsProvider.d.ts +6 -0
- package/lib/typescript/commonjs/src/PlatformComponentsProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts +35 -47
- package/lib/typescript/commonjs/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts +11 -0
- package/lib/typescript/commonjs/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SelectionMenu.d.ts +44 -2
- package/lib/typescript/commonjs/src/SelectionMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts +20 -4
- package/lib/typescript/commonjs/src/SelectionMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/SplitButton.d.ts +83 -0
- package/lib/typescript/commonjs/src/SplitButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TabBar.d.ts +144 -0
- package/lib/typescript/commonjs/src/TabBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TabBarAccessoryNativeComponent.d.ts +13 -0
- package/lib/typescript/commonjs/src/TabBarAccessoryNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TabBarNativeComponent.d.ts +110 -0
- package/lib/typescript/commonjs/src/TabBarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TextField.d.ts +332 -0
- package/lib/typescript/commonjs/src/TextField.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts +240 -0
- package/lib/typescript/commonjs/src/TextFieldNativeComponent.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/focusRegistry.d.ts +13 -0
- package/lib/typescript/commonjs/src/focusRegistry.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/haptics.d.ts +20 -0
- package/lib/typescript/commonjs/src/haptics.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/icons.d.ts +58 -0
- package/lib/typescript/commonjs/src/icons.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/index.d.ts +13 -0
- package/lib/typescript/commonjs/src/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/index.web.d.ts +25 -133
- package/lib/typescript/commonjs/src/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/jest.d.ts +139 -0
- package/lib/typescript/commonjs/src/jest.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/labelStyle.d.ts +20 -0
- package/lib/typescript/commonjs/src/labelStyle.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/menuItems.d.ts +100 -0
- package/lib/typescript/commonjs/src/menuItems.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts +3 -0
- package/lib/typescript/commonjs/src/selectionIdentifiers.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/sharedTypes.d.ts +6 -1
- package/lib/typescript/commonjs/src/sharedTypes.d.ts.map +1 -1
- package/lib/typescript/commonjs/src/submitBehavior.d.ts +9 -0
- package/lib/typescript/commonjs/src/submitBehavior.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/tabItems.d.ts +55 -0
- package/lib/typescript/commonjs/src/tabItems.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/Button.d.ts +9 -0
- package/lib/typescript/commonjs/src/web/Button.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/ButtonGroup.d.ts +11 -0
- package/lib/typescript/commonjs/src/web/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/DatePicker.d.ts +14 -0
- package/lib/typescript/commonjs/src/web/DatePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/DateRangePicker.d.ts +11 -0
- package/lib/typescript/commonjs/src/web/DateRangePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/Dialog.d.ts +12 -0
- package/lib/typescript/commonjs/src/web/Dialog.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/FloatingActionButton.d.ts +8 -0
- package/lib/typescript/commonjs/src/web/FloatingActionButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/Icon.d.ts +12 -0
- package/lib/typescript/commonjs/src/web/Icon.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/NativeTheme.d.ts +10 -0
- package/lib/typescript/commonjs/src/web/NativeTheme.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/NavigationRail.d.ts +8 -0
- package/lib/typescript/commonjs/src/web/NavigationRail.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/PlatformComponentsProvider.d.ts +10 -0
- package/lib/typescript/commonjs/src/web/PlatformComponentsProvider.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/SegmentedControl.d.ts +8 -0
- package/lib/typescript/commonjs/src/web/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/SelectionMenu.d.ts +10 -0
- package/lib/typescript/commonjs/src/web/SelectionMenu.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/SplitButton.d.ts +9 -0
- package/lib/typescript/commonjs/src/web/SplitButton.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/TabBar.d.ts +9 -0
- package/lib/typescript/commonjs/src/web/TabBar.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/TextField.d.ts +9 -0
- package/lib/typescript/commonjs/src/web/TextField.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/components.d.ts +17 -0
- package/lib/typescript/commonjs/src/web/components.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/containers.d.ts +24 -0
- package/lib/typescript/commonjs/src/web/containers.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/dateInput.d.ts +20 -0
- package/lib/typescript/commonjs/src/web/dateInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/web/shared.d.ts +43 -0
- package/lib/typescript/commonjs/src/web/shared.d.ts.map +1 -0
- package/lib/typescript/commonjs/src/webComponents.d.ts +43 -0
- package/lib/typescript/commonjs/src/webComponents.d.ts.map +1 -0
- package/lib/typescript/module/docs/sidebars.d.ts.map +1 -1
- package/lib/typescript/module/src/Button.d.ts +184 -0
- package/lib/typescript/module/src/Button.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroup.d.ts +117 -0
- package/lib/typescript/module/src/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts +154 -0
- package/lib/typescript/module/src/ButtonGroupNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts +148 -0
- package/lib/typescript/module/src/ButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/ContextMenu.d.ts +20 -33
- package/lib/typescript/module/src/ContextMenu.d.ts.map +1 -1
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts +45 -47
- package/lib/typescript/module/src/ContextMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/DatePicker.d.ts +74 -1
- package/lib/typescript/module/src/DatePicker.d.ts.map +1 -1
- package/lib/typescript/module/src/DatePickerNativeComponent.d.ts +16 -1
- package/lib/typescript/module/src/DatePickerNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/FloatingActionButton.d.ts +67 -0
- package/lib/typescript/module/src/FloatingActionButton.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingActionButtonNativeComponent.d.ts +43 -0
- package/lib/typescript/module/src/FloatingActionButtonNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingToolbar.d.ts +75 -0
- package/lib/typescript/module/src/FloatingToolbar.d.ts.map +1 -0
- package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts +36 -0
- package/lib/typescript/module/src/FloatingToolbarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/LiquidGlass.d.ts +17 -0
- package/lib/typescript/module/src/LiquidGlass.d.ts.map +1 -1
- package/lib/typescript/module/src/LiquidGlassContainer.d.ts +27 -0
- package/lib/typescript/module/src/LiquidGlassContainer.d.ts.map +1 -0
- package/lib/typescript/module/src/LiquidGlassContainerNativeComponent.d.ts +13 -0
- package/lib/typescript/module/src/LiquidGlassContainerNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts +7 -0
- package/lib/typescript/module/src/LiquidGlassNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/NavigationRail.d.ts +86 -0
- package/lib/typescript/module/src/NavigationRail.d.ts.map +1 -0
- package/lib/typescript/module/src/NavigationRailNativeComponent.d.ts +81 -0
- package/lib/typescript/module/src/NavigationRailNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/PlatformComponentsProvider.d.ts +6 -0
- package/lib/typescript/module/src/PlatformComponentsProvider.d.ts.map +1 -0
- package/lib/typescript/module/src/SegmentedControl.d.ts +35 -47
- package/lib/typescript/module/src/SegmentedControl.d.ts.map +1 -1
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts +11 -0
- package/lib/typescript/module/src/SegmentedControlNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/SelectionMenu.d.ts +44 -2
- package/lib/typescript/module/src/SelectionMenu.d.ts.map +1 -1
- package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts +20 -4
- package/lib/typescript/module/src/SelectionMenuNativeComponent.d.ts.map +1 -1
- package/lib/typescript/module/src/SplitButton.d.ts +83 -0
- package/lib/typescript/module/src/SplitButton.d.ts.map +1 -0
- package/lib/typescript/module/src/TabBar.d.ts +144 -0
- package/lib/typescript/module/src/TabBar.d.ts.map +1 -0
- package/lib/typescript/module/src/TabBarAccessoryNativeComponent.d.ts +13 -0
- package/lib/typescript/module/src/TabBarAccessoryNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/TabBarNativeComponent.d.ts +110 -0
- package/lib/typescript/module/src/TabBarNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/TextField.d.ts +332 -0
- package/lib/typescript/module/src/TextField.d.ts.map +1 -0
- package/lib/typescript/module/src/TextFieldNativeComponent.d.ts +240 -0
- package/lib/typescript/module/src/TextFieldNativeComponent.d.ts.map +1 -0
- package/lib/typescript/module/src/focusRegistry.d.ts +13 -0
- package/lib/typescript/module/src/focusRegistry.d.ts.map +1 -0
- package/lib/typescript/module/src/haptics.d.ts +20 -0
- package/lib/typescript/module/src/haptics.d.ts.map +1 -0
- package/lib/typescript/module/src/icons.d.ts +58 -0
- package/lib/typescript/module/src/icons.d.ts.map +1 -0
- package/lib/typescript/module/src/index.d.ts +13 -0
- package/lib/typescript/module/src/index.d.ts.map +1 -1
- package/lib/typescript/module/src/index.web.d.ts +25 -133
- package/lib/typescript/module/src/index.web.d.ts.map +1 -1
- package/lib/typescript/module/src/jest.d.ts +139 -0
- package/lib/typescript/module/src/jest.d.ts.map +1 -0
- package/lib/typescript/module/src/labelStyle.d.ts +20 -0
- package/lib/typescript/module/src/labelStyle.d.ts.map +1 -0
- package/lib/typescript/module/src/menuItems.d.ts +100 -0
- package/lib/typescript/module/src/menuItems.d.ts.map +1 -0
- package/lib/typescript/module/src/selectionIdentifiers.d.ts +3 -0
- package/lib/typescript/module/src/selectionIdentifiers.d.ts.map +1 -0
- package/lib/typescript/module/src/sharedTypes.d.ts +6 -1
- package/lib/typescript/module/src/sharedTypes.d.ts.map +1 -1
- package/lib/typescript/module/src/submitBehavior.d.ts +9 -0
- package/lib/typescript/module/src/submitBehavior.d.ts.map +1 -0
- package/lib/typescript/module/src/tabItems.d.ts +55 -0
- package/lib/typescript/module/src/tabItems.d.ts.map +1 -0
- package/lib/typescript/module/src/web/Button.d.ts +9 -0
- package/lib/typescript/module/src/web/Button.d.ts.map +1 -0
- package/lib/typescript/module/src/web/ButtonGroup.d.ts +11 -0
- package/lib/typescript/module/src/web/ButtonGroup.d.ts.map +1 -0
- package/lib/typescript/module/src/web/DatePicker.d.ts +14 -0
- package/lib/typescript/module/src/web/DatePicker.d.ts.map +1 -0
- package/lib/typescript/module/src/web/DateRangePicker.d.ts +11 -0
- package/lib/typescript/module/src/web/DateRangePicker.d.ts.map +1 -0
- package/lib/typescript/module/src/web/Dialog.d.ts +12 -0
- package/lib/typescript/module/src/web/Dialog.d.ts.map +1 -0
- package/lib/typescript/module/src/web/FloatingActionButton.d.ts +8 -0
- package/lib/typescript/module/src/web/FloatingActionButton.d.ts.map +1 -0
- package/lib/typescript/module/src/web/Icon.d.ts +12 -0
- package/lib/typescript/module/src/web/Icon.d.ts.map +1 -0
- package/lib/typescript/module/src/web/NativeTheme.d.ts +10 -0
- package/lib/typescript/module/src/web/NativeTheme.d.ts.map +1 -0
- package/lib/typescript/module/src/web/NavigationRail.d.ts +8 -0
- package/lib/typescript/module/src/web/NavigationRail.d.ts.map +1 -0
- package/lib/typescript/module/src/web/PlatformComponentsProvider.d.ts +10 -0
- package/lib/typescript/module/src/web/PlatformComponentsProvider.d.ts.map +1 -0
- package/lib/typescript/module/src/web/SegmentedControl.d.ts +8 -0
- package/lib/typescript/module/src/web/SegmentedControl.d.ts.map +1 -0
- package/lib/typescript/module/src/web/SelectionMenu.d.ts +10 -0
- package/lib/typescript/module/src/web/SelectionMenu.d.ts.map +1 -0
- package/lib/typescript/module/src/web/SplitButton.d.ts +9 -0
- package/lib/typescript/module/src/web/SplitButton.d.ts.map +1 -0
- package/lib/typescript/module/src/web/TabBar.d.ts +9 -0
- package/lib/typescript/module/src/web/TabBar.d.ts.map +1 -0
- package/lib/typescript/module/src/web/TextField.d.ts +9 -0
- package/lib/typescript/module/src/web/TextField.d.ts.map +1 -0
- package/lib/typescript/module/src/web/components.d.ts +17 -0
- package/lib/typescript/module/src/web/components.d.ts.map +1 -0
- package/lib/typescript/module/src/web/containers.d.ts +24 -0
- package/lib/typescript/module/src/web/containers.d.ts.map +1 -0
- package/lib/typescript/module/src/web/dateInput.d.ts +20 -0
- package/lib/typescript/module/src/web/dateInput.d.ts.map +1 -0
- package/lib/typescript/module/src/web/shared.d.ts +43 -0
- package/lib/typescript/module/src/web/shared.d.ts.map +1 -0
- package/lib/typescript/module/src/webComponents.d.ts +43 -0
- package/lib/typescript/module/src/webComponents.d.ts.map +1 -0
- package/package.json +55 -4
- package/plugin/build/index.d.ts +3 -2
- package/plugin/build/index.d.ts.map +1 -1
- package/react-native.config.js +6 -0
- package/shared/PCButtonComponentDescriptors-custom.h +22 -0
- package/shared/PCButtonGroupComponentDescriptors-custom.h +22 -0
- package/shared/PCButtonGroupShadowNode-custom.cpp +48 -0
- package/shared/PCButtonGroupShadowNode-custom.h +55 -0
- package/shared/PCButtonGroupState-custom.h +61 -0
- package/shared/PCButtonShadowNode-custom.cpp +48 -0
- package/shared/PCButtonShadowNode-custom.h +55 -0
- package/shared/PCButtonState-custom.h +61 -0
- package/shared/PCFloatingActionButtonComponentDescriptors-custom.h +22 -0
- package/shared/PCFloatingActionButtonShadowNode-custom.cpp +48 -0
- package/shared/PCFloatingActionButtonShadowNode-custom.h +54 -0
- package/shared/PCFloatingActionButtonState-custom.h +61 -0
- package/shared/PCNavigationRailComponentDescriptors-custom.h +47 -0
- package/shared/PCNavigationRailShadowNode-custom.h +42 -0
- package/shared/PCNavigationRailState-custom.h +62 -0
- package/shared/PCTabBarComponentDescriptors-custom.h +22 -0
- package/shared/PCTabBarShadowNode-custom.cpp +54 -0
- package/shared/PCTabBarShadowNode-custom.h +54 -0
- package/shared/PCTabBarState-custom.h +62 -0
- package/shared/PCTextFieldComponentDescriptors-custom.h +22 -0
- package/shared/PCTextFieldShadowNode-custom.cpp +47 -0
- package/shared/PCTextFieldShadowNode-custom.h +55 -0
- package/shared/PCTextFieldState-custom.h +61 -0
- package/shared/react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h +6 -0
- package/src/Button.tsx +359 -0
- package/src/ButtonGroup.tsx +251 -0
- package/src/ButtonGroupNativeComponent.ts +191 -0
- package/src/ButtonNativeComponent.ts +192 -0
- package/src/ContextMenu.tsx +34 -81
- package/src/ContextMenuNativeComponent.ts +47 -49
- package/src/DatePicker.tsx +182 -4
- package/src/DatePickerNativeComponent.ts +19 -2
- package/src/FloatingActionButton.tsx +121 -0
- package/src/FloatingActionButtonNativeComponent.ts +58 -0
- package/src/FloatingToolbar.tsx +164 -0
- package/src/FloatingToolbarNativeComponent.ts +47 -0
- package/src/LiquidGlass.tsx +24 -1
- package/src/LiquidGlassContainer.tsx +42 -0
- package/src/LiquidGlassContainerNativeComponent.ts +17 -0
- package/src/LiquidGlassNativeComponent.ts +8 -0
- package/src/NavigationRail.tsx +203 -0
- package/src/NavigationRailNativeComponent.ts +107 -0
- package/src/PlatformComponentsProvider.tsx +15 -0
- package/src/SegmentedControl.tsx +57 -119
- package/src/SegmentedControlNativeComponent.ts +14 -0
- package/src/SelectionMenu.tsx +81 -6
- package/src/SelectionMenuNativeComponent.ts +26 -4
- package/src/SplitButton.tsx +198 -0
- package/src/TabBar.tsx +373 -0
- package/src/TabBarAccessoryNativeComponent.ts +19 -0
- package/src/TabBarNativeComponent.ts +145 -0
- package/src/TextField.tsx +908 -0
- package/src/TextFieldNativeComponent.ts +324 -0
- package/src/focusRegistry.ts +90 -0
- package/src/haptics.ts +31 -0
- package/src/icons.ts +143 -0
- package/src/index.tsx +13 -0
- package/src/index.web.tsx +53 -199
- package/src/jest.tsx +958 -0
- package/src/labelStyle.ts +36 -0
- package/src/menuItems.ts +152 -0
- package/src/reactNativeInternals.d.ts +22 -0
- package/src/selectionIdentifiers.ts +17 -0
- package/src/sharedTypes.ts +7 -1
- package/src/submitBehavior.ts +16 -0
- package/src/tabItems.ts +113 -0
- package/src/web/Button.tsx +201 -0
- package/src/web/ButtonGroup.tsx +154 -0
- package/src/web/DatePicker.tsx +204 -0
- package/src/web/DateRangePicker.tsx +135 -0
- package/src/web/Dialog.tsx +54 -0
- package/src/web/FloatingActionButton.tsx +76 -0
- package/src/web/Icon.tsx +33 -0
- package/src/web/NativeTheme.ts +23 -0
- package/src/web/NavigationRail.tsx +148 -0
- package/src/web/PlatformComponentsProvider.tsx +48 -0
- package/src/web/SegmentedControl.tsx +127 -0
- package/src/web/SelectionMenu.tsx +149 -0
- package/src/web/SplitButton.tsx +32 -0
- package/src/web/TabBar.tsx +150 -0
- package/src/web/TextField.tsx +361 -0
- package/src/web/components.tsx +71 -0
- package/src/web/containers.tsx +130 -0
- package/src/web/dateInput.ts +137 -0
- package/src/web/shared.ts +176 -0
- package/src/webComponents.ts +45 -0
- package/android/src/main/java/com/platformcomponents/DateConstraints.kt +0 -83
|
@@ -0,0 +1,1558 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// A native text field: a caption label, a `UITextField` (or a growing
|
|
4
|
+
/// `UITextView` for multi-line text) and supporting text below, with the
|
|
5
|
+
/// text traits iOS adds each year exposed as props.
|
|
6
|
+
///
|
|
7
|
+
/// The text and its edit counter live here. User edits report the counter
|
|
8
|
+
/// with the text; JS pushes a controlled value back with the last counter it
|
|
9
|
+
/// saw, and a push older than the latest edit is dropped so a slow JS
|
|
10
|
+
/// round-trip can't erase what was typed meanwhile.
|
|
11
|
+
@objcMembers
|
|
12
|
+
public final class PCTextFieldView: UIView, UITextFieldDelegate, UITextViewDelegate, UIGestureRecognizerDelegate {
|
|
13
|
+
// MARK: - Props (set from ObjC++)
|
|
14
|
+
|
|
15
|
+
public var label: String = "" {
|
|
16
|
+
didSet { if oldValue != label { applyLabel() } }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public var placeholder: String = "" {
|
|
20
|
+
didSet { if oldValue != placeholder { applyPlaceholder() } }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public var supportingText: String = "" {
|
|
24
|
+
didSet { if oldValue != supportingText { applyFooter() } }
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// "none" | "error"
|
|
28
|
+
public var errorState: String = "none" {
|
|
29
|
+
didSet { if oldValue != errorState { applyFooter(); applyColors() } }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public var errorText: String = "" {
|
|
33
|
+
didSet { if oldValue != errorText { applyFooter() } }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public var prefix: String = "" {
|
|
37
|
+
didSet { if oldValue != prefix { applyAccessories() } }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public var suffix: String = "" {
|
|
41
|
+
didSet { if oldValue != suffix { applyAccessories() } }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// "never" | "while-editing" | "unless-editing" | "always"
|
|
45
|
+
public var clearButtonMode: String = "never" {
|
|
46
|
+
didSet { if oldValue != clearButtonMode { applyAccessories() } }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public var passwordToggle: Bool = false {
|
|
50
|
+
didSet { if oldValue != passwordToggle { applyAccessories() } }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public var characterCount: Bool = false {
|
|
54
|
+
didSet { if oldValue != characterCount { applyFooter() } }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// 0 = unlimited
|
|
58
|
+
public var maxLength: Int = 0 {
|
|
59
|
+
didSet { if oldValue != maxLength { applyFooter() } }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public var keyboardType: String = "default" {
|
|
63
|
+
didSet { if oldValue != keyboardType { applyTraits() } }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public var returnKeyType: String = "default" {
|
|
67
|
+
didSet { if oldValue != returnKeyType { applyTraits() } }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public var autoCapitalize: String = "sentences" {
|
|
71
|
+
didSet { if oldValue != autoCapitalize { applyTraits() } }
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public var autoCorrect: Bool = true {
|
|
75
|
+
didSet { if oldValue != autoCorrect { applyTraits() } }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public var secureTextEntry: Bool = false {
|
|
79
|
+
didSet { if oldValue != secureTextEntry { revealed = false; applyTraits(); applyAccessories() } }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public var multiline: Bool = false {
|
|
83
|
+
didSet { if oldValue != multiline { rebuildInput() } }
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/// What the return key does: "submit" | "blurAndSubmit" | "newline";
|
|
87
|
+
/// "" keeps the defaults (a newline in multi-line fields, else blur and submit)
|
|
88
|
+
public var submitBehavior: String = ""
|
|
89
|
+
|
|
90
|
+
/// UITextInputPasswordRules descriptor; "" = none
|
|
91
|
+
public var passwordRules: String = "" {
|
|
92
|
+
didSet { if oldValue != passwordRules { applyTraits() } }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// The keyboard toolbar's items (kind, itemId, title, systemItem, icon
|
|
96
|
+
/// fields, prominent, accessibilityLabel, testID); empty = no toolbar
|
|
97
|
+
public var keyboardToolbarItems: [NSDictionary] = [] {
|
|
98
|
+
didSet { if oldValue != keyboardToolbarItems { applyKeyboardToolbar() } }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// "enabled" | "disabled"
|
|
102
|
+
public var interactivity: String = "enabled" {
|
|
103
|
+
didSet { if oldValue != interactivity { applyEnabled() } }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public var autoFocus: Bool = false {
|
|
107
|
+
didSet { if autoFocus, window != nil { performAutoFocus() } }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public var selectTextOnFocus: Bool = false
|
|
111
|
+
|
|
112
|
+
/// React Native autoComplete value; "" = default
|
|
113
|
+
public var autoComplete: String = "" {
|
|
114
|
+
didSet { if oldValue != autoComplete { applyTraits() } }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// "default" | "light" | "dark"
|
|
118
|
+
public var keyboardAppearance: String = "default" {
|
|
119
|
+
didSet { if oldValue != keyboardAppearance { applyTraits() } }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Input font; nil keeps the body text style
|
|
123
|
+
public var textFont: UIFont? {
|
|
124
|
+
didSet { applyFonts() }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Screen-reader label; empty uses the label
|
|
128
|
+
public var spokenLabel: String = "" {
|
|
129
|
+
didSet { applyAccessibility() }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// iOS text traits, "" for the system default
|
|
133
|
+
public var writingTools: String = "" {
|
|
134
|
+
didSet { if oldValue != writingTools { applyTraits() } }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
public var inlinePrediction: String = "" {
|
|
138
|
+
didSet { if oldValue != inlinePrediction { applyTraits() } }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
public var smartQuotes: String = "" {
|
|
142
|
+
didSet { if oldValue != smartQuotes { applyTraits() } }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public var smartDashes: String = "" {
|
|
146
|
+
didSet { if oldValue != smartDashes { applyTraits() } }
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public var smartInsertDelete: String = "" {
|
|
150
|
+
didSet { if oldValue != smartInsertDelete { applyTraits() } }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public var mathExpressionCompletion: String = "" {
|
|
154
|
+
didSet { if oldValue != mathExpressionCompletion { applyTraits() } }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/// "" | "roundedRect" | "none" | "line" | "bezel"
|
|
158
|
+
public var borderStyle: String = "" {
|
|
159
|
+
didSet { if oldValue != borderStyle { applyBorderStyle() } }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// "" | "above" | "leading": the caption above the field, or in a column
|
|
163
|
+
/// beside it, the layout of the grouped forms in Contacts and Settings
|
|
164
|
+
public var labelPlacement: String = "" {
|
|
165
|
+
didSet { if oldValue != labelPlacement { applyLabel(); applyFonts(); applyColors() } }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// Width of the leading label column; 0 = default
|
|
169
|
+
public var labelWidth: CGFloat = 0 {
|
|
170
|
+
didSet { if oldValue != labelWidth { setNeedsLayout(); onNeedsRemeasure?() } }
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Test id of the inner input, where E2E drivers type (see PCTextField.mm)
|
|
174
|
+
public var inputTestID: String = "" {
|
|
175
|
+
didSet { if oldValue != inputTestID { applyAccessibility() } }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
public var leadingIconTestID: String = "" {
|
|
179
|
+
didSet { if oldValue != leadingIconTestID { applyAccessories() } }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
public var leadingIconSpokenLabel: String = "" {
|
|
183
|
+
didSet { if oldValue != leadingIconSpokenLabel { applyAccessories() } }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
public var trailingIconTestID: String = "" {
|
|
187
|
+
didSet { if oldValue != trailingIconTestID { applyAccessories() } }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
public var trailingIconSpokenLabel: String = "" {
|
|
191
|
+
didSet { if oldValue != trailingIconSpokenLabel { applyAccessories() } }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Colors; nil keeps the system color
|
|
195
|
+
public var activeColor: UIColor? {
|
|
196
|
+
didSet { if oldValue != activeColor { applyBorderStyle(); applyColors() } }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
public var outlineColor: UIColor? {
|
|
200
|
+
didSet { if oldValue != outlineColor { applyBorderStyle(); applyColors() } }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
public var errorColor: UIColor? {
|
|
204
|
+
didSet { if oldValue != errorColor { applyColors() } }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
public var containerColor: UIColor? {
|
|
208
|
+
didSet { if oldValue != containerColor { applyBorderStyle(); applyColors() } }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
public var textColor: UIColor? {
|
|
212
|
+
didSet { if oldValue != textColor { applyColors() } }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
public var placeholderTextColor: UIColor? {
|
|
216
|
+
didSet { if oldValue != placeholderTextColor { applyPlaceholder() } }
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/// Cap on the Dynamic Type scale of every text; values below 1 mean no cap
|
|
220
|
+
public var maxFontSizeMultiplier: CGFloat = 0 {
|
|
221
|
+
didSet { if oldValue != maxFontSizeMultiplier { applyFonts(); applyAccessories() } }
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/// "" | "left" | "center" | "right"
|
|
225
|
+
public var textAlign: String = "" {
|
|
226
|
+
didSet { if oldValue != textAlign { applyAlignment() } }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/// Multi-line height bounds in lines; 0 = unset
|
|
230
|
+
public var minLines: Int = 0 {
|
|
231
|
+
didSet { if oldValue != minLines { setNeedsLayout(); onNeedsRemeasure?() } }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
public var maxLines: Int = 0 {
|
|
235
|
+
didSet { if oldValue != maxLines { setNeedsLayout(); onNeedsRemeasure?() } }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/// A non-editable field that reports presses, as a picker's anchor
|
|
239
|
+
public var pressable: Bool = false {
|
|
240
|
+
didSet { if oldValue != pressable { applyEnabled() } }
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private var leadingLabel: Bool { labelPlacement == "leading" && !label.isEmpty }
|
|
244
|
+
|
|
245
|
+
/// Whether the field acts as a button: pressable and not editable
|
|
246
|
+
private var actsAsButton: Bool { pressable && interactivity == "disabled" }
|
|
247
|
+
|
|
248
|
+
/// The rounded rect drawn here instead of by UIKit, whose border can't be colored
|
|
249
|
+
private var drawsBox: Bool {
|
|
250
|
+
(borderStyle.isEmpty || borderStyle == "roundedRect")
|
|
251
|
+
&& (activeColor != nil || outlineColor != nil || containerColor != nil)
|
|
252
|
+
}
|
|
253
|
+
private var leadingColumnWidth: CGFloat { labelWidth > 0 ? labelWidth : 100 }
|
|
254
|
+
private let leadingSpacing: CGFloat = 8
|
|
255
|
+
|
|
256
|
+
// MARK: - Events back to ObjC++
|
|
257
|
+
|
|
258
|
+
public var onChange: ((String, Int) -> Void)?
|
|
259
|
+
public var onFocusChange: ((Bool, String) -> Void)?
|
|
260
|
+
public var onSubmit: ((String) -> Void)?
|
|
261
|
+
public var onSelectionChange: ((Int, Int) -> Void)?
|
|
262
|
+
public var onKeyboardToolbarPress: ((String) -> Void)?
|
|
263
|
+
public var onTrailingIconPress: (() -> Void)?
|
|
264
|
+
public var onPress: (() -> Void)?
|
|
265
|
+
|
|
266
|
+
/// Called when content changed in a way that affects the height (a
|
|
267
|
+
/// multi-line edit, an error message appearing), so the Fabric
|
|
268
|
+
/// measurement can be refreshed.
|
|
269
|
+
public var onNeedsRemeasure: (() -> Void)?
|
|
270
|
+
|
|
271
|
+
// MARK: - Text state
|
|
272
|
+
|
|
273
|
+
public private(set) var text: String = ""
|
|
274
|
+
private var nativeEventCount = 0
|
|
275
|
+
private var initialTextApplied = false
|
|
276
|
+
private var autoFocusDone = false
|
|
277
|
+
|
|
278
|
+
/// Whether a secure field is currently showing its text (password toggle)
|
|
279
|
+
private var revealed = false
|
|
280
|
+
|
|
281
|
+
/// The selection JS last heard of, so a change is reported once. Changes
|
|
282
|
+
/// made from JS (text or selection) update it without an event.
|
|
283
|
+
private var lastReportedSelection = NSRange(location: NSNotFound, length: 0)
|
|
284
|
+
private var applyingFromJS = false
|
|
285
|
+
|
|
286
|
+
/// The toolbar above the keyboard, while there are toolbar items
|
|
287
|
+
private var keyboardToolbar: UIToolbar?
|
|
288
|
+
private var toolbarGeneration = 0
|
|
289
|
+
|
|
290
|
+
// MARK: - Subviews
|
|
291
|
+
|
|
292
|
+
private let captionLabel = UILabel()
|
|
293
|
+
private let textField = PCInputTextField()
|
|
294
|
+
private let textView = PCPlaceholderTextView()
|
|
295
|
+
private let footerLabel = UILabel()
|
|
296
|
+
private let counterLabel = UILabel()
|
|
297
|
+
|
|
298
|
+
private var leadingIcon = PCButtonSupport.Icon.none
|
|
299
|
+
private var trailingIcon = PCButtonSupport.Icon.none
|
|
300
|
+
private var leadingImage: UIImage?
|
|
301
|
+
private var trailingImage: UIImage?
|
|
302
|
+
private var leadingIconGeneration = 0
|
|
303
|
+
private var trailingIconGeneration = 0
|
|
304
|
+
|
|
305
|
+
// MARK: - Layout metrics
|
|
306
|
+
|
|
307
|
+
private let captionSpacing: CGFloat = 6
|
|
308
|
+
private let footerSpacing: CGFloat = 4
|
|
309
|
+
private let singleLineMinHeight: CGFloat = 34
|
|
310
|
+
private let textViewInsets = UIEdgeInsets(top: 7, left: 4, bottom: 7, right: 4)
|
|
311
|
+
|
|
312
|
+
// MARK: - Init
|
|
313
|
+
|
|
314
|
+
public override init(frame: CGRect) {
|
|
315
|
+
super.init(frame: frame)
|
|
316
|
+
setup()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public required init?(coder: NSCoder) {
|
|
320
|
+
super.init(coder: coder)
|
|
321
|
+
setup()
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private func setup() {
|
|
325
|
+
captionLabel.numberOfLines = 1
|
|
326
|
+
captionLabel.adjustsFontForContentSizeCategory = true
|
|
327
|
+
|
|
328
|
+
footerLabel.numberOfLines = 0
|
|
329
|
+
footerLabel.adjustsFontForContentSizeCategory = true
|
|
330
|
+
|
|
331
|
+
counterLabel.numberOfLines = 1
|
|
332
|
+
counterLabel.adjustsFontForContentSizeCategory = true
|
|
333
|
+
counterLabel.textAlignment = .right
|
|
334
|
+
counterLabel.setContentCompressionResistancePriority(.required, for: .horizontal)
|
|
335
|
+
|
|
336
|
+
textField.delegate = self
|
|
337
|
+
textField.borderStyle = .roundedRect
|
|
338
|
+
textField.adjustsFontForContentSizeCategory = true
|
|
339
|
+
textField.addTarget(self, action: #selector(fieldEditingChanged), for: .editingChanged)
|
|
340
|
+
|
|
341
|
+
textView.delegate = self
|
|
342
|
+
textView.isScrollEnabled = false
|
|
343
|
+
textView.adjustsFontForContentSizeCategory = true
|
|
344
|
+
textView.textContainerInset = textViewInsets
|
|
345
|
+
textView.textContainer.lineFragmentPadding = 4
|
|
346
|
+
textView.backgroundColor = .clear
|
|
347
|
+
|
|
348
|
+
addSubview(captionLabel)
|
|
349
|
+
addSubview(textField)
|
|
350
|
+
addSubview(footerLabel)
|
|
351
|
+
addSubview(counterLabel)
|
|
352
|
+
|
|
353
|
+
// Presses of a field that acts as a button; accessory buttons keep their own
|
|
354
|
+
pressRecognizer.addTarget(self, action: #selector(handlePress))
|
|
355
|
+
pressRecognizer.delegate = self
|
|
356
|
+
pressRecognizer.isEnabled = false
|
|
357
|
+
addGestureRecognizer(pressRecognizer)
|
|
358
|
+
textField.onActivate = { [weak self] in self?.activateAsButton() ?? false }
|
|
359
|
+
textView.onActivate = { [weak self] in self?.activateAsButton() ?? false }
|
|
360
|
+
|
|
361
|
+
applyFonts()
|
|
362
|
+
applyColors()
|
|
363
|
+
applyBorderStyle()
|
|
364
|
+
applyTraits()
|
|
365
|
+
applyLabel()
|
|
366
|
+
applyPlaceholder()
|
|
367
|
+
applyFooter()
|
|
368
|
+
applyAccessories()
|
|
369
|
+
applyAccessibility()
|
|
370
|
+
applyAlignment()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// MARK: - Text
|
|
374
|
+
|
|
375
|
+
/// The text the field starts with; only the first value is used (see setText).
|
|
376
|
+
public func applyInitialText(_ value: String) {
|
|
377
|
+
guard !initialTextApplied else { return }
|
|
378
|
+
initialTextApplied = true
|
|
379
|
+
setTextInternal(value, moveCursorToEnd: true)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/// A controlled value from JS. Dropped when the user has edited since
|
|
383
|
+
/// `eventCount` was reported: the edit event that follows carries the
|
|
384
|
+
/// text JS should reconcile against.
|
|
385
|
+
public func setText(_ value: String, eventCount: Int) {
|
|
386
|
+
guard eventCount >= nativeEventCount, value != text else { return }
|
|
387
|
+
// Replacing text mid-composition (Japanese, Chinese keyboards) would
|
|
388
|
+
// break the composition; the next edit reconciles instead
|
|
389
|
+
guard activeInput.markedTextRange == nil else { return }
|
|
390
|
+
setTextInternal(value, moveCursorToEnd: false)
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/// Clears the text as if the user had, so JS gets a change event.
|
|
394
|
+
public func clear() {
|
|
395
|
+
guard !text.isEmpty else { return }
|
|
396
|
+
setTextInternal("", moveCursorToEnd: true)
|
|
397
|
+
reportEdit()
|
|
398
|
+
lastReportedSelection = NSRange(location: NSNotFound, length: 0)
|
|
399
|
+
reportSelection()
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// A selection from JS, in UTF-16 offsets clamped to the text. Dropped
|
|
403
|
+
/// like a stale `setText` when the user has edited since `eventCount`.
|
|
404
|
+
public func setSelection(start: Int, end: Int, eventCount: Int) {
|
|
405
|
+
guard eventCount >= nativeEventCount else { return }
|
|
406
|
+
guard activeInput.markedTextRange == nil else { return }
|
|
407
|
+
let length = (text as NSString).length
|
|
408
|
+
let from = min(max(0, start), length)
|
|
409
|
+
let to = min(max(from, end), length)
|
|
410
|
+
let range = NSRange(location: from, length: to - from)
|
|
411
|
+
applyingFromJS = true
|
|
412
|
+
if multiline {
|
|
413
|
+
textView.selectedRange = range
|
|
414
|
+
} else if let startPosition = textField.position(from: textField.beginningOfDocument, offset: from),
|
|
415
|
+
let endPosition = textField.position(from: startPosition, offset: to - from) {
|
|
416
|
+
textField.selectedTextRange = textField.textRange(from: startPosition, to: endPosition)
|
|
417
|
+
}
|
|
418
|
+
applyingFromJS = false
|
|
419
|
+
lastReportedSelection = currentSelection() ?? range
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
public func focus() {
|
|
423
|
+
activeInput.becomeFirstResponder()
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
public func blur() {
|
|
427
|
+
activeInput.resignFirstResponder()
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
public func setLeadingIcon(type: String, name: String, request: String, uri: String, scale: CGFloat, tinted: Bool) {
|
|
431
|
+
let next = PCButtonSupport.Icon(type: type, name: name, uri: uri, scale: scale, tinted: tinted, request: request)
|
|
432
|
+
guard next != leadingIcon else { return }
|
|
433
|
+
leadingIcon = next
|
|
434
|
+
leadingIconGeneration += 1
|
|
435
|
+
let generation = leadingIconGeneration
|
|
436
|
+
leadingImage = PCButtonSupport.image(for: next) { [weak self] image in
|
|
437
|
+
guard let self, self.leadingIconGeneration == generation else { return }
|
|
438
|
+
self.leadingImage = image
|
|
439
|
+
self.applyAccessories()
|
|
440
|
+
}
|
|
441
|
+
applyAccessories()
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
public func setTrailingIcon(type: String, name: String, request: String, uri: String, scale: CGFloat, tinted: Bool) {
|
|
445
|
+
let next = PCButtonSupport.Icon(type: type, name: name, uri: uri, scale: scale, tinted: tinted, request: request)
|
|
446
|
+
guard next != trailingIcon else { return }
|
|
447
|
+
trailingIcon = next
|
|
448
|
+
trailingIconGeneration += 1
|
|
449
|
+
let generation = trailingIconGeneration
|
|
450
|
+
trailingImage = PCButtonSupport.image(for: next) { [weak self] image in
|
|
451
|
+
guard let self, self.trailingIconGeneration == generation else { return }
|
|
452
|
+
self.trailingImage = image
|
|
453
|
+
self.applyAccessories()
|
|
454
|
+
}
|
|
455
|
+
applyAccessories()
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/// Resets the text state for a recycled view.
|
|
459
|
+
public func resetForRecycle() {
|
|
460
|
+
blur()
|
|
461
|
+
leadingIconGeneration += 1
|
|
462
|
+
trailingIconGeneration += 1
|
|
463
|
+
initialTextApplied = false
|
|
464
|
+
autoFocusDone = false
|
|
465
|
+
nativeEventCount = 0
|
|
466
|
+
revealed = false
|
|
467
|
+
setTextInternal("", moveCursorToEnd: true)
|
|
468
|
+
lastReportedSelection = NSRange(location: NSNotFound, length: 0)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
private var activeInput: UIView & UITextInput {
|
|
472
|
+
multiline ? textView : textField
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
private func setTextInternal(_ value: String, moveCursorToEnd: Bool) {
|
|
476
|
+
// The cursor moves with the text; JS knows, so there is no event
|
|
477
|
+
applyingFromJS = true
|
|
478
|
+
defer {
|
|
479
|
+
applyingFromJS = false
|
|
480
|
+
lastReportedSelection = currentSelection() ?? lastReportedSelection
|
|
481
|
+
}
|
|
482
|
+
if multiline {
|
|
483
|
+
let wasAtEnd = textView.selectedRange.location == (textView.text as NSString).length
|
|
484
|
+
let cursor = textView.selectedRange.location
|
|
485
|
+
textView.text = value
|
|
486
|
+
let length = (value as NSString).length
|
|
487
|
+
textView.selectedRange = NSRange(location: moveCursorToEnd || wasAtEnd ? length : min(cursor, length), length: 0)
|
|
488
|
+
textView.updatePlaceholderVisibility()
|
|
489
|
+
} else {
|
|
490
|
+
let wasAtEnd: Bool
|
|
491
|
+
if let selected = textField.selectedTextRange {
|
|
492
|
+
wasAtEnd = textField.compare(selected.end, to: textField.endOfDocument) == .orderedSame
|
|
493
|
+
} else {
|
|
494
|
+
wasAtEnd = true
|
|
495
|
+
}
|
|
496
|
+
let cursorOffset = textField.selectedTextRange.map {
|
|
497
|
+
textField.offset(from: textField.beginningOfDocument, to: $0.start)
|
|
498
|
+
} ?? 0
|
|
499
|
+
textField.text = value
|
|
500
|
+
let length = (value as NSString).length
|
|
501
|
+
let offset = moveCursorToEnd || wasAtEnd ? length : min(cursorOffset, length)
|
|
502
|
+
if let position = textField.position(from: textField.beginningOfDocument, offset: offset) {
|
|
503
|
+
textField.selectedTextRange = textField.textRange(from: position, to: position)
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
text = value
|
|
507
|
+
applyFooter()
|
|
508
|
+
onNeedsRemeasure?()
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
private func reportEdit() {
|
|
512
|
+
nativeEventCount += 1
|
|
513
|
+
onChange?(text, nativeEventCount)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/// The selection in UTF-16 offsets, as JS strings count them.
|
|
517
|
+
private func currentSelection() -> NSRange? {
|
|
518
|
+
if multiline { return textView.selectedRange }
|
|
519
|
+
guard let range = textField.selectedTextRange else { return nil }
|
|
520
|
+
let start = textField.offset(from: textField.beginningOfDocument, to: range.start)
|
|
521
|
+
let end = textField.offset(from: textField.beginningOfDocument, to: range.end)
|
|
522
|
+
return NSRange(location: start, length: max(0, end - start))
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
private func reportSelection() {
|
|
526
|
+
guard !applyingFromJS, let selection = currentSelection(), selection != lastReportedSelection else { return }
|
|
527
|
+
lastReportedSelection = selection
|
|
528
|
+
onSelectionChange?(selection.location, selection.location + selection.length)
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/// The return key: submits unless it inserts a newline, and blurs
|
|
532
|
+
/// unless submitBehavior is "submit". Returns whether it submitted.
|
|
533
|
+
private func handleReturn() -> Bool {
|
|
534
|
+
let behavior = submitBehavior.isEmpty ? (multiline ? "newline" : "blurAndSubmit") : submitBehavior
|
|
535
|
+
if multiline && behavior == "newline" { return false }
|
|
536
|
+
onSubmit?(text)
|
|
537
|
+
if behavior != "submit" { activeInput.resignFirstResponder() }
|
|
538
|
+
return true
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// MARK: - UITextField
|
|
542
|
+
|
|
543
|
+
@objc private func fieldEditingChanged() {
|
|
544
|
+
text = textField.text ?? ""
|
|
545
|
+
applyFooter()
|
|
546
|
+
reportEdit()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
public func textFieldDidBeginEditing(_ field: UITextField) {
|
|
550
|
+
if selectTextOnFocus {
|
|
551
|
+
field.selectAll(nil)
|
|
552
|
+
}
|
|
553
|
+
applyColors()
|
|
554
|
+
onFocusChange?(true, text)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
public func textFieldDidEndEditing(_ field: UITextField) {
|
|
558
|
+
applyColors()
|
|
559
|
+
onFocusChange?(false, text)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
public func textFieldShouldBeginEditing(_ field: UITextField) -> Bool {
|
|
563
|
+
!actsAsButton
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/// The clear button doesn't send .editingChanged on every iOS version;
|
|
567
|
+
/// report the cleared text once UIKit has applied it, unless it did.
|
|
568
|
+
public func textFieldShouldClear(_ field: UITextField) -> Bool {
|
|
569
|
+
DispatchQueue.main.async { [weak self] in
|
|
570
|
+
guard let self, (self.textField.text ?? "") != self.text else { return }
|
|
571
|
+
self.fieldEditingChanged()
|
|
572
|
+
}
|
|
573
|
+
return true
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
public func textFieldShouldReturn(_ field: UITextField) -> Bool {
|
|
577
|
+
// Single-line fields blur on submit unless submitBehavior is
|
|
578
|
+
// "submit", as the core TextInput does
|
|
579
|
+
_ = handleReturn()
|
|
580
|
+
return true
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
public func textFieldDidChangeSelection(_ field: UITextField) {
|
|
584
|
+
reportSelection()
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
public func textField(_ field: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
|
588
|
+
allowsChange(current: field.text ?? "", range: range, replacement: string, input: field)
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// MARK: - Press (a field acting as a button)
|
|
592
|
+
|
|
593
|
+
private let pressRecognizer = UITapGestureRecognizer()
|
|
594
|
+
|
|
595
|
+
@objc private func handlePress() {
|
|
596
|
+
guard actsAsButton else { return }
|
|
597
|
+
// Brief highlight, as a plain UIButton gives
|
|
598
|
+
activeInput.alpha = 0.5
|
|
599
|
+
UIView.animate(withDuration: 0.25, delay: 0.05, options: [.allowUserInteraction]) {
|
|
600
|
+
self.activeInput.alpha = 1
|
|
601
|
+
}
|
|
602
|
+
onPress?()
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
private func activateAsButton() -> Bool {
|
|
606
|
+
guard actsAsButton else { return false }
|
|
607
|
+
onPress?()
|
|
608
|
+
return true
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
public func gestureRecognizer(_ recognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
|
|
612
|
+
// The trailing icon and the password toggle handle their own taps
|
|
613
|
+
!(touch.view is PCAccessoryButton)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/// UITextField's own tap recognizers would otherwise take the tap (and
|
|
617
|
+
/// then find editing refused), leaving the press unreported
|
|
618
|
+
public func gestureRecognizer(
|
|
619
|
+
_ recognizer: UIGestureRecognizer,
|
|
620
|
+
shouldRecognizeSimultaneouslyWith other: UIGestureRecognizer
|
|
621
|
+
) -> Bool {
|
|
622
|
+
recognizer === pressRecognizer
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// MARK: - UITextView
|
|
626
|
+
|
|
627
|
+
public func textViewDidChange(_ view: UITextView) {
|
|
628
|
+
text = view.text ?? ""
|
|
629
|
+
textView.updatePlaceholderVisibility()
|
|
630
|
+
applyFooter()
|
|
631
|
+
reportEdit()
|
|
632
|
+
onNeedsRemeasure?()
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
public func textViewDidBeginEditing(_ view: UITextView) {
|
|
636
|
+
if selectTextOnFocus {
|
|
637
|
+
view.selectAll(nil)
|
|
638
|
+
}
|
|
639
|
+
applyColors()
|
|
640
|
+
onFocusChange?(true, text)
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
public func textViewDidEndEditing(_ view: UITextView) {
|
|
644
|
+
applyColors()
|
|
645
|
+
onFocusChange?(false, text)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
public func textView(_ view: UITextView, shouldChangeTextIn range: NSRange, replacementText string: String) -> Bool {
|
|
649
|
+
// Return submits instead of inserting a newline, unless submitBehavior is "newline"
|
|
650
|
+
if string == "\n", view.markedTextRange == nil, handleReturn() {
|
|
651
|
+
return false
|
|
652
|
+
}
|
|
653
|
+
return allowsChange(current: view.text ?? "", range: range, replacement: string, input: view)
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
public func textViewDidChangeSelection(_ view: UITextView) {
|
|
657
|
+
reportSelection()
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/// Enforces `maxLength` in UTF-16 units, like the core TextInput and the
|
|
661
|
+
/// Android length filter, leaving IME composition alone.
|
|
662
|
+
private func allowsChange(current: String, range: NSRange, replacement: String, input: UITextInput) -> Bool {
|
|
663
|
+
guard maxLength > 0, input.markedTextRange == nil else { return true }
|
|
664
|
+
let currentLength = (current as NSString).length
|
|
665
|
+
let nextLength = currentLength - range.length + (replacement as NSString).length
|
|
666
|
+
return nextLength <= maxLength
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// MARK: - Props handling
|
|
670
|
+
|
|
671
|
+
private func rebuildInput() {
|
|
672
|
+
// `multiline` already has its new value in didSet, so activeInput
|
|
673
|
+
// points to the replacement. Read focus and selection from the old input.
|
|
674
|
+
let previousInput: UIView & UITextInput = multiline ? textField : textView
|
|
675
|
+
let focused = previousInput.isFirstResponder
|
|
676
|
+
let selection = previousInput.selectedTextRange.map { range in
|
|
677
|
+
let start = previousInput.offset(from: previousInput.beginningOfDocument, to: range.start)
|
|
678
|
+
let end = previousInput.offset(from: previousInput.beginningOfDocument, to: range.end)
|
|
679
|
+
return NSRange(location: start, length: max(0, end - start))
|
|
680
|
+
}
|
|
681
|
+
textField.removeFromSuperview()
|
|
682
|
+
textView.removeFromSuperview()
|
|
683
|
+
if multiline {
|
|
684
|
+
textView.text = text
|
|
685
|
+
textView.updatePlaceholderVisibility()
|
|
686
|
+
insertSubview(textView, at: 1)
|
|
687
|
+
} else {
|
|
688
|
+
textField.text = text
|
|
689
|
+
insertSubview(textField, at: 1)
|
|
690
|
+
}
|
|
691
|
+
applyFonts()
|
|
692
|
+
applyColors()
|
|
693
|
+
applyBorderStyle()
|
|
694
|
+
applyTraits()
|
|
695
|
+
applyPlaceholder()
|
|
696
|
+
applyAccessories()
|
|
697
|
+
applyEnabled()
|
|
698
|
+
applyAccessibility()
|
|
699
|
+
applyAlignment()
|
|
700
|
+
if focused { activeInput.becomeFirstResponder() }
|
|
701
|
+
if let selection {
|
|
702
|
+
setSelection(start: selection.location, end: NSMaxRange(selection), eventCount: nativeEventCount)
|
|
703
|
+
}
|
|
704
|
+
setNeedsLayout()
|
|
705
|
+
onNeedsRemeasure?()
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
private func applyLabel() {
|
|
709
|
+
captionLabel.text = label
|
|
710
|
+
captionLabel.isHidden = label.isEmpty
|
|
711
|
+
applyAccessibility()
|
|
712
|
+
setNeedsLayout()
|
|
713
|
+
onNeedsRemeasure?()
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private func applyPlaceholder() {
|
|
717
|
+
if placeholder.isEmpty {
|
|
718
|
+
textField.placeholder = nil
|
|
719
|
+
} else if let placeholderTextColor {
|
|
720
|
+
textField.attributedPlaceholder = NSAttributedString(
|
|
721
|
+
string: placeholder,
|
|
722
|
+
attributes: [.foregroundColor: placeholderTextColor]
|
|
723
|
+
)
|
|
724
|
+
} else {
|
|
725
|
+
textField.placeholder = placeholder
|
|
726
|
+
}
|
|
727
|
+
textView.placeholder = placeholder
|
|
728
|
+
textView.placeholderLabel.textColor = placeholderTextColor ?? .placeholderText
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/// Supporting text, error message and counter below the field.
|
|
732
|
+
private func applyFooter() {
|
|
733
|
+
let showsError = errorState == "error"
|
|
734
|
+
let message = showsError && !errorText.isEmpty ? errorText : supportingText
|
|
735
|
+
footerLabel.text = message
|
|
736
|
+
footerLabel.isHidden = message.isEmpty
|
|
737
|
+
|
|
738
|
+
if characterCount {
|
|
739
|
+
let count = (text as NSString).length
|
|
740
|
+
counterLabel.text = maxLength > 0 ? "\(count) / \(maxLength)" : "\(count)"
|
|
741
|
+
counterLabel.isHidden = false
|
|
742
|
+
} else {
|
|
743
|
+
counterLabel.text = nil
|
|
744
|
+
counterLabel.isHidden = true
|
|
745
|
+
}
|
|
746
|
+
applyColors()
|
|
747
|
+
setNeedsLayout()
|
|
748
|
+
onNeedsRemeasure?()
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/// A text style's font at the current text size, capped at
|
|
752
|
+
/// `maxFontSizeMultiplier` times its size at the default text size.
|
|
753
|
+
/// `base` replaces the style's own font (the `textStyle` font, given at
|
|
754
|
+
/// its default-size point size), scaled along the style's curve.
|
|
755
|
+
private func scaledFont(_ style: UIFont.TextStyle, base: UIFont? = nil) -> UIFont {
|
|
756
|
+
let font = base ?? .preferredFont(
|
|
757
|
+
forTextStyle: style,
|
|
758
|
+
compatibleWith: UITraitCollection(preferredContentSizeCategory: .large)
|
|
759
|
+
)
|
|
760
|
+
let metrics = UIFontMetrics(forTextStyle: style)
|
|
761
|
+
if maxFontSizeMultiplier >= 1 {
|
|
762
|
+
return metrics.scaledFont(
|
|
763
|
+
for: font,
|
|
764
|
+
maximumPointSize: font.pointSize * maxFontSizeMultiplier,
|
|
765
|
+
compatibleWith: traitCollection
|
|
766
|
+
)
|
|
767
|
+
}
|
|
768
|
+
return metrics.scaledFont(for: font, compatibleWith: traitCollection)
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
private var inputFont: UIFont { scaledFont(.body, base: textFont) }
|
|
772
|
+
|
|
773
|
+
private func applyFonts() {
|
|
774
|
+
captionLabel.font = scaledFont(leadingLabel ? .body : .subheadline)
|
|
775
|
+
footerLabel.font = scaledFont(.footnote)
|
|
776
|
+
counterLabel.font = scaledFont(.footnote)
|
|
777
|
+
let inputFont = self.inputFont
|
|
778
|
+
textField.font = inputFont
|
|
779
|
+
textView.font = inputFont
|
|
780
|
+
textView.placeholderLabel.font = inputFont
|
|
781
|
+
setNeedsLayout()
|
|
782
|
+
onNeedsRemeasure?()
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private func applyColors() {
|
|
786
|
+
let showsError = errorState == "error"
|
|
787
|
+
let focused = activeInput.isFirstResponder
|
|
788
|
+
let error = errorColor ?? .systemRed
|
|
789
|
+
captionLabel.textColor = showsError
|
|
790
|
+
? error
|
|
791
|
+
: (leadingLabel ? .label : (focused ? (activeColor ?? tintColor) : .secondaryLabel))
|
|
792
|
+
footerLabel.textColor = showsError ? error : .secondaryLabel
|
|
793
|
+
counterLabel.textColor = showsError && maxLength > 0 && (text as NSString).length > maxLength
|
|
794
|
+
? error
|
|
795
|
+
: .secondaryLabel
|
|
796
|
+
|
|
797
|
+
// The outline of the text view and of a field whose box is drawn here
|
|
798
|
+
let outline: UIColor
|
|
799
|
+
if showsError {
|
|
800
|
+
outline = error
|
|
801
|
+
} else if focused {
|
|
802
|
+
outline = activeColor ?? outlineColor ?? .systemGray4
|
|
803
|
+
} else {
|
|
804
|
+
outline = outlineColor ?? .systemGray4
|
|
805
|
+
}
|
|
806
|
+
let resolved = outline.resolvedColor(with: traitCollection).cgColor
|
|
807
|
+
textView.layer.borderColor = resolved
|
|
808
|
+
textField.layer.borderColor = drawsBox ? resolved : nil
|
|
809
|
+
|
|
810
|
+
// Cursor and selection
|
|
811
|
+
textField.tintColor = activeColor
|
|
812
|
+
textView.tintColor = activeColor
|
|
813
|
+
textField.textColor = textColor ?? .label
|
|
814
|
+
textView.textColor = textColor ?? .label
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
private func applyBorderStyle() {
|
|
818
|
+
let style: UITextField.BorderStyle
|
|
819
|
+
switch borderStyle {
|
|
820
|
+
case "none": style = .none
|
|
821
|
+
case "line": style = .line
|
|
822
|
+
case "bezel": style = .bezel
|
|
823
|
+
default: style = .roundedRect
|
|
824
|
+
}
|
|
825
|
+
// UIKit's rounded rect can't take colors; draw its look instead
|
|
826
|
+
let drawsBox = self.drawsBox
|
|
827
|
+
textField.borderStyle = drawsBox ? .none : style
|
|
828
|
+
textField.layer.borderWidth = drawsBox ? 1 : 0
|
|
829
|
+
textField.layer.cornerRadius = drawsBox ? 5 : 0
|
|
830
|
+
textField.textInsets = drawsBox ? UIEdgeInsets(top: 0, left: 7, bottom: 0, right: 7) : .zero
|
|
831
|
+
if drawsBox {
|
|
832
|
+
textField.backgroundColor = containerColor ?? .systemBackground
|
|
833
|
+
} else {
|
|
834
|
+
textField.backgroundColor = style == .none ? containerColor : nil
|
|
835
|
+
}
|
|
836
|
+
// The text view mirrors the rounded-rect field
|
|
837
|
+
let bordered = style != .none
|
|
838
|
+
textView.layer.borderWidth = bordered ? 1 : 0
|
|
839
|
+
textView.layer.cornerRadius = style == .roundedRect ? 5 : 0
|
|
840
|
+
textView.backgroundColor = containerColor ?? (bordered ? .systemBackground : .clear)
|
|
841
|
+
applyColors()
|
|
842
|
+
setNeedsLayout()
|
|
843
|
+
onNeedsRemeasure?()
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
private func applyEnabled() {
|
|
847
|
+
let enabled = interactivity != "disabled"
|
|
848
|
+
let button = actsAsButton
|
|
849
|
+
// A field acting as a button keeps its enabled look; editing is
|
|
850
|
+
// refused in textFieldShouldBeginEditing and presses come from the
|
|
851
|
+
// gesture recognizer
|
|
852
|
+
textField.isEnabled = enabled || button
|
|
853
|
+
textView.isEditable = enabled
|
|
854
|
+
textView.isUserInteractionEnabled = enabled
|
|
855
|
+
textView.alpha = enabled || button ? 1 : 0.5
|
|
856
|
+
pressRecognizer.isEnabled = button
|
|
857
|
+
if button, activeInput.isFirstResponder { blur() }
|
|
858
|
+
applyAccessibility()
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private func applyAccessibility() {
|
|
862
|
+
let spoken = spokenLabel.isEmpty ? (label.isEmpty ? nil : label) : spokenLabel
|
|
863
|
+
textField.accessibilityLabel = spoken
|
|
864
|
+
textView.accessibilityLabel = spoken
|
|
865
|
+
let id = inputTestID.isEmpty ? nil : inputTestID
|
|
866
|
+
textField.accessibilityIdentifier = id
|
|
867
|
+
textView.accessibilityIdentifier = id
|
|
868
|
+
for input in [textField as UIView, textView] {
|
|
869
|
+
if actsAsButton {
|
|
870
|
+
input.accessibilityTraits.insert(.button)
|
|
871
|
+
} else {
|
|
872
|
+
input.accessibilityTraits.remove(.button)
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private func applyAlignment() {
|
|
878
|
+
let alignment: NSTextAlignment
|
|
879
|
+
switch textAlign {
|
|
880
|
+
case "left": alignment = .left
|
|
881
|
+
case "center": alignment = .center
|
|
882
|
+
case "right": alignment = .right
|
|
883
|
+
default: alignment = .natural
|
|
884
|
+
}
|
|
885
|
+
textField.textAlignment = alignment
|
|
886
|
+
textView.textAlignment = alignment
|
|
887
|
+
textView.placeholderLabel.textAlignment = alignment
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private func applyTraits() {
|
|
891
|
+
applyTraits(to: textField)
|
|
892
|
+
applyTraits(to: textView)
|
|
893
|
+
// The keyboard only picks the traits up when input restarts
|
|
894
|
+
if activeInput.isFirstResponder {
|
|
895
|
+
activeInput.reloadInputViews()
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/// Both inputs get the traits: UITextField and UITextView declare the
|
|
900
|
+
/// same UITextInputTraits properties, but as their own members.
|
|
901
|
+
private func applyTraits(to input: UITextField) {
|
|
902
|
+
input.keyboardType = PCTextFieldView.keyboardType(keyboardType)
|
|
903
|
+
input.returnKeyType = PCTextFieldView.returnKeyType(returnKeyType)
|
|
904
|
+
input.autocapitalizationType = PCTextFieldView.autocapitalization(autoCapitalize)
|
|
905
|
+
input.autocorrectionType = autoCorrect ? .default : .no
|
|
906
|
+
input.spellCheckingType = autoCorrect ? .default : .no
|
|
907
|
+
input.isSecureTextEntry = secureTextEntry && !revealed
|
|
908
|
+
input.textContentType = PCTextFieldView.contentType(autoComplete)
|
|
909
|
+
input.keyboardAppearance = PCTextFieldView.keyboardAppearance(keyboardAppearance)
|
|
910
|
+
input.passwordRules = passwordRules.isEmpty ? nil : UITextInputPasswordRules(descriptor: passwordRules)
|
|
911
|
+
input.smartQuotesType = PCTextFieldView.smartQuotesType(smartQuotes)
|
|
912
|
+
input.smartDashesType = PCTextFieldView.smartDashesType(smartDashes)
|
|
913
|
+
input.smartInsertDeleteType = PCTextFieldView.smartInsertDeleteType(smartInsertDelete)
|
|
914
|
+
if #available(iOS 17.0, *) {
|
|
915
|
+
input.inlinePredictionType = PCTextFieldView.inlinePredictionType(inlinePrediction)
|
|
916
|
+
}
|
|
917
|
+
if #available(iOS 18.0, *) {
|
|
918
|
+
input.mathExpressionCompletionType = PCTextFieldView.mathCompletionType(mathExpressionCompletion)
|
|
919
|
+
input.writingToolsBehavior = PCTextFieldView.writingToolsBehavior(writingTools)
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
private func applyTraits(to input: UITextView) {
|
|
924
|
+
input.keyboardType = PCTextFieldView.keyboardType(keyboardType)
|
|
925
|
+
input.returnKeyType = PCTextFieldView.returnKeyType(returnKeyType)
|
|
926
|
+
input.autocapitalizationType = PCTextFieldView.autocapitalization(autoCapitalize)
|
|
927
|
+
input.autocorrectionType = autoCorrect ? .default : .no
|
|
928
|
+
input.spellCheckingType = autoCorrect ? .default : .no
|
|
929
|
+
input.isSecureTextEntry = secureTextEntry && !revealed
|
|
930
|
+
input.textContentType = PCTextFieldView.contentType(autoComplete)
|
|
931
|
+
input.keyboardAppearance = PCTextFieldView.keyboardAppearance(keyboardAppearance)
|
|
932
|
+
input.passwordRules = passwordRules.isEmpty ? nil : UITextInputPasswordRules(descriptor: passwordRules)
|
|
933
|
+
input.smartQuotesType = PCTextFieldView.smartQuotesType(smartQuotes)
|
|
934
|
+
input.smartDashesType = PCTextFieldView.smartDashesType(smartDashes)
|
|
935
|
+
input.smartInsertDeleteType = PCTextFieldView.smartInsertDeleteType(smartInsertDelete)
|
|
936
|
+
if #available(iOS 17.0, *) {
|
|
937
|
+
input.inlinePredictionType = PCTextFieldView.inlinePredictionType(inlinePrediction)
|
|
938
|
+
}
|
|
939
|
+
if #available(iOS 18.0, *) {
|
|
940
|
+
input.mathExpressionCompletionType = PCTextFieldView.mathCompletionType(mathExpressionCompletion)
|
|
941
|
+
input.writingToolsBehavior = PCTextFieldView.writingToolsBehavior(writingTools)
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
/// The keyboard toolbar: a `UIToolbar` as the input's
|
|
946
|
+
/// `inputAccessoryView`, with the given buttons, flexible spaces and a
|
|
947
|
+
/// Done button that dismisses the keyboard.
|
|
948
|
+
private func applyKeyboardToolbar() {
|
|
949
|
+
toolbarGeneration += 1
|
|
950
|
+
if keyboardToolbarItems.isEmpty {
|
|
951
|
+
keyboardToolbar = nil
|
|
952
|
+
} else {
|
|
953
|
+
let toolbar = keyboardToolbar ?? UIToolbar(frame: CGRect(x: 0, y: 0, width: 320, height: 44))
|
|
954
|
+
toolbar.items = keyboardToolbarItems.map { toolbarItem(from: $0) }
|
|
955
|
+
toolbar.sizeToFit()
|
|
956
|
+
keyboardToolbar = toolbar
|
|
957
|
+
}
|
|
958
|
+
textField.inputAccessoryView = keyboardToolbar
|
|
959
|
+
textView.inputAccessoryView = keyboardToolbar
|
|
960
|
+
if activeInput.isFirstResponder {
|
|
961
|
+
activeInput.reloadInputViews()
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
private func toolbarItem(from spec: NSDictionary) -> UIBarButtonItem {
|
|
966
|
+
let kind = spec["kind"] as? String ?? "button"
|
|
967
|
+
if kind == "flexibleSpace" {
|
|
968
|
+
return .flexibleSpace()
|
|
969
|
+
}
|
|
970
|
+
let itemId = spec["itemId"] as? String ?? ""
|
|
971
|
+
let title = spec["title"] as? String ?? ""
|
|
972
|
+
let spokenLabel = spec["accessibilityLabel"] as? String ?? ""
|
|
973
|
+
let testID = spec["testID"] as? String ?? ""
|
|
974
|
+
let action: UIAction
|
|
975
|
+
if kind == "done" {
|
|
976
|
+
action = UIAction { [weak self] _ in self?.blur() }
|
|
977
|
+
} else {
|
|
978
|
+
action = UIAction { [weak self] _ in self?.onKeyboardToolbarPress?(itemId) }
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
let item: UIBarButtonItem
|
|
982
|
+
let systemItem = kind == "done" && title.isEmpty
|
|
983
|
+
? UIBarButtonItem.SystemItem.done
|
|
984
|
+
: PCTextFieldView.toolbarSystemItem(spec["systemItem"] as? String ?? "")
|
|
985
|
+
let icon = PCButtonSupport.Icon(dictionary: spec as? [String: Any] ?? [:])
|
|
986
|
+
if let systemItem {
|
|
987
|
+
item = UIBarButtonItem(systemItem: systemItem, primaryAction: action)
|
|
988
|
+
} else if icon.isPresent {
|
|
989
|
+
item = UIBarButtonItem(title: nil, image: nil, primaryAction: action, menu: nil)
|
|
990
|
+
// The title is what VoiceOver reads for an icon button
|
|
991
|
+
item.accessibilityLabel = title.isEmpty ? nil : title
|
|
992
|
+
let generation = toolbarGeneration
|
|
993
|
+
item.image = PCButtonSupport.image(for: icon) { [weak self, weak item] image in
|
|
994
|
+
guard let self, self.toolbarGeneration == generation else { return }
|
|
995
|
+
item?.image = image
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
item = UIBarButtonItem(title: title, image: nil, primaryAction: action, menu: nil)
|
|
999
|
+
}
|
|
1000
|
+
if (spec["prominent"] as? String) == "true" {
|
|
1001
|
+
PCTextFieldView.makeProminent(item)
|
|
1002
|
+
}
|
|
1003
|
+
if !spokenLabel.isEmpty {
|
|
1004
|
+
item.accessibilityLabel = spokenLabel
|
|
1005
|
+
}
|
|
1006
|
+
item.accessibilityIdentifier = testID.isEmpty ? nil : testID
|
|
1007
|
+
return item
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/// The prominent style: tinted glass on iOS 26, bold before
|
|
1011
|
+
private static func makeProminent(_ item: UIBarButtonItem) {
|
|
1012
|
+
#if compiler(>=6.2)
|
|
1013
|
+
if #available(iOS 26.0, *) {
|
|
1014
|
+
item.style = .prominent
|
|
1015
|
+
return
|
|
1016
|
+
}
|
|
1017
|
+
#endif
|
|
1018
|
+
item.style = .done
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private static func toolbarSystemItem(_ value: String) -> UIBarButtonItem.SystemItem? {
|
|
1022
|
+
switch value {
|
|
1023
|
+
case "done": return .done
|
|
1024
|
+
case "cancel": return .cancel
|
|
1025
|
+
case "save": return .save
|
|
1026
|
+
case "add": return .add
|
|
1027
|
+
case "edit": return .edit
|
|
1028
|
+
case "close": return .close
|
|
1029
|
+
case "search": return .search
|
|
1030
|
+
case "compose": return .compose
|
|
1031
|
+
case "reply": return .reply
|
|
1032
|
+
case "action": return .action
|
|
1033
|
+
case "camera": return .camera
|
|
1034
|
+
case "trash": return .trash
|
|
1035
|
+
case "undo": return .undo
|
|
1036
|
+
case "redo": return .redo
|
|
1037
|
+
default: return nil
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
/// Prefix and suffix labels, icons, the clear button and the password
|
|
1042
|
+
/// toggle. Single-line only: a text view has no accessory views.
|
|
1043
|
+
private func applyAccessories() {
|
|
1044
|
+
textField.leftView = nil
|
|
1045
|
+
textField.rightView = nil
|
|
1046
|
+
textField.leftViewMode = .never
|
|
1047
|
+
textField.rightViewMode = .never
|
|
1048
|
+
|
|
1049
|
+
if let leading = accessoryView(
|
|
1050
|
+
image: leadingImage, tinted: leadingIcon.tinted, text: prefix, action: nil,
|
|
1051
|
+
testID: leadingIconTestID, spokenLabel: leadingIconSpokenLabel
|
|
1052
|
+
) {
|
|
1053
|
+
textField.leftView = leading
|
|
1054
|
+
textField.leftViewMode = .always
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
let trailingAction: (() -> Void)?
|
|
1058
|
+
let trailingImageForSlot: UIImage?
|
|
1059
|
+
if trailingImage != nil {
|
|
1060
|
+
trailingImageForSlot = trailingImage
|
|
1061
|
+
trailingAction = { [weak self] in self?.onTrailingIconPress?() }
|
|
1062
|
+
} else if passwordToggle, secureTextEntry || revealed {
|
|
1063
|
+
trailingImageForSlot = UIImage(systemName: revealed ? "eye.slash" : "eye")
|
|
1064
|
+
trailingAction = { [weak self] in self?.toggleReveal() }
|
|
1065
|
+
} else {
|
|
1066
|
+
trailingImageForSlot = nil
|
|
1067
|
+
trailingAction = nil
|
|
1068
|
+
}
|
|
1069
|
+
let trailingTinted = trailingImage != nil ? trailingIcon.tinted : true
|
|
1070
|
+
let customTrailing = trailingImage != nil
|
|
1071
|
+
if let trailing = accessoryView(
|
|
1072
|
+
image: trailingImageForSlot, tinted: trailingTinted, text: suffix, action: trailingAction,
|
|
1073
|
+
testID: customTrailing ? trailingIconTestID : "",
|
|
1074
|
+
spokenLabel: customTrailing ? trailingIconSpokenLabel : ""
|
|
1075
|
+
) {
|
|
1076
|
+
textField.rightView = trailing
|
|
1077
|
+
textField.rightViewMode = .always
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// A right view takes the clear button's place, as UIKit draws them in the same slot
|
|
1081
|
+
switch clearButtonMode {
|
|
1082
|
+
case "while-editing": textField.clearButtonMode = .whileEditing
|
|
1083
|
+
case "unless-editing": textField.clearButtonMode = .unlessEditing
|
|
1084
|
+
case "always": textField.clearButtonMode = .always
|
|
1085
|
+
default: textField.clearButtonMode = .never
|
|
1086
|
+
}
|
|
1087
|
+
setNeedsLayout()
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
/// A label and/or an image button for the field's left or right slot.
|
|
1091
|
+
private func accessoryView(
|
|
1092
|
+
image: UIImage?, tinted: Bool, text: String, action: (() -> Void)?,
|
|
1093
|
+
testID: String, spokenLabel: String
|
|
1094
|
+
) -> UIView? {
|
|
1095
|
+
guard image != nil || !text.isEmpty else { return nil }
|
|
1096
|
+
let stack = UIStackView()
|
|
1097
|
+
stack.axis = .horizontal
|
|
1098
|
+
stack.alignment = .center
|
|
1099
|
+
stack.spacing = 6
|
|
1100
|
+
stack.isLayoutMarginsRelativeArrangement = true
|
|
1101
|
+
stack.layoutMargins = UIEdgeInsets(top: 0, left: 8, bottom: 0, right: 8)
|
|
1102
|
+
|
|
1103
|
+
if let image {
|
|
1104
|
+
let config = UIImage.SymbolConfiguration(font: inputFont)
|
|
1105
|
+
if let action {
|
|
1106
|
+
let button = PCAccessoryButton(type: .system)
|
|
1107
|
+
button.setImage(image.applyingSymbolConfiguration(config) ?? image, for: .normal)
|
|
1108
|
+
button.tintColor = tinted ? .secondaryLabel : nil
|
|
1109
|
+
button.action = action
|
|
1110
|
+
button.accessibilityLabel = passwordToggle && trailingImage == nil
|
|
1111
|
+
? (revealed ? "Hide password" : "Show password")
|
|
1112
|
+
: (spokenLabel.isEmpty ? nil : spokenLabel)
|
|
1113
|
+
button.accessibilityIdentifier = testID.isEmpty ? nil : testID
|
|
1114
|
+
stack.addArrangedSubview(button)
|
|
1115
|
+
} else {
|
|
1116
|
+
let imageView = UIImageView(image: image.applyingSymbolConfiguration(config) ?? image)
|
|
1117
|
+
imageView.tintColor = tinted ? .secondaryLabel : nil
|
|
1118
|
+
imageView.contentMode = .scaleAspectFit
|
|
1119
|
+
imageView.accessibilityIdentifier = testID.isEmpty ? nil : testID
|
|
1120
|
+
if !spokenLabel.isEmpty {
|
|
1121
|
+
imageView.isAccessibilityElement = true
|
|
1122
|
+
imageView.accessibilityLabel = spokenLabel
|
|
1123
|
+
}
|
|
1124
|
+
stack.addArrangedSubview(imageView)
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
if !text.isEmpty {
|
|
1128
|
+
let label = UILabel()
|
|
1129
|
+
label.text = text
|
|
1130
|
+
label.font = inputFont
|
|
1131
|
+
label.textColor = .secondaryLabel
|
|
1132
|
+
label.adjustsFontForContentSizeCategory = true
|
|
1133
|
+
stack.addArrangedSubview(label)
|
|
1134
|
+
}
|
|
1135
|
+
stack.frame.size = stack.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
|
1136
|
+
return stack
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/// Shows or hides the text of a secure field. Toggling `isSecureTextEntry`
|
|
1140
|
+
/// makes UIKit clear the field on the next keystroke; re-inserting the
|
|
1141
|
+
/// text through the text range keeps it and the caret.
|
|
1142
|
+
private func toggleReveal() {
|
|
1143
|
+
revealed.toggle()
|
|
1144
|
+
let current = textField.text ?? ""
|
|
1145
|
+
textField.isSecureTextEntry = secureTextEntry && !revealed
|
|
1146
|
+
if textField.isFirstResponder, let range = textField.textRange(from: textField.beginningOfDocument, to: textField.endOfDocument) {
|
|
1147
|
+
textField.replace(range, withText: current)
|
|
1148
|
+
}
|
|
1149
|
+
applyAccessories()
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
private func performAutoFocus() {
|
|
1153
|
+
guard !autoFocusDone else { return }
|
|
1154
|
+
autoFocusDone = true
|
|
1155
|
+
DispatchQueue.main.async { [weak self] in self?.focus() }
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
public override func didMoveToWindow() {
|
|
1159
|
+
super.didMoveToWindow()
|
|
1160
|
+
if window != nil, autoFocus { performAutoFocus() }
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
public override func tintColorDidChange() {
|
|
1164
|
+
super.tintColorDidChange()
|
|
1165
|
+
applyColors()
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
|
1169
|
+
super.traitCollectionDidChange(previousTraitCollection)
|
|
1170
|
+
if previousTraitCollection?.preferredContentSizeCategory != traitCollection.preferredContentSizeCategory {
|
|
1171
|
+
applyFonts()
|
|
1172
|
+
applyAccessories()
|
|
1173
|
+
}
|
|
1174
|
+
if previousTraitCollection?.hasDifferentColorAppearance(comparedTo: traitCollection) == true {
|
|
1175
|
+
applyColors()
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
// MARK: - Layout
|
|
1180
|
+
|
|
1181
|
+
private struct Metrics {
|
|
1182
|
+
var captionHeight: CGFloat
|
|
1183
|
+
var inputHeight: CGFloat
|
|
1184
|
+
var footerHeight: CGFloat
|
|
1185
|
+
var leading: Bool
|
|
1186
|
+
/// A multi-line field past maxLines scrolls its text
|
|
1187
|
+
var inputScrolls: Bool
|
|
1188
|
+
var total: CGFloat {
|
|
1189
|
+
var height = leading ? max(inputHeight, captionHeight) : inputHeight
|
|
1190
|
+
if captionHeight > 0 && !leading { height += captionHeight + 6 }
|
|
1191
|
+
if footerHeight > 0 { height += footerHeight + 4 }
|
|
1192
|
+
return height
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
/// The x where the field (and the footer) starts: after a leading label column
|
|
1197
|
+
private func fieldOffset() -> CGFloat {
|
|
1198
|
+
leadingLabel ? leadingColumnWidth + leadingSpacing : 0
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
private func metrics(for width: CGFloat) -> Metrics {
|
|
1202
|
+
let captionHeight = label.isEmpty ? 0 : ceil(captionLabel.font.lineHeight)
|
|
1203
|
+
let fieldWidth = max(1, width - fieldOffset())
|
|
1204
|
+
|
|
1205
|
+
var inputHeight: CGFloat
|
|
1206
|
+
var inputScrolls = false
|
|
1207
|
+
if multiline {
|
|
1208
|
+
let fitted = ceil(textView.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude)).height)
|
|
1209
|
+
inputHeight = max(singleLineMinHeight, fitted)
|
|
1210
|
+
let lineHeight = inputFont.lineHeight
|
|
1211
|
+
let chrome = textViewInsets.top + textViewInsets.bottom
|
|
1212
|
+
if minLines > 0 {
|
|
1213
|
+
inputHeight = max(inputHeight, ceil(lineHeight * CGFloat(minLines) + chrome))
|
|
1214
|
+
}
|
|
1215
|
+
if maxLines > 0 {
|
|
1216
|
+
let cap = max(singleLineMinHeight, ceil(lineHeight * CGFloat(maxLines) + chrome))
|
|
1217
|
+
inputScrolls = fitted > cap
|
|
1218
|
+
inputHeight = min(inputHeight, cap)
|
|
1219
|
+
}
|
|
1220
|
+
} else {
|
|
1221
|
+
let fitted = textField.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude))
|
|
1222
|
+
inputHeight = max(singleLineMinHeight, ceil(fitted.height))
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
var footerHeight: CGFloat = 0
|
|
1226
|
+
if !footerLabel.isHidden || !counterLabel.isHidden {
|
|
1227
|
+
let counterWidth = counterLabel.isHidden
|
|
1228
|
+
? 0
|
|
1229
|
+
: ceil(counterLabel.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude)).width) + 8
|
|
1230
|
+
let messageWidth = max(1, fieldWidth - counterWidth)
|
|
1231
|
+
let messageHeight = footerLabel.isHidden
|
|
1232
|
+
? 0
|
|
1233
|
+
: ceil(footerLabel.sizeThatFits(CGSize(width: messageWidth, height: .greatestFiniteMagnitude)).height)
|
|
1234
|
+
let counterHeight = counterLabel.isHidden ? 0 : ceil(counterLabel.font.lineHeight)
|
|
1235
|
+
footerHeight = max(messageHeight, counterHeight)
|
|
1236
|
+
}
|
|
1237
|
+
return Metrics(
|
|
1238
|
+
captionHeight: captionHeight,
|
|
1239
|
+
inputHeight: inputHeight,
|
|
1240
|
+
footerHeight: footerHeight,
|
|
1241
|
+
leading: leadingLabel,
|
|
1242
|
+
inputScrolls: inputScrolls
|
|
1243
|
+
)
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
public override func layoutSubviews() {
|
|
1247
|
+
super.layoutSubviews()
|
|
1248
|
+
let width = bounds.width
|
|
1249
|
+
let m = metrics(for: width)
|
|
1250
|
+
let x = fieldOffset()
|
|
1251
|
+
let fieldWidth = max(0, width - x)
|
|
1252
|
+
var y: CGFloat = 0
|
|
1253
|
+
if multiline, textView.isScrollEnabled != m.inputScrolls {
|
|
1254
|
+
textView.isScrollEnabled = m.inputScrolls
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if m.leading {
|
|
1258
|
+
// Label column and field side by side, centred on the taller one
|
|
1259
|
+
let rowHeight = max(m.inputHeight, m.captionHeight)
|
|
1260
|
+
captionLabel.frame = CGRect(
|
|
1261
|
+
x: 0,
|
|
1262
|
+
y: (rowHeight - m.captionHeight) / 2,
|
|
1263
|
+
width: leadingColumnWidth,
|
|
1264
|
+
height: m.captionHeight
|
|
1265
|
+
)
|
|
1266
|
+
activeInput.frame = CGRect(x: x, y: (rowHeight - m.inputHeight) / 2, width: fieldWidth, height: m.inputHeight)
|
|
1267
|
+
y += rowHeight
|
|
1268
|
+
} else {
|
|
1269
|
+
if m.captionHeight > 0 {
|
|
1270
|
+
captionLabel.frame = CGRect(x: 0, y: y, width: width, height: m.captionHeight)
|
|
1271
|
+
y += m.captionHeight + captionSpacing
|
|
1272
|
+
}
|
|
1273
|
+
activeInput.frame = CGRect(x: 0, y: y, width: width, height: m.inputHeight)
|
|
1274
|
+
y += m.inputHeight
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
if m.footerHeight > 0 {
|
|
1278
|
+
y += footerSpacing
|
|
1279
|
+
let counterWidth = counterLabel.isHidden
|
|
1280
|
+
? 0
|
|
1281
|
+
: ceil(counterLabel.sizeThatFits(CGSize(width: fieldWidth, height: .greatestFiniteMagnitude)).width)
|
|
1282
|
+
let messageWidth = max(0, fieldWidth - (counterWidth > 0 ? counterWidth + 8 : 0))
|
|
1283
|
+
footerLabel.frame = CGRect(x: x, y: y, width: messageWidth, height: m.footerHeight)
|
|
1284
|
+
counterLabel.frame = CGRect(x: width - counterWidth, y: y, width: counterWidth, height: m.footerHeight)
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
/// Called by the measuring pipeline to get the size for Yoga layout: the
|
|
1289
|
+
/// height the field needs at the given width. The width is whatever Yoga
|
|
1290
|
+
/// offers; a text field fills its row.
|
|
1291
|
+
@objc public func sizeForLayout(withConstrainedTo constrainedSize: CGSize) -> CGSize {
|
|
1292
|
+
let width: CGFloat
|
|
1293
|
+
if constrainedSize.width > 0 && constrainedSize.width < CGFloat.greatestFiniteMagnitude {
|
|
1294
|
+
width = constrainedSize.width
|
|
1295
|
+
} else if bounds.width > 0 {
|
|
1296
|
+
width = bounds.width
|
|
1297
|
+
} else {
|
|
1298
|
+
width = PCConstants.fallbackWidth
|
|
1299
|
+
}
|
|
1300
|
+
return CGSize(width: 0, height: ceil(metrics(for: width).total))
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// MARK: - Trait mapping
|
|
1304
|
+
|
|
1305
|
+
private static func keyboardType(_ value: String) -> UIKeyboardType {
|
|
1306
|
+
switch value {
|
|
1307
|
+
case "number-pad": return .numberPad
|
|
1308
|
+
case "decimal-pad": return .decimalPad
|
|
1309
|
+
case "numeric": return .decimalPad
|
|
1310
|
+
case "email-address": return .emailAddress
|
|
1311
|
+
case "phone-pad": return .phonePad
|
|
1312
|
+
case "url": return .URL
|
|
1313
|
+
case "ascii-capable": return .asciiCapable
|
|
1314
|
+
case "numbers-and-punctuation": return .numbersAndPunctuation
|
|
1315
|
+
case "name-phone-pad": return .namePhonePad
|
|
1316
|
+
case "twitter": return .twitter
|
|
1317
|
+
case "web-search": return .webSearch
|
|
1318
|
+
case "visible-password": return .asciiCapable
|
|
1319
|
+
default: return .default
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
private static func returnKeyType(_ value: String) -> UIReturnKeyType {
|
|
1324
|
+
switch value {
|
|
1325
|
+
case "done": return .done
|
|
1326
|
+
case "go": return .go
|
|
1327
|
+
case "next": return .next
|
|
1328
|
+
case "search": return .search
|
|
1329
|
+
case "send": return .send
|
|
1330
|
+
default: return .default
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
private static func autocapitalization(_ value: String) -> UITextAutocapitalizationType {
|
|
1335
|
+
switch value {
|
|
1336
|
+
case "none": return .none
|
|
1337
|
+
case "words": return .words
|
|
1338
|
+
case "characters": return .allCharacters
|
|
1339
|
+
default: return .sentences
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
private static func keyboardAppearance(_ value: String) -> UIKeyboardAppearance {
|
|
1344
|
+
switch value {
|
|
1345
|
+
case "light": return .light
|
|
1346
|
+
case "dark": return .dark
|
|
1347
|
+
default: return .default
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/// React Native autoComplete values onto text content types, as the
|
|
1352
|
+
/// core TextInput maps them, plus the names it leaves out. Values with no
|
|
1353
|
+
/// iOS equivalent, and unknown ones, give no content type.
|
|
1354
|
+
private static func contentType(_ value: String) -> UITextContentType? {
|
|
1355
|
+
switch value {
|
|
1356
|
+
case "username", "username-new": return .username
|
|
1357
|
+
case "password", "current-password": return .password
|
|
1358
|
+
case "new-password", "password-new": return .newPassword
|
|
1359
|
+
case "one-time-code", "sms-otp", "email-otp", "2fa-app-otp": return .oneTimeCode
|
|
1360
|
+
case "email": return .emailAddress
|
|
1361
|
+
case "name": return .name
|
|
1362
|
+
case "given-name", "name-given": return .givenName
|
|
1363
|
+
case "family-name", "name-family": return .familyName
|
|
1364
|
+
case "additional-name", "name-middle": return .middleName
|
|
1365
|
+
case "honorific-prefix", "name-prefix": return .namePrefix
|
|
1366
|
+
case "honorific-suffix", "name-suffix": return .nameSuffix
|
|
1367
|
+
case "nickname": return .nickname
|
|
1368
|
+
case "organization": return .organizationName
|
|
1369
|
+
case "organization-title": return .jobTitle
|
|
1370
|
+
case "tel", "tel-national", "tel-device": return .telephoneNumber
|
|
1371
|
+
case "street-address", "postal-address": return .fullStreetAddress
|
|
1372
|
+
case "address-line1": return .streetAddressLine1
|
|
1373
|
+
case "address-line2", "postal-address-extended": return .streetAddressLine2
|
|
1374
|
+
case "postal-address-locality": return .addressCity
|
|
1375
|
+
case "postal-address-region": return .addressState
|
|
1376
|
+
case "postal-address-dependent-locality": return .sublocality
|
|
1377
|
+
case "postal-code": return .postalCode
|
|
1378
|
+
case "country", "postal-address-country": return .countryName
|
|
1379
|
+
case "cc-number": return .creditCardNumber
|
|
1380
|
+
case "url": return .URL
|
|
1381
|
+
case "flight-number": return .flightNumber
|
|
1382
|
+
default:
|
|
1383
|
+
if #available(iOS 17.0, *) { return contentType17(value) }
|
|
1384
|
+
return nil
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
@available(iOS 17.0, *)
|
|
1389
|
+
private static func contentType17(_ value: String) -> UITextContentType? {
|
|
1390
|
+
switch value {
|
|
1391
|
+
case "cc-exp": return .creditCardExpiration
|
|
1392
|
+
case "cc-exp-month": return .creditCardExpirationMonth
|
|
1393
|
+
case "cc-exp-year": return .creditCardExpirationYear
|
|
1394
|
+
case "cc-csc": return .creditCardSecurityCode
|
|
1395
|
+
case "cc-name": return .creditCardName
|
|
1396
|
+
case "cc-given-name": return .creditCardGivenName
|
|
1397
|
+
case "cc-middle-name": return .creditCardMiddleName
|
|
1398
|
+
case "cc-family-name": return .creditCardFamilyName
|
|
1399
|
+
case "cc-type": return .creditCardType
|
|
1400
|
+
case "birthdate-full": return .birthdate
|
|
1401
|
+
case "birthdate-day": return .birthdateDay
|
|
1402
|
+
case "birthdate-month": return .birthdateMonth
|
|
1403
|
+
case "birthdate-year": return .birthdateYear
|
|
1404
|
+
default: return nil
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
private static func smartQuotesType(_ value: String) -> UITextSmartQuotesType {
|
|
1409
|
+
switch value {
|
|
1410
|
+
case "yes": return .yes
|
|
1411
|
+
case "no": return .no
|
|
1412
|
+
default: return .default
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
private static func smartDashesType(_ value: String) -> UITextSmartDashesType {
|
|
1417
|
+
switch value {
|
|
1418
|
+
case "yes": return .yes
|
|
1419
|
+
case "no": return .no
|
|
1420
|
+
default: return .default
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1424
|
+
private static func smartInsertDeleteType(_ value: String) -> UITextSmartInsertDeleteType {
|
|
1425
|
+
switch value {
|
|
1426
|
+
case "yes": return .yes
|
|
1427
|
+
case "no": return .no
|
|
1428
|
+
default: return .default
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
@available(iOS 17.0, *)
|
|
1433
|
+
private static func inlinePredictionType(_ value: String) -> UITextInlinePredictionType {
|
|
1434
|
+
switch value {
|
|
1435
|
+
case "yes": return .yes
|
|
1436
|
+
case "no": return .no
|
|
1437
|
+
default: return .default
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
@available(iOS 18.0, *)
|
|
1442
|
+
private static func mathCompletionType(_ value: String) -> UITextMathExpressionCompletionType {
|
|
1443
|
+
switch value {
|
|
1444
|
+
case "yes": return .yes
|
|
1445
|
+
case "no": return .no
|
|
1446
|
+
default: return .default
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
@available(iOS 18.0, *)
|
|
1451
|
+
private static func writingToolsBehavior(_ value: String) -> UIWritingToolsBehavior {
|
|
1452
|
+
switch value {
|
|
1453
|
+
case "complete": return .complete
|
|
1454
|
+
case "limited": return .limited
|
|
1455
|
+
case "none": return .none
|
|
1456
|
+
default: return .default
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// MARK: - Helpers
|
|
1462
|
+
|
|
1463
|
+
/// The single-line input: insets for the box the field view draws, and
|
|
1464
|
+
/// VoiceOver activation for a field acting as a button.
|
|
1465
|
+
final class PCInputTextField: UITextField {
|
|
1466
|
+
var textInsets: UIEdgeInsets = .zero {
|
|
1467
|
+
didSet { if oldValue != textInsets { setNeedsLayout() } }
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/// Returns true when it handled the activation
|
|
1471
|
+
var onActivate: (() -> Bool)?
|
|
1472
|
+
|
|
1473
|
+
private func inset(_ rect: CGRect) -> CGRect {
|
|
1474
|
+
// Accessory views bring their own margins
|
|
1475
|
+
let left = leftView == nil ? textInsets.left : 0
|
|
1476
|
+
let right = rightView == nil ? textInsets.right : 0
|
|
1477
|
+
return rect.inset(by: UIEdgeInsets(top: textInsets.top, left: left, bottom: textInsets.bottom, right: right))
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
override func textRect(forBounds bounds: CGRect) -> CGRect {
|
|
1481
|
+
inset(super.textRect(forBounds: bounds))
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
override func editingRect(forBounds bounds: CGRect) -> CGRect {
|
|
1485
|
+
inset(super.editingRect(forBounds: bounds))
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
override func placeholderRect(forBounds bounds: CGRect) -> CGRect {
|
|
1489
|
+
inset(super.placeholderRect(forBounds: bounds))
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
override func accessibilityActivate() -> Bool {
|
|
1493
|
+
onActivate?() == true || super.accessibilityActivate()
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
/// A `UITextView` with a placeholder, drawn where the text starts.
|
|
1498
|
+
final class PCPlaceholderTextView: UITextView {
|
|
1499
|
+
let placeholderLabel = UILabel()
|
|
1500
|
+
|
|
1501
|
+
/// Returns true when it handled the activation
|
|
1502
|
+
var onActivate: (() -> Bool)?
|
|
1503
|
+
|
|
1504
|
+
override func accessibilityActivate() -> Bool {
|
|
1505
|
+
onActivate?() == true || super.accessibilityActivate()
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
var placeholder: String = "" {
|
|
1509
|
+
didSet {
|
|
1510
|
+
placeholderLabel.text = placeholder
|
|
1511
|
+
updatePlaceholderVisibility()
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
override init(frame: CGRect, textContainer: NSTextContainer?) {
|
|
1516
|
+
super.init(frame: frame, textContainer: textContainer)
|
|
1517
|
+
placeholderLabel.textColor = .placeholderText
|
|
1518
|
+
placeholderLabel.numberOfLines = 0
|
|
1519
|
+
placeholderLabel.adjustsFontForContentSizeCategory = true
|
|
1520
|
+
placeholderLabel.isUserInteractionEnabled = false
|
|
1521
|
+
addSubview(placeholderLabel)
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
required init?(coder: NSCoder) {
|
|
1525
|
+
super.init(coder: coder)
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
func updatePlaceholderVisibility() {
|
|
1529
|
+
placeholderLabel.isHidden = !(text ?? "").isEmpty || placeholder.isEmpty
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
override func layoutSubviews() {
|
|
1533
|
+
super.layoutSubviews()
|
|
1534
|
+
let x = textContainerInset.left + textContainer.lineFragmentPadding
|
|
1535
|
+
let width = max(0, bounds.width - x - textContainerInset.right - textContainer.lineFragmentPadding)
|
|
1536
|
+
let height = placeholderLabel.sizeThatFits(CGSize(width: width, height: .greatestFiniteMagnitude)).height
|
|
1537
|
+
placeholderLabel.frame = CGRect(x: x, y: textContainerInset.top, width: width, height: height)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/// A button with a closure action, for the accessory slots.
|
|
1542
|
+
final class PCAccessoryButton: UIButton {
|
|
1543
|
+
var action: (() -> Void)?
|
|
1544
|
+
|
|
1545
|
+
override init(frame: CGRect) {
|
|
1546
|
+
super.init(frame: frame)
|
|
1547
|
+
addTarget(self, action: #selector(pressed), for: .touchUpInside)
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
required init?(coder: NSCoder) {
|
|
1551
|
+
super.init(coder: coder)
|
|
1552
|
+
addTarget(self, action: #selector(pressed), for: .touchUpInside)
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
@objc private func pressed() {
|
|
1556
|
+
action?()
|
|
1557
|
+
}
|
|
1558
|
+
}
|