react-native-platform-components 0.0.2
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/LICENSE +20 -0
- package/PlatformComponents.podspec +20 -0
- package/README.md +233 -0
- package/android/build.gradle +78 -0
- package/android/gradle.properties +5 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/platformcomponents/DateConstraints.kt +83 -0
- package/android/src/main/java/com/platformcomponents/Helper.kt +27 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerView.kt +684 -0
- package/android/src/main/java/com/platformcomponents/PCDatePickerViewManager.kt +149 -0
- package/android/src/main/java/com/platformcomponents/PCMaterialMode.kt +16 -0
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuView.kt +410 -0
- package/android/src/main/java/com/platformcomponents/PCSelectionMenuViewManager.kt +114 -0
- package/android/src/main/java/com/platformcomponents/PlatformComponentsPackage.kt +22 -0
- package/ios/PCDatePicker.h +11 -0
- package/ios/PCDatePicker.mm +248 -0
- package/ios/PCDatePickerView.swift +405 -0
- package/ios/PCSelectionMenu.h +10 -0
- package/ios/PCSelectionMenu.mm +182 -0
- package/ios/PCSelectionMenu.swift +434 -0
- package/lib/module/DatePicker.js +74 -0
- package/lib/module/DatePicker.js.map +1 -0
- package/lib/module/DatePickerNativeComponent.ts +68 -0
- package/lib/module/SelectionMenu.js +79 -0
- package/lib/module/SelectionMenu.js.map +1 -0
- package/lib/module/SelectionMenu.web.js +57 -0
- package/lib/module/SelectionMenu.web.js.map +1 -0
- package/lib/module/SelectionMenuNativeComponent.ts +106 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/sharedTypes.js +4 -0
- package/lib/module/sharedTypes.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/DatePicker.d.ts +38 -0
- package/lib/typescript/src/DatePicker.d.ts.map +1 -0
- package/lib/typescript/src/DatePickerNativeComponent.d.ts +53 -0
- package/lib/typescript/src/DatePickerNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.d.ts +50 -0
- package/lib/typescript/src/SelectionMenu.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenu.web.d.ts +19 -0
- package/lib/typescript/src/SelectionMenu.web.d.ts.map +1 -0
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts +85 -0
- package/lib/typescript/src/SelectionMenuNativeComponent.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +4 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/sharedTypes.d.ts +10 -0
- package/lib/typescript/src/sharedTypes.d.ts.map +1 -0
- package/package.json +178 -0
- package/shared/PCDatePickerComponentDescriptors-custom.h +52 -0
- package/shared/PCDatePickerShadowNode-custom.cpp +1 -0
- package/shared/PCDatePickerShadowNode-custom.h +27 -0
- package/shared/PCDatePickerState-custom.h +13 -0
- package/shared/PCSelectionMenuComponentDescriptors-custom.h +25 -0
- package/shared/PCSelectionMenuShadowNode-custom.cpp +36 -0
- package/shared/PCSelectionMenuShadowNode-custom.h +46 -0
- package/src/DatePicker.tsx +146 -0
- package/src/DatePickerNativeComponent.ts +68 -0
- package/src/SelectionMenu.tsx +170 -0
- package/src/SelectionMenu.web.tsx +93 -0
- package/src/SelectionMenuNativeComponent.ts +106 -0
- package/src/index.tsx +3 -0
- package/src/sharedTypes.ts +14 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// PCSelectionMenu.mm
|
|
2
|
+
|
|
3
|
+
#import "PCSelectionMenu.h"
|
|
4
|
+
|
|
5
|
+
#import <React/RCTComponentViewFactory.h>
|
|
6
|
+
#import <React/RCTConversions.h>
|
|
7
|
+
#import <React/RCTFabricComponentsPlugins.h>
|
|
8
|
+
|
|
9
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/ComponentDescriptors.h>
|
|
10
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/EventEmitters.h>
|
|
11
|
+
#import <react/renderer/components/PlatformComponentsViewSpec/Props.h>
|
|
12
|
+
|
|
13
|
+
#if __has_include(<PlatformComponents/PlatformComponents-Swift.h>)
|
|
14
|
+
#import <PlatformComponents/PlatformComponents-Swift.h>
|
|
15
|
+
#else
|
|
16
|
+
#import "PlatformComponents-Swift.h"
|
|
17
|
+
#endif
|
|
18
|
+
|
|
19
|
+
#import "PCSelectionMenuComponentDescriptors-custom.h"
|
|
20
|
+
|
|
21
|
+
using namespace facebook::react;
|
|
22
|
+
|
|
23
|
+
namespace {
|
|
24
|
+
static inline bool OptionsEqual(
|
|
25
|
+
const std::vector<facebook::react::PCSelectionMenuOptionsStruct> &a,
|
|
26
|
+
const std::vector<facebook::react::PCSelectionMenuOptionsStruct> &b) {
|
|
27
|
+
if (a.size() != b.size()) return false;
|
|
28
|
+
for (size_t i = 0; i < a.size(); i++) {
|
|
29
|
+
if (a[i].label != b[i].label) return false;
|
|
30
|
+
if (a[i].data != b[i].data) return false;
|
|
31
|
+
}
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
} // namespace
|
|
35
|
+
|
|
36
|
+
@implementation PCSelectionMenu {
|
|
37
|
+
PCSelectionMenuView *_view;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
+ (ComponentDescriptorProvider)componentDescriptorProvider {
|
|
41
|
+
return concreteComponentDescriptorProvider<
|
|
42
|
+
MeasuringPCSelectionMenuComponentDescriptor>();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
46
|
+
if (self = [super initWithFrame:frame]) {
|
|
47
|
+
_view = [PCSelectionMenuView new];
|
|
48
|
+
self.contentView = _view;
|
|
49
|
+
|
|
50
|
+
__weak __typeof(self) weakSelf = self;
|
|
51
|
+
|
|
52
|
+
_view.onSelect = ^(NSInteger index, NSString *label, NSString *data) {
|
|
53
|
+
__typeof(self) strongSelf = weakSelf;
|
|
54
|
+
if (!strongSelf) return;
|
|
55
|
+
|
|
56
|
+
auto eventEmitter =
|
|
57
|
+
std::static_pointer_cast<const PCSelectionMenuEventEmitter>(
|
|
58
|
+
strongSelf->_eventEmitter);
|
|
59
|
+
if (!eventEmitter) return;
|
|
60
|
+
|
|
61
|
+
PCSelectionMenuEventEmitter::OnSelect payload = {
|
|
62
|
+
.index = (int)index,
|
|
63
|
+
.label = label.UTF8String,
|
|
64
|
+
.data = data.UTF8String,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
eventEmitter->onSelect(payload);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
_view.onRequestClose = ^{
|
|
71
|
+
__typeof(self) strongSelf = weakSelf;
|
|
72
|
+
if (!strongSelf) return;
|
|
73
|
+
|
|
74
|
+
auto eventEmitter =
|
|
75
|
+
std::static_pointer_cast<const PCSelectionMenuEventEmitter>(
|
|
76
|
+
strongSelf->_eventEmitter);
|
|
77
|
+
if (!eventEmitter) return;
|
|
78
|
+
|
|
79
|
+
eventEmitter->onRequestClose({});
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return self;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (void)updateProps:(Props::Shared const &)props
|
|
86
|
+
oldProps:(Props::Shared const &)oldProps {
|
|
87
|
+
const auto &newProps =
|
|
88
|
+
*std::static_pointer_cast<const PCSelectionMenuProps>(props);
|
|
89
|
+
const auto prevProps =
|
|
90
|
+
std::static_pointer_cast<const PCSelectionMenuProps>(oldProps);
|
|
91
|
+
|
|
92
|
+
// options: [{label,data}]
|
|
93
|
+
if (!prevProps || !OptionsEqual(newProps.options, prevProps->options)) {
|
|
94
|
+
NSMutableArray *arr = [NSMutableArray new];
|
|
95
|
+
for (const auto &opt : newProps.options) {
|
|
96
|
+
NSString *label = opt.label.empty()
|
|
97
|
+
? @""
|
|
98
|
+
: [NSString stringWithUTF8String:opt.label.c_str()];
|
|
99
|
+
NSString *data = opt.data.empty()
|
|
100
|
+
? @""
|
|
101
|
+
: [NSString stringWithUTF8String:opt.data.c_str()];
|
|
102
|
+
[arr addObject:@{@"label" : label, @"data" : data}];
|
|
103
|
+
}
|
|
104
|
+
_view.options = arr;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// selectedData (default "")
|
|
108
|
+
if (!prevProps || newProps.selectedData != prevProps->selectedData) {
|
|
109
|
+
if (!newProps.selectedData.empty()) {
|
|
110
|
+
_view.selectedData =
|
|
111
|
+
[NSString stringWithUTF8String:newProps.selectedData.c_str()];
|
|
112
|
+
} else {
|
|
113
|
+
_view.selectedData = @""; // sentinel
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// interactivity: "enabled" | "disabled"
|
|
118
|
+
if (!prevProps || newProps.interactivity != prevProps->interactivity) {
|
|
119
|
+
if (!newProps.interactivity.empty()) {
|
|
120
|
+
_view.interactivity =
|
|
121
|
+
[NSString stringWithUTF8String:newProps.interactivity.c_str()];
|
|
122
|
+
} else {
|
|
123
|
+
_view.interactivity = @"enabled";
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// placeholder
|
|
128
|
+
if (!prevProps || newProps.placeholder != prevProps->placeholder) {
|
|
129
|
+
if (!newProps.placeholder.empty()) {
|
|
130
|
+
_view.placeholder =
|
|
131
|
+
[NSString stringWithUTF8String:newProps.placeholder.c_str()];
|
|
132
|
+
} else {
|
|
133
|
+
_view.placeholder = nil;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// anchorMode: "inline" | "headless"
|
|
138
|
+
if (!prevProps || newProps.anchorMode != prevProps->anchorMode) {
|
|
139
|
+
if (!newProps.anchorMode.empty()) {
|
|
140
|
+
_view.anchorMode =
|
|
141
|
+
[NSString stringWithUTF8String:newProps.anchorMode.c_str()];
|
|
142
|
+
} else {
|
|
143
|
+
_view.anchorMode = @"headless";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// presentation: "auto" | "popover" | "sheet"
|
|
148
|
+
if (!prevProps || newProps.presentation != prevProps->presentation) {
|
|
149
|
+
if (!newProps.presentation.empty()) {
|
|
150
|
+
_view.presentation =
|
|
151
|
+
[NSString stringWithUTF8String:newProps.presentation.c_str()];
|
|
152
|
+
} else {
|
|
153
|
+
_view.presentation = @"auto";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// visible: "open" | "closed"
|
|
158
|
+
if (!prevProps || newProps.visible != prevProps->visible) {
|
|
159
|
+
if (!newProps.visible.empty()) {
|
|
160
|
+
_view.visible = [NSString stringWithUTF8String:newProps.visible.c_str()];
|
|
161
|
+
} else {
|
|
162
|
+
_view.visible = @"closed";
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// android.material (plumbed through; iOS can ignore)
|
|
167
|
+
const auto &newAndroid = newProps.android;
|
|
168
|
+
const auto &oldAndroid =
|
|
169
|
+
prevProps ? prevProps->android : PCSelectionMenuAndroidStruct{};
|
|
170
|
+
if (!prevProps || newAndroid.material != oldAndroid.material) {
|
|
171
|
+
if (!newAndroid.material.empty()) {
|
|
172
|
+
_view.androidMaterial =
|
|
173
|
+
[NSString stringWithUTF8String:newAndroid.material.c_str()];
|
|
174
|
+
} else {
|
|
175
|
+
_view.androidMaterial = nil;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
[super updateProps:props oldProps:oldProps];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
@end
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
// MARK: - Option model (bridged from ObjC++ as dictionaries)
|
|
5
|
+
|
|
6
|
+
struct PCSelectionMenuOption {
|
|
7
|
+
let label: String
|
|
8
|
+
let data: String
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
final class PCSelectionMenuModel: ObservableObject {
|
|
12
|
+
@Published var options: [PCSelectionMenuOption] = []
|
|
13
|
+
@Published var selectedData: String = "" // sentinel = no selection
|
|
14
|
+
@Published var placeholder: String = "Select"
|
|
15
|
+
@Published var interactivity: String = "enabled" // "enabled" | "disabled"
|
|
16
|
+
|
|
17
|
+
var isDisabled: Bool { interactivity == "disabled" }
|
|
18
|
+
|
|
19
|
+
var displayTitle: String {
|
|
20
|
+
if let opt = options.first(where: { $0.data == selectedData }), !selectedData.isEmpty {
|
|
21
|
+
return opt.label
|
|
22
|
+
}
|
|
23
|
+
return placeholder
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var hasOptions: Bool { !options.isEmpty }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
private struct PCSelectionMenuInlinePickerView: View {
|
|
30
|
+
@ObservedObject var model: PCSelectionMenuModel
|
|
31
|
+
let onSelectIndex: (Int) -> Void
|
|
32
|
+
|
|
33
|
+
var body: some View {
|
|
34
|
+
Menu {
|
|
35
|
+
ForEach(Array(model.options.enumerated()), id: \.offset) { i, opt in
|
|
36
|
+
Button(opt.label) { onSelectIndex(i) }
|
|
37
|
+
}
|
|
38
|
+
} label: {
|
|
39
|
+
HStack(spacing: 8) {
|
|
40
|
+
Text(model.displayTitle)
|
|
41
|
+
.lineLimit(1)
|
|
42
|
+
.truncationMode(.tail)
|
|
43
|
+
|
|
44
|
+
Spacer(minLength: 0)
|
|
45
|
+
|
|
46
|
+
Image(systemName: "chevron.up.chevron.down")
|
|
47
|
+
.imageScale(.small)
|
|
48
|
+
.opacity(0.7)
|
|
49
|
+
}
|
|
50
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
51
|
+
.contentShape(Rectangle())
|
|
52
|
+
.padding(.vertical, 10)
|
|
53
|
+
}
|
|
54
|
+
.disabled(model.isDisabled || !model.hasOptions)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@objcMembers
|
|
59
|
+
public final class PCSelectionMenuView: UIControl,
|
|
60
|
+
UIPopoverPresentationControllerDelegate,
|
|
61
|
+
UIAdaptivePresentationControllerDelegate
|
|
62
|
+
{
|
|
63
|
+
// MARK: - Props (set from ObjC++)
|
|
64
|
+
|
|
65
|
+
/// ObjC++ sets this as an array of dictionaries: [{label,data}]
|
|
66
|
+
public var options: [Any] = [] { didSet { sync() } }
|
|
67
|
+
|
|
68
|
+
/// Controlled selection by data. "" = no selection.
|
|
69
|
+
public var selectedData: String = "" { didSet { sync() } }
|
|
70
|
+
|
|
71
|
+
/// "enabled" | "disabled"
|
|
72
|
+
public var interactivity: String = "enabled" {
|
|
73
|
+
didSet {
|
|
74
|
+
updateEnabled()
|
|
75
|
+
sync()
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public var placeholder: String? { didSet { sync() } }
|
|
80
|
+
|
|
81
|
+
/// "open" | "closed" (headless only)
|
|
82
|
+
public var visible: String = "closed" { didSet { updatePresentation() } }
|
|
83
|
+
|
|
84
|
+
/// "auto" | "popover" | "sheet" (headless only)
|
|
85
|
+
public var presentation: String = "auto" { didSet { updatePresentation() } }
|
|
86
|
+
|
|
87
|
+
/// "inline" | "headless"
|
|
88
|
+
public var anchorMode: String = "headless" { didSet { updateAnchorMode() } }
|
|
89
|
+
|
|
90
|
+
/// Android material preference (ignored on iOS; retained for debugging/log parity)
|
|
91
|
+
public var androidMaterial: String? = nil
|
|
92
|
+
|
|
93
|
+
// MARK: - Events back to ObjC++
|
|
94
|
+
|
|
95
|
+
public var onSelect: ((Int, String, String) -> Void)? // (index,label,data)
|
|
96
|
+
public var onRequestClose: (() -> Void)?
|
|
97
|
+
|
|
98
|
+
// MARK: - Internal
|
|
99
|
+
|
|
100
|
+
private weak var presentedVC: UIViewController?
|
|
101
|
+
|
|
102
|
+
private let model = PCSelectionMenuModel()
|
|
103
|
+
private var hostingController: UIHostingController<PCSelectionMenuInlinePickerView>?
|
|
104
|
+
|
|
105
|
+
private var parsedOptions: [PCSelectionMenuOption] {
|
|
106
|
+
options.compactMap { any in
|
|
107
|
+
guard let dict = any as? [String: Any] else { return nil }
|
|
108
|
+
let label = (dict["label"] as? String) ?? ""
|
|
109
|
+
let data = (dict["data"] as? String) ?? ""
|
|
110
|
+
return PCSelectionMenuOption(label: label, data: data)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// MARK: - Init
|
|
115
|
+
|
|
116
|
+
public override init(frame: CGRect) {
|
|
117
|
+
super.init(frame: frame)
|
|
118
|
+
setup()
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
public required init?(coder: NSCoder) {
|
|
122
|
+
super.init(coder: coder)
|
|
123
|
+
setup()
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private func setup() {
|
|
127
|
+
backgroundColor = .clear
|
|
128
|
+
updateEnabled()
|
|
129
|
+
updateAnchorMode()
|
|
130
|
+
sync()
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private func updateEnabled() {
|
|
134
|
+
let disabled = (interactivity == "disabled")
|
|
135
|
+
alpha = disabled ? 0.5 : 1.0
|
|
136
|
+
isUserInteractionEnabled = !disabled
|
|
137
|
+
accessibilityTraits = disabled ? [.notEnabled] : [.button]
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// MARK: - Inline vs headless
|
|
141
|
+
|
|
142
|
+
private func updateAnchorMode() {
|
|
143
|
+
if anchorMode == "inline" {
|
|
144
|
+
dismissIfNeeded(emitClose: false)
|
|
145
|
+
installInlineIfNeeded()
|
|
146
|
+
sync()
|
|
147
|
+
} else {
|
|
148
|
+
uninstallInlineIfNeeded()
|
|
149
|
+
updatePresentation()
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private func sync() {
|
|
154
|
+
model.options = parsedOptions
|
|
155
|
+
model.selectedData = selectedData
|
|
156
|
+
model.placeholder = placeholder ?? "Select"
|
|
157
|
+
model.interactivity = interactivity
|
|
158
|
+
|
|
159
|
+
if anchorMode == "inline" {
|
|
160
|
+
hostingController?.rootView = makeInlineRootView()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
invalidateIntrinsicContentSize()
|
|
164
|
+
setNeedsLayout()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func makeInlineRootView() -> PCSelectionMenuInlinePickerView {
|
|
168
|
+
PCSelectionMenuInlinePickerView(
|
|
169
|
+
model: model,
|
|
170
|
+
onSelectIndex: { [weak self] idx in
|
|
171
|
+
guard let self else { return }
|
|
172
|
+
let opts = self.parsedOptions
|
|
173
|
+
guard idx >= 0, idx < opts.count else { return }
|
|
174
|
+
let opt = opts[idx]
|
|
175
|
+
self.selectedData = opt.data
|
|
176
|
+
self.onSelect?(idx, opt.label, opt.data)
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private func installInlineIfNeeded() {
|
|
182
|
+
guard hostingController == nil else { return }
|
|
183
|
+
|
|
184
|
+
let host = UIHostingController(rootView: makeInlineRootView())
|
|
185
|
+
host.view.translatesAutoresizingMaskIntoConstraints = false
|
|
186
|
+
host.view.backgroundColor = .clear
|
|
187
|
+
|
|
188
|
+
addSubview(host.view)
|
|
189
|
+
NSLayoutConstraint.activate([
|
|
190
|
+
host.view.topAnchor.constraint(equalTo: topAnchor),
|
|
191
|
+
host.view.bottomAnchor.constraint(equalTo: bottomAnchor),
|
|
192
|
+
host.view.leadingAnchor.constraint(equalTo: leadingAnchor),
|
|
193
|
+
host.view.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
194
|
+
])
|
|
195
|
+
|
|
196
|
+
if let parent = nearestViewController() {
|
|
197
|
+
parent.addChild(host)
|
|
198
|
+
host.didMove(toParent: parent)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
hostingController = host
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private func uninstallInlineIfNeeded() {
|
|
205
|
+
guard let host = hostingController else { return }
|
|
206
|
+
hostingController = nil
|
|
207
|
+
|
|
208
|
+
host.willMove(toParent: nil)
|
|
209
|
+
host.view.removeFromSuperview()
|
|
210
|
+
host.removeFromParent()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func nearestViewController() -> UIViewController? {
|
|
214
|
+
var r: UIResponder? = self
|
|
215
|
+
while let next = r?.next {
|
|
216
|
+
if let vc = next as? UIViewController { return vc }
|
|
217
|
+
r = next
|
|
218
|
+
}
|
|
219
|
+
return nil
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// MARK: - Headless presentation
|
|
223
|
+
|
|
224
|
+
private func updatePresentation() {
|
|
225
|
+
guard anchorMode != "inline" else { return }
|
|
226
|
+
guard interactivity != "disabled" else { return }
|
|
227
|
+
|
|
228
|
+
if visible == "open" {
|
|
229
|
+
presentIfNeeded()
|
|
230
|
+
} else {
|
|
231
|
+
dismissIfNeeded(emitClose: false)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func presentIfNeeded() {
|
|
236
|
+
guard presentedVC == nil else { return }
|
|
237
|
+
guard let top = topViewController() else { return }
|
|
238
|
+
|
|
239
|
+
let opts = parsedOptions
|
|
240
|
+
let list = PCSelectionMenuListViewController(
|
|
241
|
+
titleText: placeholder ?? "Select",
|
|
242
|
+
options: opts,
|
|
243
|
+
selectedData: selectedData,
|
|
244
|
+
onSelect: { [weak self] idx in
|
|
245
|
+
guard let self else { return }
|
|
246
|
+
guard idx >= 0, idx < opts.count else { return }
|
|
247
|
+
let opt = opts[idx]
|
|
248
|
+
self.selectedData = opt.data
|
|
249
|
+
self.onSelect?(idx, opt.label, opt.data)
|
|
250
|
+
self.dismissIfNeeded(emitClose: true)
|
|
251
|
+
},
|
|
252
|
+
onCancel: { [weak self] in
|
|
253
|
+
self?.dismissIfNeeded(emitClose: true)
|
|
254
|
+
}
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
let nav = UINavigationController(rootViewController: list)
|
|
258
|
+
let style = resolvedModalStyle(for: top)
|
|
259
|
+
nav.modalPresentationStyle = style
|
|
260
|
+
|
|
261
|
+
if style == .popover {
|
|
262
|
+
nav.preferredContentSize = list.computePreferredSize()
|
|
263
|
+
|
|
264
|
+
if let pop = nav.popoverPresentationController {
|
|
265
|
+
pop.delegate = self
|
|
266
|
+
pop.sourceView = top.view
|
|
267
|
+
let rectInTopView = self.convert(self.bounds, to: top.view)
|
|
268
|
+
pop.sourceRect = rectInTopView
|
|
269
|
+
pop.permittedArrowDirections = [.up, .down]
|
|
270
|
+
pop.backgroundColor = list.view.backgroundColor
|
|
271
|
+
}
|
|
272
|
+
} else {
|
|
273
|
+
nav.modalPresentationStyle = .pageSheet
|
|
274
|
+
nav.presentationController?.delegate = self
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
nav.presentationController?.delegate = self
|
|
278
|
+
|
|
279
|
+
presentedVC = nav
|
|
280
|
+
top.present(nav, animated: true)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private func dismissIfNeeded(emitClose: Bool) {
|
|
284
|
+
guard let vc = presentedVC else { return }
|
|
285
|
+
presentedVC = nil
|
|
286
|
+
vc.dismiss(animated: true) { [weak self] in
|
|
287
|
+
guard let self else { return }
|
|
288
|
+
if emitClose { self.onRequestClose?() }
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private func resolvedModalStyle(for top: UIViewController) -> UIModalPresentationStyle {
|
|
293
|
+
let isPad = (top.traitCollection.userInterfaceIdiom == .pad)
|
|
294
|
+
switch presentation {
|
|
295
|
+
case "popover":
|
|
296
|
+
return .popover
|
|
297
|
+
case "sheet":
|
|
298
|
+
return .pageSheet
|
|
299
|
+
default:
|
|
300
|
+
return isPad ? .popover : .pageSheet
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// MARK: - Dismiss callbacks
|
|
305
|
+
|
|
306
|
+
public func popoverPresentationControllerDidDismissPopover(
|
|
307
|
+
_ popoverPresentationController: UIPopoverPresentationController
|
|
308
|
+
) {
|
|
309
|
+
presentedVC = nil
|
|
310
|
+
onRequestClose?()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
public func presentationControllerDidDismiss(_ presentationController: UIPresentationController)
|
|
314
|
+
{
|
|
315
|
+
presentedVC = nil
|
|
316
|
+
onRequestClose?()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
public func adaptivePresentationStyle(for controller: UIPresentationController)
|
|
320
|
+
-> UIModalPresentationStyle
|
|
321
|
+
{
|
|
322
|
+
.none
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// MARK: - Top VC helper
|
|
326
|
+
|
|
327
|
+
private func topViewController() -> UIViewController? {
|
|
328
|
+
guard
|
|
329
|
+
var top = UIApplication.shared.connectedScenes
|
|
330
|
+
.compactMap({ $0 as? UIWindowScene })
|
|
331
|
+
.flatMap({ $0.windows })
|
|
332
|
+
.first(where: { $0.isKeyWindow })?.rootViewController
|
|
333
|
+
else { return nil }
|
|
334
|
+
|
|
335
|
+
while true {
|
|
336
|
+
if let presented = top.presentedViewController {
|
|
337
|
+
top = presented
|
|
338
|
+
continue
|
|
339
|
+
}
|
|
340
|
+
if let nav = top as? UINavigationController, let visible = nav.visibleViewController {
|
|
341
|
+
top = visible
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
if let tab = top as? UITabBarController, let selected = tab.selectedViewController {
|
|
345
|
+
top = selected
|
|
346
|
+
continue
|
|
347
|
+
}
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
return top
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// MARK: - sizing (inline only)
|
|
354
|
+
|
|
355
|
+
public override func sizeThatFits(_ size: CGSize) -> CGSize {
|
|
356
|
+
if anchorMode != "inline" { return CGSize(width: size.width, height: 0) }
|
|
357
|
+
|
|
358
|
+
let minH: CGFloat = 44
|
|
359
|
+
guard let host = hostingController else {
|
|
360
|
+
return CGSize(width: size.width, height: minH)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
let w = (size.width > 1) ? size.width : 320
|
|
364
|
+
let fitted = host.sizeThatFits(in: CGSize(width: w, height: .greatestFiniteMagnitude))
|
|
365
|
+
return CGSize(width: size.width, height: max(minH, fitted.height))
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
public override var intrinsicContentSize: CGSize {
|
|
369
|
+
if anchorMode != "inline" {
|
|
370
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: 0)
|
|
371
|
+
}
|
|
372
|
+
let h = max(
|
|
373
|
+
44, sizeThatFits(CGSize(width: bounds.width, height: .greatestFiniteMagnitude)).height)
|
|
374
|
+
return CGSize(width: UIView.noIntrinsicMetric, height: h)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// MARK: - List VC (headless)
|
|
379
|
+
|
|
380
|
+
private final class PCSelectionMenuListViewController: UITableViewController {
|
|
381
|
+
|
|
382
|
+
private let titleText: String
|
|
383
|
+
private let options: [PCSelectionMenuOption]
|
|
384
|
+
private let selectedData: String
|
|
385
|
+
private let onSelectIndex: (Int) -> Void
|
|
386
|
+
private let onCancel: () -> Void
|
|
387
|
+
|
|
388
|
+
init(
|
|
389
|
+
titleText: String,
|
|
390
|
+
options: [PCSelectionMenuOption],
|
|
391
|
+
selectedData: String,
|
|
392
|
+
onSelect: @escaping (Int) -> Void,
|
|
393
|
+
onCancel: @escaping () -> Void
|
|
394
|
+
) {
|
|
395
|
+
self.titleText = titleText
|
|
396
|
+
self.options = options
|
|
397
|
+
self.selectedData = selectedData
|
|
398
|
+
self.onSelectIndex = onSelect
|
|
399
|
+
self.onCancel = onCancel
|
|
400
|
+
super.init(style: .insetGrouped)
|
|
401
|
+
title = titleText
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
405
|
+
|
|
406
|
+
override func viewDidLoad() {
|
|
407
|
+
super.viewDidLoad()
|
|
408
|
+
view.backgroundColor = .systemBackground
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
|
412
|
+
options.count
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)
|
|
416
|
+
-> UITableViewCell
|
|
417
|
+
{
|
|
418
|
+
let cell = UITableViewCell(style: .default, reuseIdentifier: nil)
|
|
419
|
+
let opt = options[indexPath.row]
|
|
420
|
+
cell.textLabel?.text = opt.label
|
|
421
|
+
cell.accessoryType =
|
|
422
|
+
(opt.data == selectedData && !selectedData.isEmpty) ? .checkmark : .none
|
|
423
|
+
return cell
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
427
|
+
tableView.deselectRow(at: indexPath, animated: true)
|
|
428
|
+
onSelectIndex(indexPath.row)
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
func computePreferredSize() -> CGSize {
|
|
432
|
+
CGSize(width: 320, height: min(480, CGFloat(options.count) * 44))
|
|
433
|
+
}
|
|
434
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// DatePicker.tsx
|
|
4
|
+
import React, { useCallback, useMemo } from 'react';
|
|
5
|
+
import { StyleSheet } from 'react-native';
|
|
6
|
+
import NativeDatePicker from './DatePickerNativeComponent';
|
|
7
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
8
|
+
function dateToMsOrMinusOne(d) {
|
|
9
|
+
return d ? d.getTime() : -1;
|
|
10
|
+
}
|
|
11
|
+
function normalizeVisible(presentation, visible) {
|
|
12
|
+
// Only meaningful in modal presentation. Keep undefined for inline to avoid noise.
|
|
13
|
+
if (presentation !== 'modal') return undefined;
|
|
14
|
+
return visible ? 'open' : 'closed';
|
|
15
|
+
}
|
|
16
|
+
export function DatePicker(props) {
|
|
17
|
+
const {
|
|
18
|
+
style,
|
|
19
|
+
date,
|
|
20
|
+
minDate,
|
|
21
|
+
maxDate,
|
|
22
|
+
locale,
|
|
23
|
+
timeZoneName,
|
|
24
|
+
mode,
|
|
25
|
+
presentation = 'modal',
|
|
26
|
+
visible,
|
|
27
|
+
onConfirm,
|
|
28
|
+
onClosed,
|
|
29
|
+
ios,
|
|
30
|
+
android
|
|
31
|
+
} = props;
|
|
32
|
+
const handleConfirm = useCallback(e => {
|
|
33
|
+
onConfirm?.(new Date(e.nativeEvent.timestampMs));
|
|
34
|
+
}, [onConfirm]);
|
|
35
|
+
const handleClosed = useCallback(() => {
|
|
36
|
+
onClosed?.();
|
|
37
|
+
}, [onClosed]);
|
|
38
|
+
const styles = useMemo(() => createStyles(), []);
|
|
39
|
+
const nativeProps = {
|
|
40
|
+
style: [styles.picker, style],
|
|
41
|
+
mode,
|
|
42
|
+
locale,
|
|
43
|
+
timeZoneName,
|
|
44
|
+
presentation,
|
|
45
|
+
visible: normalizeVisible(presentation, visible),
|
|
46
|
+
dateMs: dateToMsOrMinusOne(date),
|
|
47
|
+
minDateMs: dateToMsOrMinusOne(minDate ?? null),
|
|
48
|
+
maxDateMs: dateToMsOrMinusOne(maxDate ?? null),
|
|
49
|
+
onConfirm: onConfirm ? handleConfirm : undefined,
|
|
50
|
+
onClosed: onClosed ? handleClosed : undefined,
|
|
51
|
+
ios: ios ? {
|
|
52
|
+
preferredStyle: ios.preferredStyle,
|
|
53
|
+
countDownDurationSeconds: ios.countDownDurationSeconds,
|
|
54
|
+
minuteInterval: ios.minuteInterval,
|
|
55
|
+
roundsToMinuteInterval: ios.roundsToMinuteInterval
|
|
56
|
+
} : undefined,
|
|
57
|
+
android: android ? {
|
|
58
|
+
firstDayOfWeek: android.firstDayOfWeek,
|
|
59
|
+
material: android.material,
|
|
60
|
+
dialogTitle: android.dialogTitle,
|
|
61
|
+
positiveButtonTitle: android.positiveButtonTitle,
|
|
62
|
+
negativeButtonTitle: android.negativeButtonTitle
|
|
63
|
+
} : undefined
|
|
64
|
+
};
|
|
65
|
+
return /*#__PURE__*/_jsx(NativeDatePicker, {
|
|
66
|
+
...nativeProps
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function createStyles() {
|
|
70
|
+
return StyleSheet.create({
|
|
71
|
+
picker: {}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=DatePicker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["React","useCallback","useMemo","StyleSheet","NativeDatePicker","jsx","_jsx","dateToMsOrMinusOne","d","getTime","normalizeVisible","presentation","visible","undefined","DatePicker","props","style","date","minDate","maxDate","locale","timeZoneName","mode","onConfirm","onClosed","ios","android","handleConfirm","e","Date","nativeEvent","timestampMs","handleClosed","styles","createStyles","nativeProps","picker","dateMs","minDateMs","maxDateMs","preferredStyle","countDownDurationSeconds","minuteInterval","roundsToMinuteInterval","firstDayOfWeek","material","dialogTitle","positiveButtonTitle","negativeButtonTitle","create"],"sourceRoot":"../../src","sources":["DatePicker.tsx"],"mappings":";;AAAA;AACA,OAAOA,KAAK,IAAIC,WAAW,EAAEC,OAAO,QAAQ,OAAO;AAEnD,SAASC,UAAU,QAAQ,cAAc;AAEzC,OAAOC,gBAAgB,MAShB,6BAA6B;AAAC,SAAAC,GAAA,IAAAC,IAAA;AA4CrC,SAASC,kBAAkBA,CAACC,CAA0B,EAAU;EAC9D,OAAOA,CAAC,GAAGA,CAAC,CAACC,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;AAC7B;AAEA,SAASC,gBAAgBA,CACvBC,YAA+D,EAC/DC,OAA4B,EACP;EACrB;EACA,IAAID,YAAY,KAAK,OAAO,EAAE,OAAOE,SAAS;EAC9C,OAAOD,OAAO,GAAG,MAAM,GAAG,QAAQ;AACpC;AAEA,OAAO,SAASE,UAAUA,CAACC,KAAsB,EAAsB;EACrE,MAAM;IACJC,KAAK;IACLC,IAAI;IACJC,OAAO;IACPC,OAAO;IACPC,MAAM;IACNC,YAAY;IACZC,IAAI;IACJX,YAAY,GAAG,OAAO;IACtBC,OAAO;IACPW,SAAS;IACTC,QAAQ;IACRC,GAAG;IACHC;EACF,CAAC,GAAGX,KAAK;EAET,MAAMY,aAAa,GAAG1B,WAAW,CAC9B2B,CAAwC,IAAK;IAC5CL,SAAS,GAAG,IAAIM,IAAI,CAACD,CAAC,CAACE,WAAW,CAACC,WAAW,CAAC,CAAC;EAClD,CAAC,EACD,CAACR,SAAS,CACZ,CAAC;EAED,MAAMS,YAAY,GAAG/B,WAAW,CAAC,MAAM;IACrCuB,QAAQ,GAAG,CAAC;EACd,CAAC,EAAE,CAACA,QAAQ,CAAC,CAAC;EAEd,MAAMS,MAAM,GAAG/B,OAAO,CAAC,MAAMgC,YAAY,CAAC,CAAC,EAAE,EAAE,CAAC;EAEhD,MAAMC,WAAkC,GAAG;IACzCnB,KAAK,EAAE,CAACiB,MAAM,CAACG,MAAM,EAAEpB,KAAK,CAAQ;IAEpCM,IAAI;IACJF,MAAM;IACNC,YAAY;IAEZV,YAAY;IACZC,OAAO,EAAEF,gBAAgB,CAACC,YAAY,EAAEC,OAAO,CAAQ;IAEvDyB,MAAM,EAAE9B,kBAAkB,CAACU,IAAI,CAAQ;IACvCqB,SAAS,EAAE/B,kBAAkB,CAACW,OAAO,IAAI,IAAI,CAAQ;IACrDqB,SAAS,EAAEhC,kBAAkB,CAACY,OAAO,IAAI,IAAI,CAAQ;IAErDI,SAAS,EAAEA,SAAS,GAAGI,aAAa,GAAGd,SAAS;IAChDW,QAAQ,EAAEA,QAAQ,GAAGQ,YAAY,GAAGnB,SAAS;IAE7CY,GAAG,EAAEA,GAAG,GACJ;MACEe,cAAc,EAAEf,GAAG,CAACe,cAAc;MAClCC,wBAAwB,EAAEhB,GAAG,CAACgB,wBAAwB;MACtDC,cAAc,EAAEjB,GAAG,CAACiB,cAAc;MAClCC,sBAAsB,EAAElB,GAAG,CAACkB;IAC9B,CAAC,GACD9B,SAAS;IAEba,OAAO,EAAEA,OAAO,GACZ;MACEkB,cAAc,EAAElB,OAAO,CAACkB,cAAc;MACtCC,QAAQ,EAAEnB,OAAO,CAACmB,QAAe;MACjCC,WAAW,EAAEpB,OAAO,CAACoB,WAAW;MAChCC,mBAAmB,EAAErB,OAAO,CAACqB,mBAAmB;MAChDC,mBAAmB,EAAEtB,OAAO,CAACsB;IAC/B,CAAC,GACDnC;EACN,CAAC;EAED,oBAAOP,IAAA,CAACF,gBAAgB;IAAA,GAAK+B;EAAW,CAAG,CAAC;AAC9C;AAEA,SAASD,YAAYA,CAAA,EAAG;EACtB,OAAO/B,UAAU,CAAC8C,MAAM,CAAC;IACvBb,MAAM,EAAE,CAAC;EACX,CAAC,CAAC;AACJ","ignoreList":[]}
|