| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 #import "ios/chrome/browser/payments/payment_request_edit_view_controller.h" |
| 6 |
| 7 #include "base/logging.h" |
| 8 #import "base/mac/foundation_util.h" |
| 9 #include "base/strings/sys_string_conversions.h" |
| 10 #include "components/strings/grit/components_strings.h" |
| 11 #import "ios/chrome/browser/payments/cells/payments_text_item.h" |
| 12 #import "ios/chrome/browser/ui/collection_view/cells/MDCCollectionViewCell+Chrom
e.h" |
| 13 #import "ios/chrome/browser/ui/colors/MDCPalette+CrAdditions.h" |
| 14 #import "ios/chrome/browser/ui/settings/autofill_edit_accessory_view.h" |
| 15 #import "ios/chrome/browser/ui/settings/cells/autofill_edit_item.h" |
| 16 #import "ios/chrome/browser/ui/uikit_ui_util.h" |
| 17 #include "ios/chrome/grit/ios_theme_resources.h" |
| 18 #import "ios/third_party/material_components_ios/src/components/Typography/src/M
aterialTypography.h" |
| 19 #include "ui/base/l10n/l10n_util.h" |
| 20 |
| 21 #if !defined(__has_feature) || !__has_feature(objc_arc) |
| 22 #error "This file requires ARC support." |
| 23 #endif |
| 24 |
| 25 namespace { |
| 26 |
| 27 const CGFloat kSeparatorEdgeInset = 14; |
| 28 |
| 29 AutofillEditCell* AutofillEditCellForTextField(UITextField* textField) { |
| 30 for (UIView* view = textField; view; view = [view superview]) { |
| 31 AutofillEditCell* cell = base::mac::ObjCCast<AutofillEditCell>(view); |
| 32 if (cell) |
| 33 return cell; |
| 34 } |
| 35 |
| 36 // There has to be a cell associated with this text field. |
| 37 NOTREACHED(); |
| 38 return nil; |
| 39 } |
| 40 |
| 41 // Item type for the error message item. This value is chosen to avoid |
| 42 // overlapping with the values starting from kItemTypeEnumZero. This is a |
| 43 // repeated item type. |
| 44 const NSInteger ItemTypeErrorMessage = kItemTypeEnumZero + 100; |
| 45 |
| 46 } // namespace |
| 47 |
| 48 @interface PaymentRequestEditViewController ()<AutofillEditAccessoryDelegate> { |
| 49 // The currently focused cell. May be nil. |
| 50 __weak AutofillEditCell* _currentEditingCell; |
| 51 |
| 52 AutofillEditAccessoryView* _accessoryView; |
| 53 } |
| 54 |
| 55 // Returns the index path for the cell associated with the currently focused |
| 56 // text field. |
| 57 - (NSIndexPath*)indexPathForCurrentTextField; |
| 58 |
| 59 // Returns the validation error string for |value| for the autofill type |
| 60 // |autofillType|. |required| indicates whether this is a required field or not. |
| 61 // If there are no validation errors, an empty string is returned. |
| 62 - (NSString*)validateValue:(NSString*)value |
| 63 autofillType:(autofill::ServerFieldType)autofillType |
| 64 required:(BOOL)required; |
| 65 |
| 66 // Adds an error message item in the section |sectionIdentifier| if |
| 67 // |errorMessage| is non-empty. Otherwise removes such an item if one exists. |
| 68 - (void)addOrRemoveErrorMessage:(NSString*)errorMessage |
| 69 inSectionWithIdentifier:(NSInteger)sectionIdentifier; |
| 70 |
| 71 // Returns the indexPath for the same row as that of |indexPath| in a section |
| 72 // with the given offset relative to that of |indexPath|. It may return nil. |
| 73 - (NSIndexPath*)indexPathWithSectionOffset:(NSInteger)offset |
| 74 fromPath:(NSIndexPath*)indexPath; |
| 75 |
| 76 // Returns the text field with the given offset relative to the currently |
| 77 // focused text field. May return nil. |
| 78 - (AutofillEditCell*)nextTextFieldWithOffset:(NSInteger)offset; |
| 79 |
| 80 // Enables or disables the accessory view's previous and next buttons depending |
| 81 // on whether there is a text field before and after the currently focused text |
| 82 // field. |
| 83 - (void)updateAccessoryViewButtonsStates; |
| 84 |
| 85 @end |
| 86 |
| 87 @implementation PaymentRequestEditViewController |
| 88 |
| 89 - (instancetype)init { |
| 90 self = [super initWithStyle:CollectionViewControllerStyleAppBar]; |
| 91 if (self) { |
| 92 _accessoryView = [[AutofillEditAccessoryView alloc] initWithDelegate:self]; |
| 93 } |
| 94 return self; |
| 95 } |
| 96 |
| 97 - (void)viewDidAppear:(BOOL)animated { |
| 98 [super viewDidAppear:animated]; |
| 99 [[NSNotificationCenter defaultCenter] |
| 100 addObserver:self |
| 101 selector:@selector(keyboardDidShow) |
| 102 name:UIKeyboardDidShowNotification |
| 103 object:nil]; |
| 104 } |
| 105 |
| 106 - (void)viewWillDisappear:(BOOL)animated { |
| 107 [super viewWillDisappear:animated]; |
| 108 [[NSNotificationCenter defaultCenter] |
| 109 removeObserver:self |
| 110 name:UIKeyboardDidShowNotification |
| 111 object:nil]; |
| 112 } |
| 113 |
| 114 #pragma mark - CollectionViewController methods |
| 115 |
| 116 - (void)loadModel { |
| 117 [super loadModel]; |
| 118 CollectionViewModel* model = self.collectionViewModel; |
| 119 |
| 120 // Iterate over the field definitions and add the respective sections and |
| 121 // items. |
| 122 for (const auto& field : [self editorFields]) { |
| 123 [model addSectionWithIdentifier:field.section_id]; |
| 124 AutofillEditItem* item = |
| 125 [[AutofillEditItem alloc] initWithType:field.item_type]; |
| 126 item.textFieldName = base::SysUTF16ToNSString(field.label); |
| 127 item.textFieldEnabled = YES; |
| 128 item.required = field.required; |
| 129 item.autofillType = field.data_type; |
| 130 [model addItem:item toSectionWithIdentifier:field.section_id]; |
| 131 } |
| 132 } |
| 133 |
| 134 - (void)viewDidLoad { |
| 135 [super viewDidLoad]; |
| 136 |
| 137 // Customize collection view settings. |
| 138 self.styler.cellStyle = MDCCollectionViewCellStyleCard; |
| 139 self.styler.separatorInset = |
| 140 UIEdgeInsetsMake(0, kSeparatorEdgeInset, 0, kSeparatorEdgeInset); |
| 141 } |
| 142 |
| 143 #pragma mark - UITextFieldDelegate |
| 144 |
| 145 - (void)textFieldDidBeginEditing:(UITextField*)textField { |
| 146 _currentEditingCell = AutofillEditCellForTextField(textField); |
| 147 [textField setInputAccessoryView:_accessoryView]; |
| 148 [self updateAccessoryViewButtonsStates]; |
| 149 } |
| 150 |
| 151 - (void)textFieldDidEndEditing:(UITextField*)textField { |
| 152 DCHECK(_currentEditingCell == AutofillEditCellForTextField(textField)); |
| 153 CollectionViewModel* model = self.collectionViewModel; |
| 154 |
| 155 // Validate the text field. If there is a validation error, display an error |
| 156 // message item in the same section as the field. Otherwise remove the error |
| 157 // message item in that section. |
| 158 NSIndexPath* indexPath = [self indexPathForCurrentTextField]; |
| 159 AutofillEditItem* item = base::mac::ObjCCastStrict<AutofillEditItem>( |
| 160 [model itemAtIndexPath:indexPath]); |
| 161 |
| 162 NSString* errorMessage = [self validateValue:textField.text |
| 163 autofillType:item.autofillType |
| 164 required:item.required]; |
| 165 NSInteger sectionIdentifier = |
| 166 [model sectionIdentifierForSection:[indexPath section]]; |
| 167 [self addOrRemoveErrorMessage:errorMessage |
| 168 inSectionWithIdentifier:sectionIdentifier]; |
| 169 |
| 170 [textField setInputAccessoryView:nil]; |
| 171 _currentEditingCell = nil; |
| 172 } |
| 173 |
| 174 - (BOOL)textFieldShouldReturn:(UITextField*)textField { |
| 175 DCHECK([_currentEditingCell textField] == textField); |
| 176 [self nextPressed]; |
| 177 return NO; |
| 178 } |
| 179 |
| 180 #pragma mark - AutofillEditAccessoryDelegate |
| 181 |
| 182 - (void)nextPressed { |
| 183 AutofillEditCell* nextCell = [self nextTextFieldWithOffset:1]; |
| 184 if (nextCell) |
| 185 [nextCell.textField becomeFirstResponder]; |
| 186 } |
| 187 |
| 188 - (void)previousPressed { |
| 189 AutofillEditCell* previousCell = [self nextTextFieldWithOffset:-1]; |
| 190 if (previousCell) |
| 191 [previousCell.textField becomeFirstResponder]; |
| 192 } |
| 193 |
| 194 - (void)closePressed { |
| 195 [[_currentEditingCell textField] resignFirstResponder]; |
| 196 } |
| 197 |
| 198 #pragma mark - UICollectionViewDataSource |
| 199 |
| 200 - (UICollectionViewCell*)collectionView:(UICollectionView*)collectionView |
| 201 cellForItemAtIndexPath:(NSIndexPath*)indexPath { |
| 202 UICollectionViewCell* cell = |
| 203 [super collectionView:collectionView cellForItemAtIndexPath:indexPath]; |
| 204 |
| 205 if ([cell isKindOfClass:[AutofillEditCell class]]) { |
| 206 AutofillEditCell* autofillEditCell = |
| 207 base::mac::ObjCCast<AutofillEditCell>(cell); |
| 208 autofillEditCell.textField.delegate = self; |
| 209 } |
| 210 |
| 211 CollectionViewItem* item = |
| 212 [self.collectionViewModel itemAtIndexPath:indexPath]; |
| 213 switch (item.type) { |
| 214 case ItemTypeErrorMessage: { |
| 215 PaymentsTextCell* errorMessageCell = |
| 216 base::mac::ObjCCastStrict<PaymentsTextCell>(cell); |
| 217 errorMessageCell.textLabel.font = [MDCTypography body1Font]; |
| 218 errorMessageCell.textLabel.textColor = |
| 219 [[MDCPalette cr_redPalette] tint600]; |
| 220 break; |
| 221 } |
| 222 default: |
| 223 break; |
| 224 } |
| 225 |
| 226 return cell; |
| 227 } |
| 228 |
| 229 #pragma mark MDCCollectionViewStylingDelegate |
| 230 |
| 231 - (CGFloat)collectionView:(UICollectionView*)collectionView |
| 232 cellHeightAtIndexPath:(NSIndexPath*)indexPath { |
| 233 CollectionViewItem* item = |
| 234 [self.collectionViewModel itemAtIndexPath:indexPath]; |
| 235 |
| 236 if ([item isKindOfClass:[AutofillEditItem class]] || |
| 237 item.type == ItemTypeErrorMessage) { |
| 238 return [MDCCollectionViewCell |
| 239 cr_preferredHeightForWidth:CGRectGetWidth(collectionView.bounds) |
| 240 forItem:item]; |
| 241 } |
| 242 |
| 243 NOTREACHED(); |
| 244 return MDCCellDefaultOneLineHeight; |
| 245 } |
| 246 |
| 247 - (BOOL)collectionView:(UICollectionView*)collectionView |
| 248 hidesInkViewAtIndexPath:(NSIndexPath*)indexPath { |
| 249 NSInteger type = [self.collectionViewModel itemTypeForIndexPath:indexPath]; |
| 250 switch (type) { |
| 251 case ItemTypeErrorMessage: |
| 252 return YES; |
| 253 default: |
| 254 return NO; |
| 255 } |
| 256 } |
| 257 |
| 258 #pragma mark - Protected methods |
| 259 |
| 260 - (std::vector<EditorField>)editorFields { |
| 261 return std::vector<EditorField>(); |
| 262 } |
| 263 |
| 264 #pragma mark - Helper methods |
| 265 |
| 266 - (NSIndexPath*)indexPathForCurrentTextField { |
| 267 DCHECK(_currentEditingCell); |
| 268 NSIndexPath* indexPath = |
| 269 [[self collectionView] indexPathForCell:_currentEditingCell]; |
| 270 DCHECK(indexPath); |
| 271 return indexPath; |
| 272 } |
| 273 |
| 274 - (NSString*)validateValue:(NSString*)value |
| 275 autofillType:(autofill::ServerFieldType)autofillType |
| 276 required:(BOOL)required { |
| 277 if (required && value.length == 0) { |
| 278 return l10n_util::GetNSString( |
| 279 IDS_PAYMENTS_FIELD_REQUIRED_VALIDATION_MESSAGE); |
| 280 } |
| 281 return @""; |
| 282 } |
| 283 |
| 284 - (void)addOrRemoveErrorMessage:(NSString*)errorMessage |
| 285 inSectionWithIdentifier:(NSInteger)sectionIdentifier { |
| 286 CollectionViewModel* model = self.collectionViewModel; |
| 287 if ([model hasItemForItemType:ItemTypeErrorMessage |
| 288 sectionIdentifier:sectionIdentifier]) { |
| 289 NSIndexPath* indexPath = [model indexPathForItemType:ItemTypeErrorMessage |
| 290 sectionIdentifier:sectionIdentifier]; |
| 291 if (errorMessage.length == 0) { |
| 292 // Remove the item at the index path. |
| 293 [model removeItemWithType:ItemTypeErrorMessage |
| 294 fromSectionWithIdentifier:sectionIdentifier]; |
| 295 [self.collectionView deleteItemsAtIndexPaths:@[ indexPath ]]; |
| 296 } else { |
| 297 // Reload the item at the index path. |
| 298 PaymentsTextItem* item = base::mac::ObjCCastStrict<PaymentsTextItem>( |
| 299 [model itemAtIndexPath:indexPath]); |
| 300 item.text = errorMessage; |
| 301 [self.collectionView reloadItemsAtIndexPaths:@[ indexPath ]]; |
| 302 } |
| 303 } else if (errorMessage.length != 0) { |
| 304 // Insert an item at the index path. |
| 305 PaymentsTextItem* errorMessageItem = |
| 306 [[PaymentsTextItem alloc] initWithType:ItemTypeErrorMessage]; |
| 307 errorMessageItem.text = errorMessage; |
| 308 errorMessageItem.image = NativeImage(IDR_IOS_PAYMENTS_WARNING); |
| 309 [model addItem:errorMessageItem toSectionWithIdentifier:sectionIdentifier]; |
| 310 NSIndexPath* indexPath = [model indexPathForItemType:ItemTypeErrorMessage |
| 311 sectionIdentifier:sectionIdentifier]; |
| 312 [self.collectionView insertItemsAtIndexPaths:@[ indexPath ]]; |
| 313 } |
| 314 } |
| 315 |
| 316 - (NSIndexPath*)indexPathWithSectionOffset:(NSInteger)offset |
| 317 fromPath:(NSIndexPath*)indexPath { |
| 318 DCHECK(indexPath); |
| 319 DCHECK(offset); |
| 320 NSInteger nextSection = [indexPath section] + offset; |
| 321 if (nextSection >= 0 && |
| 322 nextSection < [[self collectionView] numberOfSections]) { |
| 323 return [NSIndexPath indexPathForRow:[indexPath row] inSection:nextSection]; |
| 324 } |
| 325 return nil; |
| 326 } |
| 327 |
| 328 - (AutofillEditCell*)nextTextFieldWithOffset:(NSInteger)offset { |
| 329 UICollectionView* collectionView = [self collectionView]; |
| 330 NSIndexPath* currentCellPath = [self indexPathForCurrentTextField]; |
| 331 DCHECK(currentCellPath); |
| 332 NSIndexPath* nextCellPath = |
| 333 [self indexPathWithSectionOffset:offset fromPath:currentCellPath]; |
| 334 if (nextCellPath) { |
| 335 id nextCell = [collectionView cellForItemAtIndexPath:nextCellPath]; |
| 336 if ([nextCell isKindOfClass:[AutofillEditCell class]]) { |
| 337 return base::mac::ObjCCastStrict<AutofillEditCell>( |
| 338 [collectionView cellForItemAtIndexPath:nextCellPath]); |
| 339 } |
| 340 } |
| 341 return nil; |
| 342 } |
| 343 |
| 344 - (void)updateAccessoryViewButtonsStates { |
| 345 AutofillEditCell* previousCell = [self nextTextFieldWithOffset:-1]; |
| 346 [[_accessoryView previousButton] setEnabled:previousCell != nil]; |
| 347 |
| 348 AutofillEditCell* nextCell = [self nextTextFieldWithOffset:1]; |
| 349 [[_accessoryView nextButton] setEnabled:nextCell != nil]; |
| 350 } |
| 351 |
| 352 #pragma mark - Keyboard handling |
| 353 |
| 354 - (void)keyboardDidShow { |
| 355 [self.collectionView scrollRectToVisible:[_currentEditingCell frame] |
| 356 animated:YES]; |
| 357 } |
| 358 |
| 359 @end |
| OLD | NEW |