blob: 8c018d85ce4fb6387fb0e19e9b39ff3a16e4de2b [file] [log] [blame]
/*
* Copyright (C) 2010 Google Inc. All rights reserved.
* Copyright (C) 2016-2018 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:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
* OWNER OR 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.
*/
#include "config.h"
#include "BaseDateAndTimeInputType.h"
#if ENABLE(DATE_AND_TIME_INPUT_TYPES)
#include "BaseClickableWithKeyInputType.h"
#include "Chrome.h"
#include "DateComponents.h"
#include "DateTimeChooserParameters.h"
#include "Decimal.h"
#include "FocusController.h"
#include "FrameView.h"
#include "HTMLDataListElement.h"
#include "HTMLDivElement.h"
#include "HTMLInputElement.h"
#include "HTMLNames.h"
#include "HTMLOptionElement.h"
#include "KeyboardEvent.h"
#include "Page.h"
#include "PlatformLocale.h"
#include "Settings.h"
#include "ShadowPseudoIds.h"
#include "ShadowRoot.h"
#include "StepRange.h"
#include "Text.h"
#include "UserGestureIndicator.h"
#include <limits>
#include <wtf/DateMath.h>
#include <wtf/MathExtras.h>
#include <wtf/text/StringView.h>
namespace WebCore {
using namespace HTMLNames;
static const int msecPerMinute = 60 * 1000;
static const int msecPerSecond = 1000;
void BaseDateAndTimeInputType::DateTimeFormatValidator::visitField(DateTimeFormat::FieldType fieldType, int)
{
switch (fieldType) {
case DateTimeFormat::FieldTypeYear:
m_results.add(DateTimeFormatValidationResults::HasYear);
break;
case DateTimeFormat::FieldTypeMonth:
case DateTimeFormat::FieldTypeMonthStandAlone:
m_results.add(DateTimeFormatValidationResults::HasMonth);
break;
case DateTimeFormat::FieldTypeWeekOfYear:
m_results.add(DateTimeFormatValidationResults::HasWeek);
break;
case DateTimeFormat::FieldTypeDayOfMonth:
m_results.add(DateTimeFormatValidationResults::HasDay);
break;
case DateTimeFormat::FieldTypePeriod:
m_results.add(DateTimeFormatValidationResults::HasMeridiem);
break;
case DateTimeFormat::FieldTypeHour11:
case DateTimeFormat::FieldTypeHour12:
m_results.add(DateTimeFormatValidationResults::HasHour);
break;
case DateTimeFormat::FieldTypeHour23:
case DateTimeFormat::FieldTypeHour24:
m_results.add(DateTimeFormatValidationResults::HasHour);
m_results.add(DateTimeFormatValidationResults::HasMeridiem);
break;
case DateTimeFormat::FieldTypeMinute:
m_results.add(DateTimeFormatValidationResults::HasMinute);
break;
case DateTimeFormat::FieldTypeSecond:
m_results.add(DateTimeFormatValidationResults::HasSecond);
break;
default:
break;
}
}
bool BaseDateAndTimeInputType::DateTimeFormatValidator::validateFormat(const String& format, const BaseDateAndTimeInputType& inputType)
{
if (!DateTimeFormat::parse(format, *this))
return false;
return inputType.isValidFormat(m_results);
}
BaseDateAndTimeInputType::~BaseDateAndTimeInputType()
{
closeDateTimeChooser();
}
double BaseDateAndTimeInputType::valueAsDate() const
{
return valueAsDouble();
}
ExceptionOr<void> BaseDateAndTimeInputType::setValueAsDate(double value) const
{
ASSERT(element());
element()->setValue(serializeWithMilliseconds(value));
return { };
}
double BaseDateAndTimeInputType::valueAsDouble() const
{
ASSERT(element());
const Decimal value = parseToNumber(element()->value(), Decimal::nan());
return value.isFinite() ? value.toDouble() : DateComponents::invalidMilliseconds();
}
ExceptionOr<void> BaseDateAndTimeInputType::setValueAsDecimal(const Decimal& newValue, TextFieldEventBehavior eventBehavior) const
{
ASSERT(element());
element()->setValue(serialize(newValue), eventBehavior);
return { };
}
bool BaseDateAndTimeInputType::typeMismatchFor(const String& value) const
{
return !value.isEmpty() && !parseToDateComponents(value);
}
bool BaseDateAndTimeInputType::typeMismatch() const
{
ASSERT(element());
return typeMismatchFor(element()->value());
}
Decimal BaseDateAndTimeInputType::defaultValueForStepUp() const
{
double ms = WallTime::now().secondsSinceEpoch().milliseconds();
int offset = calculateLocalTimeOffset(ms).offset / msPerMinute;
return Decimal::fromDouble(ms + (offset * msPerMinute));
}
Decimal BaseDateAndTimeInputType::parseToNumber(const String& source, const Decimal& defaultValue) const
{
auto date = parseToDateComponents(source);
if (!date)
return defaultValue;
double msec = date->millisecondsSinceEpoch();
ASSERT(std::isfinite(msec));
return Decimal::fromDouble(msec);
}
String BaseDateAndTimeInputType::serialize(const Decimal& value) const
{
if (!value.isFinite())
return { };
auto date = setMillisecondToDateComponents(value.toDouble());
if (!date)
return { };
return serializeWithComponents(*date);
}
String BaseDateAndTimeInputType::serializeWithComponents(const DateComponents& date) const
{
ASSERT(element());
Decimal step;
if (!element()->getAllowedValueStep(&step) || step.remainder(msecPerMinute).isZero())
return date.toString();
if (step.remainder(msecPerSecond).isZero())
return date.toString(SecondFormat::Second);
return date.toString(SecondFormat::Millisecond);
}
String BaseDateAndTimeInputType::serializeWithMilliseconds(double value) const
{
return serialize(Decimal::fromDouble(value));
}
String BaseDateAndTimeInputType::localizeValue(const String& proposedValue) const
{
auto date = parseToDateComponents(proposedValue);
if (!date)
return proposedValue;
ASSERT(element());
String localized = element()->locale().formatDateTime(*date);
return localized.isEmpty() ? proposedValue : localized;
}
String BaseDateAndTimeInputType::visibleValue() const
{
ASSERT(element());
return localizeValue(element()->value());
}
String BaseDateAndTimeInputType::sanitizeValue(const String& proposedValue) const
{
return typeMismatchFor(proposedValue) ? String() : proposedValue;
}
bool BaseDateAndTimeInputType::supportsReadOnly() const
{
return true;
}
bool BaseDateAndTimeInputType::shouldRespectListAttribute()
{
return InputType::themeSupportsDataListUI(this);
}
bool BaseDateAndTimeInputType::valueMissing(const String& value) const
{
ASSERT(element());
return element()->isRequired() && value.isEmpty();
}
bool BaseDateAndTimeInputType::isKeyboardFocusable(KeyboardEvent*) const
{
ASSERT(element());
return !element()->isReadOnly() && element()->isTextFormControlFocusable();
}
bool BaseDateAndTimeInputType::isMouseFocusable() const
{
ASSERT(element());
return element()->isTextFormControlFocusable();
}
bool BaseDateAndTimeInputType::shouldHaveSecondField(const DateComponents& date) const
{
if (date.second())
return true;
auto stepRange = createStepRange(AnyStepHandling::Default);
return !stepRange.minimum().remainder(msecPerMinute).isZero()
|| !stepRange.step().remainder(msecPerMinute).isZero();
}
bool BaseDateAndTimeInputType::shouldHaveMillisecondField(const DateComponents& date) const
{
if (date.millisecond())
return true;
auto stepRange = createStepRange(AnyStepHandling::Default);
return !stepRange.minimum().remainder(msecPerSecond).isZero()
|| !stepRange.step().remainder(msecPerSecond).isZero();
}
void BaseDateAndTimeInputType::setValue(const String& value, bool valueChanged, TextFieldEventBehavior eventBehavior)
{
InputType::setValue(value, valueChanged, eventBehavior);
if (valueChanged)
updateInnerTextValue();
}
void BaseDateAndTimeInputType::handleDOMActivateEvent(Event&)
{
ASSERT(element());
if (element()->isDisabledOrReadOnly() || !element()->renderer() || !UserGestureIndicator::processingUserGesture())
return;
if (m_dateTimeChooser)
return;
if (!element()->document().page())
return;
DateTimeChooserParameters parameters;
if (!setupDateTimeChooserParameters(parameters))
return;
if (auto chrome = this->chrome()) {
m_dateTimeChooser = chrome->createDateTimeChooser(*this);
if (m_dateTimeChooser)
m_dateTimeChooser->showChooser(parameters);
}
}
void BaseDateAndTimeInputType::createShadowSubtreeAndUpdateInnerTextElementEditability(ContainerNode::ChildChange::Source source, bool)
{
ASSERT(needsShadowSubtree());
ASSERT(element());
auto& element = *this->element();
auto& document = element.document();
if (document.settings().dateTimeInputsEditableComponentsEnabled()) {
m_dateTimeEditElement = DateTimeEditElement::create(document, *this);
element.userAgentShadowRoot()->appendChild(source, *m_dateTimeEditElement);
} else {
auto valueContainer = HTMLDivElement::create(document);
valueContainer->setPseudo(ShadowPseudoIds::webkitDateAndTimeValue());
element.userAgentShadowRoot()->appendChild(source, valueContainer);
}
updateInnerTextValue();
}
void BaseDateAndTimeInputType::destroyShadowSubtree()
{
InputType::destroyShadowSubtree();
m_dateTimeEditElement = nullptr;
}
void BaseDateAndTimeInputType::updateInnerTextValue()
{
ASSERT(element());
if (!m_dateTimeEditElement) {
auto node = element()->userAgentShadowRoot()->firstChild();
if (!is<HTMLElement>(node))
return;
auto displayValue = visibleValue();
if (displayValue.isEmpty()) {
// Need to put something to keep text baseline.
displayValue = " "_s;
}
downcast<HTMLElement>(*node).setInnerText(displayValue);
return;
}
DateTimeEditElement::LayoutParameters layoutParameters(element()->locale());
auto date = parseToDateComponents(element()->value());
if (date)
setupLayoutParameters(layoutParameters, *date);
else {
if (auto dateForLayout = setMillisecondToDateComponents(createStepRange(AnyStepHandling::Default).minimum().toDouble()))
setupLayoutParameters(layoutParameters, *dateForLayout);
else
setupLayoutParameters(layoutParameters, DateComponents());
}
if (!DateTimeFormatValidator().validateFormat(layoutParameters.dateTimeFormat, *this))
layoutParameters.dateTimeFormat = layoutParameters.fallbackDateTimeFormat;
if (date)
m_dateTimeEditElement->setValueAsDate(layoutParameters, *date);
else
m_dateTimeEditElement->setEmptyValue(layoutParameters);
}
bool BaseDateAndTimeInputType::hasCustomFocusLogic() const
{
if (m_dateTimeEditElement)
return false;
return InputType::hasCustomFocusLogic();
}
void BaseDateAndTimeInputType::attributeChanged(const QualifiedName& name)
{
if (name == maxAttr || name == minAttr) {
if (auto* element = this->element())
element->invalidateStyleForSubtree();
} else if (name == valueAttr) {
if (auto* element = this->element()) {
if (!element->hasDirtyValue())
updateInnerTextValue();
}
} else if (name == stepAttr && m_dateTimeEditElement)
updateInnerTextValue();
InputType::attributeChanged(name);
}
void BaseDateAndTimeInputType::elementDidBlur()
{
if (!m_dateTimeEditElement)
closeDateTimeChooser();
}
void BaseDateAndTimeInputType::detach()
{
closeDateTimeChooser();
}
bool BaseDateAndTimeInputType::isPresentingAttachedView() const
{
return !!m_dateTimeChooser;
}
auto BaseDateAndTimeInputType::handleKeydownEvent(KeyboardEvent& event) -> ShouldCallBaseEventHandler
{
ASSERT(element());
return BaseClickableWithKeyInputType::handleKeydownEvent(*element(), event);
}
void BaseDateAndTimeInputType::handleKeypressEvent(KeyboardEvent& event)
{
// The return key should not activate the element, as it conflicts with
// the key binding to submit a form.
if (event.charCode() == '\r')
return;
ASSERT(element());
BaseClickableWithKeyInputType::handleKeypressEvent(*element(), event);
}
void BaseDateAndTimeInputType::handleKeyupEvent(KeyboardEvent& event)
{
BaseClickableWithKeyInputType::handleKeyupEvent(*this, event);
}
void BaseDateAndTimeInputType::handleFocusEvent(Node* oldFocusedNode, FocusDirection direction)
{
if (!m_dateTimeEditElement) {
InputType::handleFocusEvent(oldFocusedNode, direction);
return;
}
// If the element contains editable components, the element itself should not
// be focused. Instead, one of it's children should receive focus.
if (direction == FocusDirection::Backward) {
// If the element received focus when going backwards, advance the focus one more time
// so that this element no longer has focus. In this case, one of the children should
// not be focused as the element is losing focus entirely.
if (auto* page = element()->document().page())
CheckedRef(page->focusController())->advanceFocus(direction, 0);
} else {
// If the element received focus in any other direction, transfer focus to the first focusable child.
m_dateTimeEditElement->focusByOwner();
}
}
bool BaseDateAndTimeInputType::accessKeyAction(bool sendMouseEvents)
{
InputType::accessKeyAction(sendMouseEvents);
ASSERT(element());
return BaseClickableWithKeyInputType::accessKeyAction(*element(), sendMouseEvents);
}
void BaseDateAndTimeInputType::didBlurFromControl()
{
closeDateTimeChooser();
}
void BaseDateAndTimeInputType::didChangeValueFromControl()
{
String value = sanitizeValue(m_dateTimeEditElement->value());
InputType::setValue(value, value != element()->value(), DispatchInputAndChangeEvent);
DateTimeChooserParameters parameters;
if (!setupDateTimeChooserParameters(parameters))
return;
if (m_dateTimeChooser)
m_dateTimeChooser->showChooser(parameters);
}
bool BaseDateAndTimeInputType::isEditControlOwnerDisabled() const
{
ASSERT(element());
return element()->isDisabledFormControl();
}
bool BaseDateAndTimeInputType::isEditControlOwnerReadOnly() const
{
ASSERT(element());
return element()->isReadOnly();
}
AtomString BaseDateAndTimeInputType::localeIdentifier() const
{
ASSERT(element());
return element()->computeInheritedLanguage();
}
void BaseDateAndTimeInputType::didChooseValue(StringView value)
{
ASSERT(element());
element()->setValue(value.toString(), DispatchInputAndChangeEvent);
}
void BaseDateAndTimeInputType::didEndChooser()
{
m_dateTimeChooser = nullptr;
}
bool BaseDateAndTimeInputType::setupDateTimeChooserParameters(DateTimeChooserParameters& parameters)
{
ASSERT(element());
auto& element = *this->element();
auto& document = element.document();
if (!document.view())
return false;
parameters.type = element.type();
parameters.minimum = element.minimum();
parameters.maximum = element.maximum();
parameters.required = element.isRequired();
if (!document.settings().langAttributeAwareFormControlUIEnabled())
parameters.locale = defaultLanguage();
else {
AtomString computedLocale = element.computeInheritedLanguage();
parameters.locale = computedLocale.isEmpty() ? AtomString(defaultLanguage()) : computedLocale;
}
auto stepRange = createStepRange(AnyStepHandling::Reject);
if (stepRange.hasStep()) {
parameters.step = stepRange.step().toDouble();
parameters.stepBase = stepRange.stepBase().toDouble();
} else {
parameters.step = 1.0;
parameters.stepBase = 0;
}
if (RenderElement* renderer = element.renderer())
parameters.anchorRectInRootView = document.view()->contentsToRootView(renderer->absoluteBoundingBoxRect());
else
parameters.anchorRectInRootView = IntRect();
parameters.currentValue = element.value();
auto* computedStyle = element.computedStyle();
parameters.isAnchorElementRTL = computedStyle->direction() == TextDirection::RTL;
parameters.useDarkAppearance = document.useDarkAppearance(computedStyle);
auto date = parseToDateComponents(element.value()).value_or(DateComponents());
parameters.hasSecondField = shouldHaveSecondField(date);
parameters.hasMillisecondField = shouldHaveMillisecondField(date);
#if ENABLE(DATALIST_ELEMENT)
if (auto dataList = element.dataList()) {
for (auto& option : dataList->suggestions()) {
auto label = option.label();
auto value = option.value();
if (!element.isValidValue(value))
continue;
parameters.suggestionValues.append(element.sanitizeValue(value));
parameters.localizedSuggestionValues.append(element.localizeValue(value));
parameters.suggestionLabels.append(value == label ? String() : label);
}
}
#endif
return true;
}
void BaseDateAndTimeInputType::closeDateTimeChooser()
{
if (m_dateTimeChooser)
m_dateTimeChooser->endChooser();
}
} // namespace WebCore
#endif