@onekeyfe/react-native-native-list 3.0.157-alpha.249 → 3.0.157-alpha.250
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/README.md +21 -5
- package/android/src/main/java/com/onekey/nativelist/NativeListAccessoryStack.kt +711 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActionRenderer.kt +169 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListActivityRenderer.kt +243 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListAdapter.kt +20 -6
- package/android/src/main/java/com/onekey/nativelist/NativeListDataRowRenderer.kt +487 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListIdentityRenderer.kt +820 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListImageSlot.kt +147 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListLeadingVisual.kt +453 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMarketRenderer.kt +997 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMediaTileRenderer.kt +234 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMessageRenderer.kt +232 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListMetricCardRenderer.kt +551 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListModels.kt +24 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListPrimitives.kt +475 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRailRenderer.kt +143 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRegistry.kt +42 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRendererRowView.kt +415 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListResolvedText.kt +163 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowHost.kt +70 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSectionHeaderRenderer.kt +748 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListSystemRenderer.kt +464 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListTextView.kt +25 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListView.kt +174 -104
- package/android/src/main/java/com/onekey/nativelist/NativeListWalletGroupRenderer.kt +407 -0
- package/android/src/test/java/com/margelo/nitro/nativelist/NativeListSelectorBackgroundColorTest.kt +34 -0
- package/docs/DESIGN.md +518 -9
- package/docs/MIGRATION_BASELINE.md +117 -0
- package/docs/ROW_TEMPLATES.md +359 -0
- package/docs/SPEC.md +358 -0
- package/docs/STYLE_SPEC.md +978 -0
- package/docs/images/list-capabilities.svg +39 -0
- package/docs/images/row-action.svg +17 -0
- package/docs/images/row-activity.svg +24 -0
- package/docs/images/row-data.svg +24 -0
- package/docs/images/row-identity.svg +25 -0
- package/docs/images/row-market.svg +23 -0
- package/docs/images/row-media-tile.svg +26 -0
- package/docs/images/row-message.svg +20 -0
- package/docs/images/row-metric-card.svg +33 -0
- package/docs/images/row-rail.svg +18 -0
- package/docs/images/row-section-header.svg +19 -0
- package/docs/images/row-style-regions.svg +30 -0
- package/docs/images/row-system.svg +30 -0
- package/docs/images/row-wallet-group.svg +26 -0
- package/ios/NativeListAccessoryStack.swift +680 -0
- package/ios/NativeListActionRenderer.swift +108 -0
- package/ios/NativeListActivityRenderer.swift +179 -0
- package/ios/NativeListDataRowRenderer.swift +444 -0
- package/ios/NativeListIdentityRenderer.swift +685 -0
- package/ios/NativeListImageSlot.swift +140 -0
- package/ios/NativeListLeadingVisual.swift +372 -0
- package/ios/NativeListMarketRenderer.swift +698 -0
- package/ios/NativeListMediaTileRenderer.swift +184 -0
- package/ios/NativeListMessageRenderer.swift +168 -0
- package/ios/NativeListMetricCardRenderer.swift +420 -0
- package/ios/NativeListModels.swift +32 -0
- package/ios/NativeListRailRenderer.swift +152 -0
- package/ios/NativeListRendererCell.swift +311 -0
- package/ios/NativeListRendererRegistry.swift +50 -0
- package/ios/NativeListResolvedText.swift +122 -0
- package/ios/NativeListRowHost.swift +63 -0
- package/ios/NativeListSectionHeaderRenderer.swift +708 -0
- package/ios/NativeListSystemRenderer.swift +568 -0
- package/ios/NativeListTextLabel.swift +42 -0
- package/ios/NativeListTextStyles.swift +236 -0
- package/ios/NativeListWalletGroupRenderer.swift +255 -0
- package/ios/RNCNativeListView.swift +111 -180
- package/lib/module/models.js +54 -0
- package/lib/module/validation.js +324 -26
- package/lib/module/web/NativeListWebEngine.js +148 -936
- package/lib/module/web/templates/ActionRowRenderer.js +97 -0
- package/lib/module/web/templates/ActivityRowRenderer.js +95 -0
- package/lib/module/web/templates/DataRowRenderer.js +145 -0
- package/lib/module/web/templates/IdentityRowRenderer.js +289 -0
- package/lib/module/web/templates/MarketRowRenderer.js +310 -0
- package/lib/module/web/templates/MediaTileRowRenderer.js +132 -0
- package/lib/module/web/templates/MessageRowRenderer.js +165 -0
- package/lib/module/web/templates/MetricCardRowRenderer.js +172 -0
- package/lib/module/web/templates/RailRowRenderer.js +98 -0
- package/lib/module/web/templates/RowContainerStyle.js +100 -0
- package/lib/module/web/templates/RowElements.js +93 -0
- package/lib/module/web/templates/RowRendererRegistry.js +70 -0
- package/lib/module/web/templates/RowText.js +121 -0
- package/lib/module/web/templates/RowVisual.js +43 -0
- package/lib/module/web/templates/SectionHeaderRowRenderer.js +146 -0
- package/lib/module/web/templates/SystemRowRenderer.js +161 -0
- package/lib/module/web/templates/WalletGroupRowRenderer.js +96 -0
- package/lib/typescript/src/models.d.ts +166 -13
- package/lib/typescript/src/web/NativeListWebEngine.d.ts +18 -16
- package/lib/typescript/src/web/templates/ActionRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/ActivityRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/DataRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/IdentityRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/MarketRowRenderer.d.ts +30 -0
- package/lib/typescript/src/web/templates/MediaTileRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/MessageRowRenderer.d.ts +44 -0
- package/lib/typescript/src/web/templates/MetricCardRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/RailRowRenderer.d.ts +28 -0
- package/lib/typescript/src/web/templates/RowContainerStyle.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowElements.d.ts +38 -0
- package/lib/typescript/src/web/templates/RowRendererRegistry.d.ts +83 -0
- package/lib/typescript/src/web/templates/RowText.d.ts +9 -0
- package/lib/typescript/src/web/templates/RowVisual.d.ts +20 -0
- package/lib/typescript/src/web/templates/SectionHeaderRowRenderer.d.ts +15 -0
- package/lib/typescript/src/web/templates/SystemRowRenderer.d.ts +16 -0
- package/lib/typescript/src/web/templates/WalletGroupRowRenderer.d.ts +20 -0
- package/package.json +3 -3
- package/src/models.ts +281 -19
- package/src/validation.ts +501 -46
- package/src/web/NativeListWebEngine.ts +361 -1932
- package/src/web/templates/ActionRowRenderer.ts +123 -0
- package/src/web/templates/ActivityRowRenderer.ts +159 -0
- package/src/web/templates/DataRowRenderer.ts +212 -0
- package/src/web/templates/IdentityRowRenderer.ts +527 -0
- package/src/web/templates/MarketRowRenderer.ts +467 -0
- package/src/web/templates/MediaTileRowRenderer.ts +182 -0
- package/src/web/templates/MessageRowRenderer.ts +306 -0
- package/src/web/templates/MetricCardRowRenderer.ts +337 -0
- package/src/web/templates/RailRowRenderer.ts +136 -0
- package/src/web/templates/RowContainerStyle.ts +131 -0
- package/src/web/templates/RowElements.ts +160 -0
- package/src/web/templates/RowRendererRegistry.ts +111 -0
- package/src/web/templates/RowText.ts +181 -0
- package/src/web/templates/RowVisual.ts +115 -0
- package/src/web/templates/SectionHeaderRowRenderer.ts +243 -0
- package/src/web/templates/SystemRowRenderer.ts +282 -0
- package/src/web/templates/WalletGroupRowRenderer.ts +131 -0
- package/android/src/main/java/com/onekey/nativelist/NativeListRowView.kt +0 -4538
- package/ios/NativeListCell.swift +0 -4300
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import CoreText
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
// Apply semantic overrides after a renderer has restored its resolved defaults.
|
|
5
|
+
enum NativeListTextStyles {
|
|
6
|
+
static func applyStyledText(_ label: UILabel, _ style: [String: Any]) {
|
|
7
|
+
let text = label.attributedText?.string ?? label.text ?? ""
|
|
8
|
+
guard !text.isEmpty, !style.isEmpty else { return }
|
|
9
|
+
let originalText = label.attributedText
|
|
10
|
+
let textLabel = label as? NativeListTextLabel
|
|
11
|
+
let baseFont =
|
|
12
|
+
(originalText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont) ?? label.font
|
|
13
|
+
?? nativeListFont(ofSize: 14)
|
|
14
|
+
let size = CGFloat(style.double("fontSize", default: Double(baseFont.pointSize)))
|
|
15
|
+
let font: UIFont
|
|
16
|
+
if let weightName = style["fontWeight"] as? String {
|
|
17
|
+
font = nativeListFont(
|
|
18
|
+
ofSize: size,
|
|
19
|
+
weight: marketFontWeight(weightName, fallback: .regular)
|
|
20
|
+
)
|
|
21
|
+
} else {
|
|
22
|
+
font = baseFont.withSize(size)
|
|
23
|
+
}
|
|
24
|
+
let color =
|
|
25
|
+
(style["color"] as? String)
|
|
26
|
+
.map { UIColor(nativeListHex: $0, fallback: label.textColor ?? .black) }
|
|
27
|
+
?? label.textColor ?? .black
|
|
28
|
+
if style["lines"] != nil {
|
|
29
|
+
label.numberOfLines = min(3, max(1, style.int("lines", default: 1)))
|
|
30
|
+
label.lineBreakMode = .byTruncatingTail
|
|
31
|
+
}
|
|
32
|
+
if let alignmentName = style["alignment"] as? String {
|
|
33
|
+
label.textAlignment = marketTextAlignment(
|
|
34
|
+
alignmentName, direction: label.effectiveUserInterfaceLayoutDirection)
|
|
35
|
+
}
|
|
36
|
+
if style["truncate"] != nil || style["lines"] != nil {
|
|
37
|
+
label.lineBreakMode = styledLineBreakMode(style, lines: label.numberOfLines)
|
|
38
|
+
}
|
|
39
|
+
if let alignment = style["verticalAlignment"] as? String {
|
|
40
|
+
textLabel?.rowVerticalAlignment = alignment
|
|
41
|
+
}
|
|
42
|
+
if style["offsetY"] != nil { textLabel?.rowOffsetY = CGFloat(style.double("offsetY")) }
|
|
43
|
+
label.font = font
|
|
44
|
+
label.textColor = color
|
|
45
|
+
let attributed =
|
|
46
|
+
originalText.map { NSMutableAttributedString(attributedString: $0) }
|
|
47
|
+
?? NSMutableAttributedString(string: text)
|
|
48
|
+
let paragraph =
|
|
49
|
+
(attributed.attribute(.paragraphStyle, at: 0, effectiveRange: nil)
|
|
50
|
+
as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
|
51
|
+
if style["alignment"] != nil { paragraph.alignment = label.textAlignment }
|
|
52
|
+
if style["lines"] != nil || style["truncate"] != nil {
|
|
53
|
+
paragraph.lineBreakMode = label.lineBreakMode
|
|
54
|
+
}
|
|
55
|
+
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
56
|
+
applyStyledFont(attributed, style, fallback: baseFont)
|
|
57
|
+
if style["color"] != nil { attributes[.foregroundColor] = color }
|
|
58
|
+
if style["alignment"] != nil || style["lines"] != nil || style["truncate"] != nil
|
|
59
|
+
|| style["lineHeight"] != nil
|
|
60
|
+
{
|
|
61
|
+
attributes[.paragraphStyle] = paragraph
|
|
62
|
+
}
|
|
63
|
+
if style["lineHeight"] != nil {
|
|
64
|
+
let box = CGFloat(style.double("lineHeight"))
|
|
65
|
+
paragraph.minimumLineHeight = box
|
|
66
|
+
paragraph.maximumLineHeight = box
|
|
67
|
+
// React Native centers font metrics inside an explicit line height.
|
|
68
|
+
attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
|
|
69
|
+
}
|
|
70
|
+
attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
|
|
71
|
+
if label.numberOfLines == 1 && (style["lines"] != nil || style["truncate"] != nil) {
|
|
72
|
+
normalizeSingleLine(attributed)
|
|
73
|
+
}
|
|
74
|
+
label.attributedText = attributed
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
static func applyStyledButton(_ button: UIButton, _ style: [String: Any]) {
|
|
78
|
+
let text =
|
|
79
|
+
button.attributedTitle(for: .normal)?.string
|
|
80
|
+
?? button.title(for: .normal)
|
|
81
|
+
?? ""
|
|
82
|
+
guard !text.isEmpty, !style.isEmpty else { return }
|
|
83
|
+
let originalText = button.attributedTitle(for: .normal)
|
|
84
|
+
let accessory = button as? NativeListAccessoryButton
|
|
85
|
+
let baseFont =
|
|
86
|
+
(originalText?.attribute(.font, at: 0, effectiveRange: nil) as? UIFont) ?? button.titleLabel?
|
|
87
|
+
.font ?? nativeListFont(ofSize: 14)
|
|
88
|
+
let size = CGFloat(style.double("fontSize", default: Double(baseFont.pointSize)))
|
|
89
|
+
let font: UIFont
|
|
90
|
+
if let weightName = style["fontWeight"] as? String {
|
|
91
|
+
font = nativeListFont(
|
|
92
|
+
ofSize: size,
|
|
93
|
+
weight: marketFontWeight(weightName, fallback: .regular)
|
|
94
|
+
)
|
|
95
|
+
} else {
|
|
96
|
+
font = baseFont.withSize(size)
|
|
97
|
+
}
|
|
98
|
+
let fallbackColor = button.titleColor(for: .normal) ?? .black
|
|
99
|
+
let color =
|
|
100
|
+
(style["color"] as? String)
|
|
101
|
+
.map { UIColor(nativeListHex: $0, fallback: fallbackColor) }
|
|
102
|
+
?? fallbackColor
|
|
103
|
+
let attributed =
|
|
104
|
+
button.attributedTitle(for: .normal)
|
|
105
|
+
.map { NSMutableAttributedString(attributedString: $0) }
|
|
106
|
+
?? NSMutableAttributedString(string: text)
|
|
107
|
+
let paragraph =
|
|
108
|
+
(attributed.attribute(.paragraphStyle, at: 0, effectiveRange: nil)
|
|
109
|
+
as? NSParagraphStyle)?.mutableCopy() as? NSMutableParagraphStyle ?? NSMutableParagraphStyle()
|
|
110
|
+
if let alignmentName = style["alignment"] as? String {
|
|
111
|
+
paragraph.alignment = marketTextAlignment(
|
|
112
|
+
alignmentName, direction: button.effectiveUserInterfaceLayoutDirection)
|
|
113
|
+
button.contentHorizontalAlignment =
|
|
114
|
+
alignmentName == "start"
|
|
115
|
+
? .leading
|
|
116
|
+
: alignmentName == "end" ? .trailing : .center
|
|
117
|
+
}
|
|
118
|
+
var attributes: [NSAttributedString.Key: Any] = [:]
|
|
119
|
+
applyStyledFont(attributed, style, fallback: baseFont)
|
|
120
|
+
if style["color"] != nil { attributes[.foregroundColor] = color }
|
|
121
|
+
if style["alignment"] != nil || style["lineHeight"] != nil || style["lines"] != nil
|
|
122
|
+
|| style["truncate"] != nil
|
|
123
|
+
{
|
|
124
|
+
attributes[.paragraphStyle] = paragraph
|
|
125
|
+
}
|
|
126
|
+
if style["lineHeight"] != nil {
|
|
127
|
+
let box = CGFloat(style.double("lineHeight"))
|
|
128
|
+
paragraph.minimumLineHeight = box
|
|
129
|
+
paragraph.maximumLineHeight = box
|
|
130
|
+
attributes[.baselineOffset] = max(0, (box - font.lineHeight) / 2)
|
|
131
|
+
if accessory?.marketLineHeight != nil { accessory?.marketLineHeight = box }
|
|
132
|
+
if accessory?.selectorSummaryLineHeight != nil { accessory?.selectorSummaryLineHeight = box }
|
|
133
|
+
}
|
|
134
|
+
if style["lines"] != nil {
|
|
135
|
+
button.titleLabel?.numberOfLines = min(3, max(1, style.int("lines", default: 1)))
|
|
136
|
+
}
|
|
137
|
+
if style["lines"] != nil || style["truncate"] != nil {
|
|
138
|
+
let mode = styledLineBreakMode(style, lines: button.titleLabel?.numberOfLines ?? 1)
|
|
139
|
+
button.titleLabel?.lineBreakMode = mode
|
|
140
|
+
paragraph.lineBreakMode = mode
|
|
141
|
+
if button.titleLabel?.numberOfLines == 1 { normalizeSingleLine(attributed) }
|
|
142
|
+
}
|
|
143
|
+
if let alignment = style["verticalAlignment"] as? String {
|
|
144
|
+
button.contentVerticalAlignment =
|
|
145
|
+
alignment == "top" ? .top : alignment == "bottom" ? .bottom : .center
|
|
146
|
+
}
|
|
147
|
+
if style["offsetY"] != nil {
|
|
148
|
+
(button as? NativeListAccessoryButton)?.rowTextOffsetY = CGFloat(style.double("offsetY"))
|
|
149
|
+
}
|
|
150
|
+
button.titleLabel?.font = font
|
|
151
|
+
attributed.addAttributes(attributes, range: NSRange(location: 0, length: attributed.length))
|
|
152
|
+
button.setAttributedTitle(attributed, for: .normal)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static func applyStyledFont(
|
|
156
|
+
_ text: NSMutableAttributedString, _ style: [String: Any], fallback: UIFont
|
|
157
|
+
) {
|
|
158
|
+
guard style["fontSize"] != nil || style["fontWeight"] != nil else { return }
|
|
159
|
+
text.enumerateAttribute(.font, in: NSRange(location: 0, length: text.length)) {
|
|
160
|
+
value, range, _ in
|
|
161
|
+
let original = (value as? UIFont) ?? fallback
|
|
162
|
+
let size = CGFloat(style.double("fontSize", default: Double(original.pointSize)))
|
|
163
|
+
let font =
|
|
164
|
+
style["fontWeight"] == nil
|
|
165
|
+
? original.withSize(size)
|
|
166
|
+
: nativeListFont(
|
|
167
|
+
ofSize: size, weight: marketFontWeight(style.string("fontWeight"), fallback: .regular))
|
|
168
|
+
text.addAttribute(.font, value: font, range: range)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
static func normalizeSingleLine(_ text: NSMutableAttributedString) {
|
|
173
|
+
let pattern = try! NSRegularExpression(pattern: "\\r\\n|[\\r\\n]")
|
|
174
|
+
for match in pattern.matches(in: text.string, range: NSRange(location: 0, length: text.length))
|
|
175
|
+
.reversed()
|
|
176
|
+
{
|
|
177
|
+
text.replaceCharacters(in: match.range, with: " ")
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
static func styledLineBreakMode(_ style: [String: Any], lines: Int) -> NSLineBreakMode {
|
|
182
|
+
if style.string("truncate", default: "tail") != "clip" { return .byTruncatingTail }
|
|
183
|
+
// Clipping disables wrapping; multi-line clip must wrap without adding an ellipsis.
|
|
184
|
+
return lines == 1 ? .byClipping : .byWordWrapping
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
static func marketFontWeight(_ value: String, fallback: NativeListFontWeight)
|
|
188
|
+
-> NativeListFontWeight
|
|
189
|
+
{
|
|
190
|
+
switch value {
|
|
191
|
+
case "regular": return .regular
|
|
192
|
+
case "semibold": return .semibold
|
|
193
|
+
case "bold": return .bold
|
|
194
|
+
case "medium": return .medium
|
|
195
|
+
default: return fallback
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Legacy selector/warning typography: monospaced digits and zero kerning on the label.
|
|
200
|
+
static func applyTabularDigits(_ label: UILabel) {
|
|
201
|
+
guard let font = label.font else { return }
|
|
202
|
+
label.font = tabularDigitsFont(font)
|
|
203
|
+
guard let original = label.attributedText else { return }
|
|
204
|
+
let result = NSMutableAttributedString(attributedString: original)
|
|
205
|
+
let range = NSRange(location: 0, length: result.length)
|
|
206
|
+
result.addAttribute(.kern, value: 0, range: range)
|
|
207
|
+
original.enumerateAttribute(.font, in: range) { value, range, _ in
|
|
208
|
+
if let font = value as? UIFont {
|
|
209
|
+
result.addAttribute(.font, value: tabularDigitsFont(font), range: range)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
label.attributedText = result
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
static func tabularDigitsFont(_ font: UIFont) -> UIFont {
|
|
216
|
+
var settings =
|
|
217
|
+
font.fontDescriptor.fontAttributes[.featureSettings] as? [[UIFontDescriptor.FeatureKey: Int]]
|
|
218
|
+
?? []
|
|
219
|
+
settings.removeAll { $0[.type] == kNumberSpacingType }
|
|
220
|
+
settings.append([.type: kNumberSpacingType, .selector: kMonospacedNumbersSelector])
|
|
221
|
+
return UIFont(
|
|
222
|
+
descriptor: font.fontDescriptor.addingAttributes([.featureSettings: settings]),
|
|
223
|
+
size: font.pointSize)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
static func marketTextAlignment(_ value: String, direction: UIUserInterfaceLayoutDirection)
|
|
227
|
+
-> NSTextAlignment
|
|
228
|
+
{
|
|
229
|
+
switch value {
|
|
230
|
+
case "center": return .center
|
|
231
|
+
case "start": return direction == .rightToLeft ? .right : .left
|
|
232
|
+
case "end": return direction == .rightToLeft ? .left : .right
|
|
233
|
+
default: return .natural
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Composite only: member content, assets and local styles belong to Identity.
|
|
4
|
+
final class NativeListWalletGroupCell: NativeListRendererCell, UIGestureRecognizerDelegate {
|
|
5
|
+
private struct MemberView {
|
|
6
|
+
let cell: NativeListIdentityCell
|
|
7
|
+
let height: NSLayoutConstraint
|
|
8
|
+
}
|
|
9
|
+
private var members: [NativeListItem] = []
|
|
10
|
+
private var memberViews: [String: MemberView] = [:]
|
|
11
|
+
private var compactCell: NativeListIdentityCell?
|
|
12
|
+
private let compactContainer = UIView()
|
|
13
|
+
private let dragBadge = NativeListInsetLabel()
|
|
14
|
+
private var compact = false
|
|
15
|
+
private lazy var memberTap = UITapGestureRecognizer(
|
|
16
|
+
target: self, action: #selector(memberPressed(_:)))
|
|
17
|
+
override var usesIntrinsicContentHeight: Bool { true }
|
|
18
|
+
override var defaultCornerRadius: CGFloat { 20 }
|
|
19
|
+
override var defaultCornerCurve: CALayerCornerCurve { .continuous }
|
|
20
|
+
override var defaultBorderWidth: CGFloat { 1 }
|
|
21
|
+
override var pressChangesBackground: Bool { false }
|
|
22
|
+
override var showsSelection: Bool { false }
|
|
23
|
+
// Legacy fill is `subduedBackground`; the group's row-level `backgroundColor` is ignored.
|
|
24
|
+
override var honorsRowBackgroundColor: Bool { false }
|
|
25
|
+
override func defaultBorderColor(_ theme: [String: Any]?) -> UIColor {
|
|
26
|
+
nativeListColor(theme, "separator", "#E0E0E0")
|
|
27
|
+
}
|
|
28
|
+
override func unselectedBackground(
|
|
29
|
+
_ item: NativeListItem, theme: [String: Any]?, layout: String, itemIndex: Int?
|
|
30
|
+
) -> UIColor {
|
|
31
|
+
nativeListColor(theme, "subduedBackground", "#F9F9F9")
|
|
32
|
+
}
|
|
33
|
+
private static func memberItems(_ item: NativeListItem) -> [NativeListItem] {
|
|
34
|
+
([item.data.dictionary("parent")].compactMap { $0 } + item.data.dictionaries("children"))
|
|
35
|
+
.compactMap { try? NativeListItem(data: $0) }
|
|
36
|
+
}
|
|
37
|
+
private static func memberHeight(_ item: NativeListItem) -> CGFloat {
|
|
38
|
+
item.styledHeight
|
|
39
|
+
?? (item.data["height"] != nil
|
|
40
|
+
? CGFloat(item.data.double("height"))
|
|
41
|
+
// Legacy member default is independent of the member's presentation.
|
|
42
|
+
: item.data.dictionaries("badges").isEmpty ? 68 : 92)
|
|
43
|
+
}
|
|
44
|
+
/// Legacy default: a one-point inset (source group border) when the parent carries an
|
|
45
|
+
/// explicit height, otherwise none. `style.*Padding` overrides it; bind and measure share it.
|
|
46
|
+
private static func padding(_ item: NativeListItem, members: [NativeListItem]) -> (
|
|
47
|
+
horizontal: CGFloat, vertical: CGFloat
|
|
48
|
+
) {
|
|
49
|
+
let style = item.data.dictionary("style") ?? [:]
|
|
50
|
+
let inset: Double = members.first?.data["height"] != nil ? 1 : 0
|
|
51
|
+
return (
|
|
52
|
+
CGFloat(style.double("horizontalPadding", default: inset)),
|
|
53
|
+
CGFloat(style.double("verticalPadding", default: inset))
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
override class func appliesSizePreset(_ item: NativeListItem) -> Bool { false }
|
|
57
|
+
override class func measure(
|
|
58
|
+
_ item: NativeListItem, width: CGFloat, theme: [String: Any]?, layout: String
|
|
59
|
+
) -> CGFloat? {
|
|
60
|
+
let members = memberItems(item)
|
|
61
|
+
return members.reduce(
|
|
62
|
+
CGFloat(max(0, members.count - 1) * 12) + padding(item, members: members).vertical * 2
|
|
63
|
+
) { $0 + memberHeight($1) }
|
|
64
|
+
}
|
|
65
|
+
override init(frame: CGRect) {
|
|
66
|
+
super.init(frame: frame)
|
|
67
|
+
root.axis = .vertical
|
|
68
|
+
root.alignment = .fill
|
|
69
|
+
root.spacing = 12
|
|
70
|
+
memberTap.delegate = self
|
|
71
|
+
root.addGestureRecognizer(memberTap)
|
|
72
|
+
compactContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
73
|
+
compactContainer.isUserInteractionEnabled = false
|
|
74
|
+
compactContainer.isHidden = true
|
|
75
|
+
contentView.addSubview(compactContainer)
|
|
76
|
+
dragBadge.translatesAutoresizingMaskIntoConstraints = false
|
|
77
|
+
dragBadge.horizontalInset = 6
|
|
78
|
+
dragBadge.textAlignment = .center
|
|
79
|
+
dragBadge.font = nativeListTabularFont(ofSize: 12, weight: .semibold)
|
|
80
|
+
dragBadge.layer.cornerRadius = 12
|
|
81
|
+
dragBadge.layer.masksToBounds = true
|
|
82
|
+
dragBadge.layer.borderWidth = 1
|
|
83
|
+
compactContainer.addSubview(dragBadge)
|
|
84
|
+
NSLayoutConstraint.activate([
|
|
85
|
+
compactContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
|
86
|
+
compactContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
|
|
87
|
+
compactContainer.topAnchor.constraint(equalTo: contentView.topAnchor),
|
|
88
|
+
compactContainer.heightAnchor.constraint(equalToConstant: 68),
|
|
89
|
+
dragBadge.trailingAnchor.constraint(equalTo: compactContainer.trailingAnchor, constant: -4),
|
|
90
|
+
dragBadge.bottomAnchor.constraint(equalTo: compactContainer.bottomAnchor, constant: -4),
|
|
91
|
+
dragBadge.heightAnchor.constraint(equalToConstant: 24),
|
|
92
|
+
dragBadge.widthAnchor.constraint(greaterThanOrEqualToConstant: 24),
|
|
93
|
+
])
|
|
94
|
+
}
|
|
95
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
96
|
+
override func accessibilityText(_ item: NativeListItem) -> String {
|
|
97
|
+
item.data.string(
|
|
98
|
+
"accessibilityLabel", default: item.data.dictionary("parent")?.string("title") ?? "")
|
|
99
|
+
}
|
|
100
|
+
override func bindContent(
|
|
101
|
+
_ item: NativeListItem, theme: [String: Any]?, layout: String,
|
|
102
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
103
|
+
) {
|
|
104
|
+
members = Self.memberItems(item)
|
|
105
|
+
let keys = Set(members.map(\.key))
|
|
106
|
+
for key in Array(memberViews.keys) where !keys.contains(key) {
|
|
107
|
+
let view = memberViews.removeValue(forKey: key)!
|
|
108
|
+
view.cell.prepareForReuse()
|
|
109
|
+
root.removeArrangedSubview(view.cell)
|
|
110
|
+
view.cell.removeFromSuperview()
|
|
111
|
+
}
|
|
112
|
+
let (horizontal, vertical) = Self.padding(item, members: members)
|
|
113
|
+
contentInsets = UIEdgeInsets(
|
|
114
|
+
top: vertical, left: horizontal, bottom: vertical, right: horizontal)
|
|
115
|
+
for (index, member) in members.enumerated() {
|
|
116
|
+
let view: MemberView
|
|
117
|
+
if let existing = memberViews[member.key] {
|
|
118
|
+
view = existing
|
|
119
|
+
} else {
|
|
120
|
+
let cell = NativeListIdentityCell(frame: .zero)
|
|
121
|
+
cell.translatesAutoresizingMaskIntoConstraints = false
|
|
122
|
+
let height = cell.heightAnchor.constraint(equalToConstant: Self.memberHeight(member))
|
|
123
|
+
height.isActive = true
|
|
124
|
+
view = MemberView(cell: cell, height: height)
|
|
125
|
+
memberViews[member.key] = view
|
|
126
|
+
cell.onAction = { [weak self] item, action, target, origin in
|
|
127
|
+
self?.onAction?(item, action, target, origin)
|
|
128
|
+
}
|
|
129
|
+
cell.onBindingInvalidated = { [weak self] cell, epoch in
|
|
130
|
+
self?.onBindingInvalidated?(cell, epoch)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
view.height.constant = Self.memberHeight(member)
|
|
134
|
+
view.cell.listStyle = listStyle
|
|
135
|
+
view.cell.bind(
|
|
136
|
+
item: member, theme: theme, layout: layout, selected: member.data.bool("selected"),
|
|
137
|
+
checkboxState: checkboxState)
|
|
138
|
+
root.insertArrangedSubview(view.cell, at: index)
|
|
139
|
+
}
|
|
140
|
+
if let parent = members.first {
|
|
141
|
+
if compactCell == nil {
|
|
142
|
+
let cell = NativeListIdentityCell(frame: .zero)
|
|
143
|
+
cell.translatesAutoresizingMaskIntoConstraints = false
|
|
144
|
+
compactContainer.insertSubview(cell, at: 0)
|
|
145
|
+
NSLayoutConstraint.activate([
|
|
146
|
+
cell.leadingAnchor.constraint(equalTo: compactContainer.leadingAnchor),
|
|
147
|
+
cell.trailingAnchor.constraint(equalTo: compactContainer.trailingAnchor),
|
|
148
|
+
cell.topAnchor.constraint(equalTo: compactContainer.topAnchor),
|
|
149
|
+
cell.bottomAnchor.constraint(equalTo: compactContainer.bottomAnchor),
|
|
150
|
+
])
|
|
151
|
+
compactCell = cell
|
|
152
|
+
}
|
|
153
|
+
compactCell?.listStyle = listStyle
|
|
154
|
+
compactCell?.bind(
|
|
155
|
+
item: parent, theme: theme, layout: layout, selected: parent.data.bool("selected"),
|
|
156
|
+
checkboxState: checkboxState)
|
|
157
|
+
}
|
|
158
|
+
let count = members.dropFirst().filter { ($0.data["draggable"] as? Bool) != false }.count
|
|
159
|
+
dragBadge.text = count > 0 ? "+\(count)" : nil
|
|
160
|
+
dragBadge.isHidden = count == 0
|
|
161
|
+
dragBadge.backgroundColor = nativeListColor(theme, "inverseBackground", "#202020")
|
|
162
|
+
dragBadge.textColor = nativeListColor(theme, "inverseText", "#FCFCFC")
|
|
163
|
+
dragBadge.layer.borderColor = nativeListColor(theme, "rowBackground", "#FFFFFF").cgColor
|
|
164
|
+
}
|
|
165
|
+
override func bindSelectionContent(
|
|
166
|
+
_ item: NativeListItem,
|
|
167
|
+
checkboxState: (NativeListItem, NativeSelectionTarget?, String) -> String
|
|
168
|
+
) {
|
|
169
|
+
members = Self.memberItems(item)
|
|
170
|
+
for member in members {
|
|
171
|
+
memberViews[member.key]?.cell.updateSelection(
|
|
172
|
+
item: member, selected: member.data.bool("selected"), checkboxState: checkboxState)
|
|
173
|
+
}
|
|
174
|
+
if let parent = members.first {
|
|
175
|
+
compactCell?.updateSelection(
|
|
176
|
+
item: parent, selected: parent.data.bool("selected"), checkboxState: checkboxState)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch)
|
|
180
|
+
-> Bool
|
|
181
|
+
{
|
|
182
|
+
var view = touch.view
|
|
183
|
+
while let current = view, current !== root {
|
|
184
|
+
if current is UIControl { return false }
|
|
185
|
+
view = current.superview
|
|
186
|
+
}
|
|
187
|
+
return true
|
|
188
|
+
}
|
|
189
|
+
@objc private func memberPressed(_ gesture: UITapGestureRecognizer) {
|
|
190
|
+
let point = gesture.location(in: root)
|
|
191
|
+
for member in members {
|
|
192
|
+
guard let cell = memberViews[member.key]?.cell, cell.frame.contains(point) else { continue }
|
|
193
|
+
if !member.data.bool("disabled") && !member.data.bool("pressDisabled") {
|
|
194
|
+
onAction?(member, "press", nil, cell.rowActionOrigin())
|
|
195
|
+
}
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
override func canStartWalletGroupReorder(at point: CGPoint) -> Bool {
|
|
200
|
+
let point = root.convert(point, from: self)
|
|
201
|
+
for member in members where memberViews[member.key]?.cell.frame.contains(point) == true {
|
|
202
|
+
// Legacy: only `draggable: false` excludes a member; disabled members may start a drag.
|
|
203
|
+
return (member.data["draggable"] as? Bool) != false
|
|
204
|
+
}
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
override func applyAppearance() {
|
|
208
|
+
super.applyAppearance()
|
|
209
|
+
if compact {
|
|
210
|
+
contentView.backgroundColor = .clear
|
|
211
|
+
contentView.layer.borderWidth = 0
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
override func setPressed(_ pressed: Bool) {
|
|
215
|
+
if compact { compactCell?.setPressed(pressed) } else { super.setPressed(pressed) }
|
|
216
|
+
}
|
|
217
|
+
override func setWalletGroupReorderCompact(_ compact: Bool) {
|
|
218
|
+
self.compact = compact
|
|
219
|
+
root.isHidden = compact
|
|
220
|
+
root.alpha = compact ? 0 : 1
|
|
221
|
+
compactContainer.isHidden = !compact
|
|
222
|
+
compactContainer.alpha = compact ? 1 : 0
|
|
223
|
+
applyAppearance()
|
|
224
|
+
}
|
|
225
|
+
override func prepareWalletGroupReorderExpansion() {
|
|
226
|
+
compact = false
|
|
227
|
+
root.isHidden = false
|
|
228
|
+
root.alpha = 0
|
|
229
|
+
compactContainer.isHidden = false
|
|
230
|
+
compactContainer.alpha = 1
|
|
231
|
+
applyAppearance()
|
|
232
|
+
}
|
|
233
|
+
override func animateWalletGroupReorderExpansion() {
|
|
234
|
+
root.alpha = 1
|
|
235
|
+
compactContainer.alpha = 0
|
|
236
|
+
}
|
|
237
|
+
override func finishWalletGroupReorderExpansion() { setWalletGroupReorderCompact(false) }
|
|
238
|
+
override func recycleContent() {
|
|
239
|
+
for view in memberViews.values {
|
|
240
|
+
view.cell.prepareForReuse()
|
|
241
|
+
view.cell.removeFromSuperview()
|
|
242
|
+
}
|
|
243
|
+
memberViews.removeAll()
|
|
244
|
+
members.removeAll()
|
|
245
|
+
compactCell?.prepareForReuse()
|
|
246
|
+
compactCell?.removeFromSuperview()
|
|
247
|
+
compactCell = nil
|
|
248
|
+
compact = false
|
|
249
|
+
root.isHidden = false
|
|
250
|
+
root.alpha = 1
|
|
251
|
+
compactContainer.isHidden = true
|
|
252
|
+
compactContainer.alpha = 1
|
|
253
|
+
dragBadge.text = nil
|
|
254
|
+
}
|
|
255
|
+
}
|