2bb02ca67eb8eb84d1a0fb77677f70ebb8e7551c
[WebKit-https.git] / Source / WebKit / UIProcess / ios / forms / WKFormInputControl.mm
1 /*
2  * Copyright (C) 2014 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "WKFormInputControl.h"
28
29 #if PLATFORM(IOS_FAMILY)
30
31 #import "UIKitSPI.h"
32 #import "WKContentView.h"
33 #import "WKContentViewInteraction.h"
34 #import "WKFormPopover.h"
35 #import "WebPageProxy.h"
36 #import <UIKit/UIBarButtonItem.h>
37 #import <UIKit/UIDatePicker.h>
38 #import <WebCore/LocalizedStrings.h>
39 #import <wtf/RetainPtr.h>
40
41 using namespace WebKit;
42
43 @interface WKDateTimePopoverViewController : UIViewController {
44     RetainPtr<NSObject<WKFormControl>> _innerControl;
45 }
46 - (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)datePickerMode;
47 - (NSObject<WKFormControl> *)innerControl;
48 @end
49
50 @interface WKDateTimePopover : WKFormRotatingAccessoryPopover<WKFormControl> {
51     RetainPtr<WKDateTimePopoverViewController> _viewController;
52     WKContentView *_view;
53 }
54 - (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode;
55 - (WKDateTimePopoverViewController *)viewController;
56 @end
57
58 @interface WKDateTimePicker : NSObject<WKFormControl> {
59     RetainPtr<UIDatePicker> _datePicker;
60     NSString *_formatString;
61     BOOL _shouldRemoveTimeZoneInformation;
62     BOOL _isTimeInput;
63     WKContentView* _view;
64 }
65 - (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode;
66 - (UIDatePicker *)datePicker;
67
68 @end
69
70 @implementation WKDateTimePicker
71
72 static NSString * const kDateFormatString = @"yyyy-MM-dd"; // "2011-01-27".
73 static NSString * const kMonthFormatString = @"yyyy-MM"; // "2011-01".
74 static NSString * const kTimeFormatString = @"HH:mm"; // "13:45".
75 static const NSTimeInterval kMillisecondsPerSecond = 1000;
76
77 - (UIDatePicker *)datePicker
78 {
79     return _datePicker.get();
80 }
81
82 - (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode
83 {
84     if (!(self = [super init]))
85         return nil;
86
87     _view = view;
88     _shouldRemoveTimeZoneInformation = NO;
89     _isTimeInput = NO;
90     switch (view.assistedNodeInformation.elementType) {
91     case InputType::Date:
92         _formatString = kDateFormatString;
93         break;
94     case InputType::Month:
95         _formatString = kMonthFormatString;
96         break;
97     case InputType::Time:
98         _formatString = kTimeFormatString;
99         _isTimeInput = YES;
100         break;
101     case InputType::DateTimeLocal:
102         _shouldRemoveTimeZoneInformation = YES;
103         break;
104     default:
105         break;
106    }
107
108     CGSize size = currentUserInterfaceIdiomIsPad() ? [UIPickerView defaultSizeForCurrentOrientation] : [UIKeyboard defaultSizeForInterfaceOrientation:[UIApp interfaceOrientation]];
109
110     _datePicker = adoptNS([[UIDatePicker alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)]);
111     _datePicker.get().datePickerMode = mode;
112     _datePicker.get().hidden = NO;
113     
114     if ([self shouldPresentGregorianCalendar:view.assistedNodeInformation])
115         _datePicker.get().calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
116     
117     [_datePicker addTarget:self action:@selector(_dateChangeHandler:) forControlEvents:UIControlEventValueChanged];
118
119     return self;
120 }
121
122 - (NSString *)calendarType
123 {
124     return _datePicker.get().calendar.calendarIdentifier;
125 }
126
127 - (void)dealloc
128 {
129     [_datePicker removeTarget:self action:NULL forControlEvents:UIControlEventValueChanged];
130     [super dealloc];
131 }
132
133 - (BOOL)shouldPresentGregorianCalendar:(const AssistedNodeInformation&)nodeInfo
134 {
135     return nodeInfo.autofillFieldName == WebCore::AutofillFieldName::CcExpMonth
136         || nodeInfo.autofillFieldName == WebCore::AutofillFieldName::CcExp
137         || nodeInfo.autofillFieldName == WebCore::AutofillFieldName::CcExpYear;
138 }
139
140 - (UIView *)controlView
141 {
142     return _datePicker.get();
143 }
144
145 - (NSInteger)_timeZoneOffsetFromGMT:(NSDate *)date
146 {
147     if (!_shouldRemoveTimeZoneInformation)
148         return 0;
149
150     return [_datePicker.get().timeZone secondsFromGMTForDate:date];
151 }
152
153 - (NSString *)_sanitizeInputValueForFormatter:(NSString *)value
154 {
155     // The "time" input type may have seconds and milliseconds information which we
156     // just ignore. For example: "01:56:20.391" is shortened to just "01:56".
157     if (_isTimeInput)
158         return [value substringToIndex:[kTimeFormatString length]];
159
160     return value;
161 }
162
163 - (void)_dateChangedSetAsNumber
164 {
165     NSDate *date = [_datePicker date];
166     [_view page]->setAssistedNodeValueAsNumber(([date timeIntervalSince1970] + [self _timeZoneOffsetFromGMT:date]) * kMillisecondsPerSecond);
167 }
168
169 - (RetainPtr<NSDateFormatter>)dateFormatterForPicker
170 {
171     RetainPtr<NSLocale> englishLocale = adoptNS([[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]);
172     RetainPtr<NSDateFormatter> dateFormatter = adoptNS([[NSDateFormatter alloc] init]);
173     [dateFormatter setTimeZone:_datePicker.get().timeZone];
174     [dateFormatter setDateFormat:_formatString];
175     [dateFormatter setLocale:englishLocale.get()];
176     return dateFormatter;
177 }
178
179 - (void)_dateChangedSetAsString
180 {
181     // Force English locale because that is what HTML5 value parsing expects.
182     RetainPtr<NSDateFormatter> dateFormatter = [self dateFormatterForPicker];
183
184     [_view page]->setAssistedNodeValue([dateFormatter stringFromDate:[_datePicker date]]);
185 }
186
187 - (void)_dateChanged
188 {
189     // Internally, DOMHTMLInputElement setValueAs* each take different values for
190     // different date types. It is sometimes easier to set the date in different ways:
191     //   - use setValueAsString for "date", "month", and "time".
192     //   - use setValueAsNumber for "datetime-local".
193     if (_formatString)
194         [self _dateChangedSetAsString];
195     else
196         [self _dateChangedSetAsNumber];
197 }
198
199 - (void)_dateChangeHandler:(id)sender
200 {
201     [self _dateChanged];
202 }
203
204 - (void)controlBeginEditing
205 {
206     // Set the time zone in case it changed.
207     _datePicker.get().timeZone = [NSTimeZone localTimeZone];
208
209     // Currently no value for the <input>. Start the picker with the current time.
210     // Also, update the actual <input> value.
211     NSString *value = _view.assistedNodeInformation.value;
212     if (_view.assistedNodeInformation.value.isEmpty()) {
213         [_datePicker setDate:[NSDate date]];
214         [self _dateChanged];
215         return;
216     }
217
218     // Convert the string value to a date object for the fields where we have a format string.
219     if (_formatString) {
220         value = [self _sanitizeInputValueForFormatter:value];
221         RetainPtr<NSDateFormatter> dateFormatter = [self dateFormatterForPicker];
222         NSDate *parsedDate = [dateFormatter dateFromString:value];
223         [_datePicker setDate:parsedDate ? parsedDate : [NSDate date]];
224         return;
225     }
226
227     // Convert the number value to a date object for the fields affected by timezones.
228     NSTimeInterval secondsSince1970 = _view.assistedNodeInformation.valueAsNumber / kMillisecondsPerSecond;
229     NSInteger timeZoneOffset = [self _timeZoneOffsetFromGMT:[NSDate dateWithTimeIntervalSince1970:secondsSince1970]];
230     NSTimeInterval adjustedSecondsSince1970 = secondsSince1970 - timeZoneOffset;
231     [_datePicker setDate:[NSDate dateWithTimeIntervalSince1970:adjustedSecondsSince1970]];
232 }
233
234 - (void)controlEndEditing
235 {
236 }
237
238 @end
239
240 // WKFormInputControl
241 @implementation WKFormInputControl {
242     RetainPtr<id<WKFormControl>> _control;
243 }
244
245 - (instancetype)initWithView:(WKContentView *)view
246 {
247     if (!(self = [super init]))
248         return nil;
249
250     UIDatePickerMode mode;
251
252     switch (view.assistedNodeInformation.elementType) {
253     case InputType::Date:
254         mode = UIDatePickerModeDate;
255         break;
256
257     case InputType::DateTimeLocal:
258         mode = UIDatePickerModeDateAndTime;
259         break;
260
261     case InputType::Time:
262         mode = UIDatePickerModeTime;
263         break;
264
265     case InputType::Month:
266         mode = (UIDatePickerMode)UIDatePickerModeYearAndMonth;
267         break;
268
269     default:
270         [self release];
271         return nil;
272     }
273
274     if (currentUserInterfaceIdiomIsPad())
275         _control = adoptNS([[WKDateTimePopover alloc] initWithView:view datePickerMode:mode]);
276     else
277         _control = adoptNS([[WKDateTimePicker alloc] initWithView:view datePickerMode:mode]);
278
279     return self;
280
281 }
282
283 - (void)beginEditing
284 {
285     [_control controlBeginEditing];
286 }
287
288 - (void)endEditing
289 {
290     [_control controlEndEditing];
291 }
292
293 - (UIView *)assistantView
294 {
295     return [_control controlView];
296 }
297
298 @end
299
300 @implementation WKFormInputControl (WKTesting)
301 - (NSString *)dateTimePickerCalendarType
302 {
303     if ([(NSObject *)_control.get() isKindOfClass:WKDateTimePicker.class])
304         return [(WKDateTimePicker *)_control.get() calendarType];
305     return nil;
306 }
307 @end
308
309 @implementation WKDateTimePopoverViewController
310
311 - (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)datePickerMode
312 {
313     if (!(self = [super init]))
314         return nil;
315
316     _innerControl = adoptNS([[WKDateTimePicker alloc] initWithView:view datePickerMode:datePickerMode]);
317
318     return self;
319 }
320
321 - (NSObject<WKFormControl> *)innerControl
322 {
323     return _innerControl.get();
324 }
325
326 - (void)loadView
327 {
328     self.view = [_innerControl controlView];
329 }
330
331 @end
332
333 @implementation WKDateTimePopover
334
335 - (void)clear:(id)sender
336 {
337     [_view page]->setAssistedNodeValue(String());
338 }
339
340 - (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode
341 {
342     if (!(self = [super initWithView:view]))
343         return nil;
344
345     _view = view;
346     _viewController = adoptNS([[WKDateTimePopoverViewController alloc] initWithView:view datePickerMode:mode]);
347     UIDatePicker *datePicker = [(WKDateTimePicker *)_viewController.get().innerControl datePicker];
348     CGFloat popoverWidth = [datePicker _contentWidth];
349     CGFloat popoverHeight = _viewController.get().view.frame.size.height;
350     [_viewController setPreferredContentSize:CGSizeMake(popoverWidth, popoverHeight)];
351     [_viewController setEdgesForExtendedLayout:UIRectEdgeNone];
352     [_viewController setTitle:_view.assistedNodeInformation.title];
353
354     // Always have a navigation controller with a clear button, and a title if the input element has a title.
355     RetainPtr<UINavigationController> navigationController = adoptNS([[UINavigationController alloc] initWithRootViewController:_viewController.get()]);
356     UINavigationItem *navigationItem = navigationController.get().navigationBar.topItem;
357     NSString *clearString = WEB_UI_STRING_KEY("Clear", "Clear Button Date Popover", "Clear button in date input popover");
358     ALLOW_DEPRECATED_DECLARATIONS_BEGIN
359     UIBarButtonItem *clearButton = [[[UIBarButtonItem alloc] initWithTitle:clearString style:UIBarButtonItemStyleBordered target:self action:@selector(clear:)] autorelease];
360     ALLOW_DEPRECATED_DECLARATIONS_END
361     [navigationItem setRightBarButtonItem:clearButton];
362
363     ALLOW_DEPRECATED_DECLARATIONS_BEGIN
364     RetainPtr<UIPopoverController> controller = adoptNS([[UIPopoverController alloc] initWithContentViewController:navigationController.get()]);
365     ALLOW_DEPRECATED_DECLARATIONS_END
366     [self setPopoverController:controller.get()];
367
368     return self;
369 }
370
371 - (WKDateTimePopoverViewController *)viewController
372 {
373     return _viewController.get();
374 }
375
376 - (void)controlBeginEditing
377 {
378     [self presentPopoverAnimated:NO];
379     [_viewController.get().innerControl controlBeginEditing];
380 }
381
382 - (void)controlEndEditing
383 {
384 }
385
386 - (UIView *)controlView
387 {
388     return nil;
389 }
390
391 @end
392
393 #endif // PLATFORM(IOS_FAMILY)