WKFormInputControl.mm   [plain text]

/*
* Copyright (C) 2014 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/

#import "config.h"
#import "WKFormInputControl.h"

#if PLATFORM(IOS_FAMILY)

#import "UIKitSPI.h"
#import "WKContentView.h"
#import "WKContentViewInteraction.h"
#import "WKFormPopover.h"
#import "WebPageProxy.h"
#import <UIKit/UIBarButtonItem.h>
#import <UIKit/UIDatePicker.h>
#import <WebCore/LocalizedStrings.h>
#import <wtf/RetainPtr.h>

using namespace WebKit;

@interface WKDateTimePopoverViewController : UIViewController {
RetainPtr<NSObject<WKFormControl>> _innerControl;
}
- (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)datePickerMode;
- (NSObject<WKFormControl> *)innerControl;
@end

@interface WKDateTimePopover : WKFormRotatingAccessoryPopover<WKFormControl> {
RetainPtr<WKDateTimePopoverViewController> _viewController;
WKContentView *_view;
}
- (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode;
- (WKDateTimePopoverViewController *)viewController;
@property (nonatomic, readonly) NSString *calendarType;
@end

@interface WKDateTimePicker : NSObject<WKFormControl> {
RetainPtr<UIDatePicker> _datePicker;
NSString *_formatString;
BOOL _shouldRemoveTimeZoneInformation;
BOOL _isTimeInput;
WKContentView* _view;
}
- (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode;
- (UIDatePicker *)datePicker;

@end

@implementation WKDateTimePicker

static NSString * const kDateFormatString = @"yyyy-MM-dd"; // "2011-01-27".
static NSString * const kMonthFormatString = @"yyyy-MM"; // "2011-01".
static NSString * const kTimeFormatString = @"HH:mm"; // "13:45".
static const NSTimeInterval kMillisecondsPerSecond = 1000;

- (UIDatePicker *)datePicker
{
return _datePicker.get();
}

- (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode
{
if (!(self = [super init]))
return nil;

_view = view;
_shouldRemoveTimeZoneInformation = NO;
_isTimeInput = NO;
switch (view.focusedElementInformation.elementType) {
case InputType::Date:
_formatString = kDateFormatString;
break;
case InputType::Month:
_formatString = kMonthFormatString;
break;
case InputType::Time:
_formatString = kTimeFormatString;
_isTimeInput = YES;
break;
case InputType::DateTimeLocal:
_shouldRemoveTimeZoneInformation = YES;
break;
default:
break;
}

auto size = currentUserInterfaceIdiomIsPad() ? [UIPickerView defaultSizeForCurrentOrientation] : [UIKeyboard defaultSizeForInterfaceOrientation:view.interfaceOrientation];

_datePicker = adoptNS([[UIDatePicker alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)]);
_datePicker.get().datePickerMode = mode;
_datePicker.get().hidden = NO;

if ([self shouldPresentGregorianCalendar:view.focusedElementInformation])
_datePicker.get().calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];

[_datePicker addTarget:self action:@selector(_dateChangeHandler:) forControlEvents:UIControlEventValueChanged];

return self;
}

- (NSString *)calendarType
{
return _datePicker.get().calendar.calendarIdentifier;
}

- (void)dealloc
{
[_datePicker removeTarget:self action:NULL forControlEvents:UIControlEventValueChanged];
[super dealloc];
}

- (BOOL)shouldPresentGregorianCalendar:(const FocusedElementInformation&)nodeInfo
{
return nodeInfo.autofillFieldName == WebCore::AutofillFieldName::CcExpMonth
|| nodeInfo.autofillFieldName == WebCore::AutofillFieldName::CcExp
|| nodeInfo.autofillFieldName == WebCore::AutofillFieldName::CcExpYear;
}

- (UIView *)controlView
{
return _datePicker.get();
}

- (NSInteger)_timeZoneOffsetFromGMT:(NSDate *)date
{
if (!_shouldRemoveTimeZoneInformation)
return 0;

return [_datePicker.get().timeZone secondsFromGMTForDate:date];
}

- (NSString *)_sanitizeInputValueForFormatter:(NSString *)value
{
// The "time" input type may have seconds and milliseconds information which we
// just ignore. For example: "01:56:20.391" is shortened to just "01:56".
if (_isTimeInput)
return [value substringToIndex:[kTimeFormatString length]];

return value;
}

- (void)_dateChangedSetAsNumber
{
NSDate *date = [_datePicker date];
[_view page]->setFocusedElementValueAsNumber(([date timeIntervalSince1970] + [self _timeZoneOffsetFromGMT:date]) * kMillisecondsPerSecond);
}

- (RetainPtr<NSDateFormatter>)dateFormatterForPicker
{
RetainPtr<NSLocale> englishLocale = adoptNS([[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]);
RetainPtr<NSDateFormatter> dateFormatter = adoptNS([[NSDateFormatter alloc] init]);
[dateFormatter setTimeZone:_datePicker.get().timeZone];
[dateFormatter setDateFormat:_formatString];
[dateFormatter setLocale:englishLocale.get()];
return dateFormatter;
}

- (void)_dateChangedSetAsString
{
// Force English locale because that is what HTML5 value parsing expects.
RetainPtr<NSDateFormatter> dateFormatter = [self dateFormatterForPicker];

[_view page]->setFocusedElementValue([dateFormatter stringFromDate:[_datePicker date]]);
}

- (void)_dateChanged
{
// Internally, DOMHTMLInputElement setValueAs* each take different values for
// different date types. It is sometimes easier to set the date in different ways:
// - use setValueAsString for "date", "month", and "time".
// - use setValueAsNumber for "datetime-local".
if (_formatString)
[self _dateChangedSetAsString];
else
[self _dateChangedSetAsNumber];
}

- (void)_dateChangeHandler:(id)sender
{
[self _dateChanged];
}

- (void)controlBeginEditing
{
// Set the time zone in case it changed.
_datePicker.get().timeZone = [NSTimeZone localTimeZone];

// Currently no value for the <input>. Start the picker with the current time.
// Also, update the actual <input> value.
NSString *value = _view.focusedElementInformation.value;
if (_view.focusedElementInformation.value.isEmpty()) {
[_datePicker setDate:[NSDate date]];
[self _dateChanged];
return;
}

// Convert the string value to a date object for the fields where we have a format string.
if (_formatString) {
value = [self _sanitizeInputValueForFormatter:value];
RetainPtr<NSDateFormatter> dateFormatter = [self dateFormatterForPicker];
NSDate *parsedDate = [dateFormatter dateFromString:value];
[_datePicker setDate:parsedDate ? parsedDate : [NSDate date]];
return;
}

// Convert the number value to a date object for the fields affected by timezones.
NSTimeInterval secondsSince1970 = _view.focusedElementInformation.valueAsNumber / kMillisecondsPerSecond;
NSInteger timeZoneOffset = [self _timeZoneOffsetFromGMT:[NSDate dateWithTimeIntervalSince1970:secondsSince1970]];
NSTimeInterval adjustedSecondsSince1970 = secondsSince1970 - timeZoneOffset;
[_datePicker setDate:[NSDate dateWithTimeIntervalSince1970:adjustedSecondsSince1970]];
}

- (void)controlEndEditing
{
}

@end

// WKFormInputControl
@implementation WKFormInputControl

- (instancetype)initWithView:(WKContentView *)view
{
UIDatePickerMode mode;

switch (view.focusedElementInformation.elementType) {
case InputType::Date:
mode = UIDatePickerModeDate;
break;
case InputType::DateTimeLocal:
mode = UIDatePickerModeDateAndTime;
break;
case InputType::Time:
mode = UIDatePickerModeTime;
break;
case InputType::Month:
mode = (UIDatePickerMode)UIDatePickerModeYearAndMonth;
break;
default:
[self release];
return nil;
}

RetainPtr<NSObject <WKFormControl>> control;
if (currentUserInterfaceIdiomIsPad())
control = adoptNS([[WKDateTimePopover alloc] initWithView:view datePickerMode:mode]);
else
control = adoptNS([[WKDateTimePicker alloc] initWithView:view datePickerMode:mode]);
return [super initWithView:view control:WTFMove(control)];
}

@end

@implementation WKFormInputControl (WKTesting)

- (NSString *)dateTimePickerCalendarType
{
if ([self.control isKindOfClass:WKDateTimePicker.class])
return [(WKDateTimePicker *)self.control calendarType];
if ([self.control isKindOfClass:WKDateTimePopover.class])
return [(WKDateTimePopover *)self.control calendarType];
return nil;
}

@end

@implementation WKDateTimePopoverViewController

- (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)datePickerMode
{
if (!(self = [super init]))
return nil;

_innerControl = adoptNS([[WKDateTimePicker alloc] initWithView:view datePickerMode:datePickerMode]);

return self;
}

- (NSObject<WKFormControl> *)innerControl
{
return _innerControl.get();
}

- (void)loadView
{
self.view = [_innerControl controlView];
}

@end

@implementation WKDateTimePopover

- (void)clear:(id)sender
{
[_view page]->setFocusedElementValue(String());
}

- (id)initWithView:(WKContentView *)view datePickerMode:(UIDatePickerMode)mode
{
if (!(self = [super initWithView:view]))
return nil;

_view = view;
_viewController = adoptNS([[WKDateTimePopoverViewController alloc] initWithView:view datePickerMode:mode]);
UIDatePicker *datePicker = [(WKDateTimePicker *)_viewController.get().innerControl datePicker];
CGFloat popoverWidth = [datePicker _contentWidth];
CGFloat popoverHeight = _viewController.get().view.frame.size.height;
[_viewController setPreferredContentSize:CGSizeMake(popoverWidth, popoverHeight)];
[_viewController setEdgesForExtendedLayout:UIRectEdgeNone];
[_viewController setTitle:_view.focusedElementInformation.title];

// Always have a navigation controller with a clear button, and a title if the input element has a title.
RetainPtr<UINavigationController> navigationController = adoptNS([[UINavigationController alloc] initWithRootViewController:_viewController.get()]);
UINavigationItem *navigationItem = navigationController.get().navigationBar.topItem;
NSString *clearString = WEB_UI_STRING_KEY("Clear", "Clear Button Date Popover", "Clear button in date input popover");
ALLOW_DEPRECATED_DECLARATIONS_BEGIN
UIBarButtonItem *clearButton = [[[UIBarButtonItem alloc] initWithTitle:clearString style:UIBarButtonItemStyleBordered target:self action:@selector(clear:)] autorelease];
ALLOW_DEPRECATED_DECLARATIONS_END
[navigationItem setRightBarButtonItem:clearButton];

ALLOW_DEPRECATED_DECLARATIONS_BEGIN
RetainPtr<UIPopoverController> controller = adoptNS([[UIPopoverController alloc] initWithContentViewController:navigationController.get()]);
ALLOW_DEPRECATED_DECLARATIONS_END
[self setPopoverController:controller.get()];

return self;
}

- (WKDateTimePopoverViewController *)viewController
{
return _viewController.get();
}

- (void)controlBeginEditing
{
[self presentPopoverAnimated:NO];
[_viewController.get().innerControl controlBeginEditing];
}

- (void)controlEndEditing
{
[self dismissPopoverAnimated:NO];
[_viewController.get().innerControl controlEndEditing];
}

- (UIView *)controlView
{
return nil;
}

- (NSString *)calendarType
{
WKDateTimePicker *dateTimePicker = (WKDateTimePicker *)self.viewController.innerControl;
return dateTimePicker.datePicker.calendar.calendarIdentifier;
}

@end

#endif // PLATFORM(IOS_FAMILY)