/* * 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 "ShadowRoot.h" #include "StepRange.h" #include "Text.h" #include "UserGestureIndicator.h" #include #include #include #include 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 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 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 { static MainThreadNeverDestroyed valueContainerPseudo("-webkit-date-and-time-value", AtomString::ConstructFromLiteral); auto valueContainer = HTMLDivElement::create(document); valueContainer->setPseudo(valueContainerPseudo); 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(node)) return; auto displayValue = visibleValue(); if (displayValue.isEmpty()) { // Need to put something to keep text baseline. displayValue = " "_s; } downcast(*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()) 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