blob: 10f9d0c248255f4012c3b03b14eeb38c209f8924 [file] [log] [blame]
/*
* Copyright (C) 2010 Google 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 "core/html/forms/MultipleFieldsTemporalInputTypeView.h"
#include "core/CSSValueKeywords.h"
#include "core/dom/StyleChangeReason.h"
#include "core/dom/shadow/ShadowRoot.h"
#include "core/events/KeyboardEvent.h"
#include "core/events/ScopedEventQueue.h"
#include "core/html/HTMLDataListElement.h"
#include "core/html/HTMLInputElement.h"
#include "core/html/HTMLOptionElement.h"
#include "core/html/forms/BaseTemporalInputType.h"
#include "core/html/forms/DateTimeFieldsState.h"
#include "core/html/forms/FormController.h"
#include "core/html/shadow/ShadowElementNames.h"
#include "core/layout/LayoutTheme.h"
#include "core/page/FocusController.h"
#include "core/page/Page.h"
#include "core/style/ComputedStyle.h"
#include "platform/DateComponents.h"
#include "platform/RuntimeEnabledFeatures.h"
#include "platform/text/DateTimeFormat.h"
#include "platform/text/PlatformLocale.h"
#include "wtf/DateMath.h"
namespace blink {
class DateTimeFormatValidator : public DateTimeFormat::TokenHandler {
public:
DateTimeFormatValidator()
: m_hasYear(false)
, m_hasMonth(false)
, m_hasWeek(false)
, m_hasDay(false)
, m_hasAMPM(false)
, m_hasHour(false)
, m_hasMinute(false)
, m_hasSecond(false) { }
void visitField(DateTimeFormat::FieldType, int) final;
void visitLiteral(const String&) final { }
bool validateFormat(const String& format, const BaseTemporalInputType&);
private:
bool m_hasYear;
bool m_hasMonth;
bool m_hasWeek;
bool m_hasDay;
bool m_hasAMPM;
bool m_hasHour;
bool m_hasMinute;
bool m_hasSecond;
};
void DateTimeFormatValidator::visitField(DateTimeFormat::FieldType fieldType, int)
{
switch (fieldType) {
case DateTimeFormat::FieldTypeYear:
m_hasYear = true;
break;
case DateTimeFormat::FieldTypeMonth: // Fallthrough.
case DateTimeFormat::FieldTypeMonthStandAlone:
m_hasMonth = true;
break;
case DateTimeFormat::FieldTypeWeekOfYear:
m_hasWeek = true;
break;
case DateTimeFormat::FieldTypeDayOfMonth:
m_hasDay = true;
break;
case DateTimeFormat::FieldTypePeriod:
m_hasAMPM = true;
break;
case DateTimeFormat::FieldTypeHour11: // Fallthrough.
case DateTimeFormat::FieldTypeHour12:
m_hasHour = true;
break;
case DateTimeFormat::FieldTypeHour23: // Fallthrough.
case DateTimeFormat::FieldTypeHour24:
m_hasHour = true;
m_hasAMPM = true;
break;
case DateTimeFormat::FieldTypeMinute:
m_hasMinute = true;
break;
case DateTimeFormat::FieldTypeSecond:
m_hasSecond = true;
break;
default:
break;
}
}
bool DateTimeFormatValidator::validateFormat(const String& format, const BaseTemporalInputType& inputType)
{
if (!DateTimeFormat::parse(format, *this))
return false;
return inputType.isValidFormat(m_hasYear, m_hasMonth, m_hasWeek, m_hasDay, m_hasAMPM, m_hasHour, m_hasMinute, m_hasSecond);
}
DateTimeEditElement* MultipleFieldsTemporalInputTypeView::dateTimeEditElement() const
{
return toDateTimeEditElement(element().userAgentShadowRoot()->getElementById(ShadowElementNames::dateTimeEdit()));
}
SpinButtonElement* MultipleFieldsTemporalInputTypeView::spinButtonElement() const
{
return toSpinButtonElement(element().userAgentShadowRoot()->getElementById(ShadowElementNames::spinButton()));
}
ClearButtonElement* MultipleFieldsTemporalInputTypeView::clearButtonElement() const
{
return toClearButtonElement(element().userAgentShadowRoot()->getElementById(ShadowElementNames::clearButton()));
}
PickerIndicatorElement* MultipleFieldsTemporalInputTypeView::pickerIndicatorElement() const
{
return toPickerIndicatorElement(element().userAgentShadowRoot()->getElementById(ShadowElementNames::pickerIndicator()));
}
inline bool MultipleFieldsTemporalInputTypeView::containsFocusedShadowElement() const
{
return element().userAgentShadowRoot()->contains(element().document().focusedElement());
}
void MultipleFieldsTemporalInputTypeView::didBlurFromControl()
{
// We don't need to call blur(). This function is called when control
// lost focus.
if (containsFocusedShadowElement())
return;
EventQueueScope scope;
// Remove focus ring by CSS "focus" pseudo class.
element().setFocus(false);
if (SpinButtonElement *spinButton = spinButtonElement())
spinButton->releaseCapture();
}
void MultipleFieldsTemporalInputTypeView::didFocusOnControl()
{
// We don't need to call focus(). This function is called when control
// got focus.
if (!containsFocusedShadowElement())
return;
// Add focus ring by CSS "focus" pseudo class.
// FIXME: Setting the focus flag to non-focused element is too tricky.
element().setFocus(true);
}
void MultipleFieldsTemporalInputTypeView::editControlValueChanged()
{
String oldValue = element().value();
String newValue = m_inputType->sanitizeValue(dateTimeEditElement()->value());
// Even if oldValue is null and newValue is "", we should assume they are same.
if ((oldValue.isEmpty() && newValue.isEmpty()) || oldValue == newValue) {
element().setNeedsValidityCheck();
} else {
element().setValueInternal(newValue, DispatchNoEvent);
element().setNeedsStyleRecalc(SubtreeStyleChange, StyleChangeReasonForTracing::create(StyleChangeReason::ControlValue));
element().dispatchFormControlInputEvent();
}
element().notifyFormStateChanged();
element().updateClearButtonVisibility();
}
String MultipleFieldsTemporalInputTypeView::formatDateTimeFieldsState(const DateTimeFieldsState& state) const
{
return m_inputType->formatDateTimeFieldsState(state);
}
bool MultipleFieldsTemporalInputTypeView::hasCustomFocusLogic() const
{
return false;
}
bool MultipleFieldsTemporalInputTypeView::isEditControlOwnerDisabled() const
{
return element().isDisabledFormControl();
}
bool MultipleFieldsTemporalInputTypeView::isEditControlOwnerReadOnly() const
{
return element().isReadOnly();
}
void MultipleFieldsTemporalInputTypeView::focusAndSelectSpinButtonOwner()
{
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->focusIfNoFocus();
}
bool MultipleFieldsTemporalInputTypeView::shouldSpinButtonRespondToMouseEvents()
{
return !element().isDisabledOrReadOnly();
}
bool MultipleFieldsTemporalInputTypeView::shouldSpinButtonRespondToWheelEvents()
{
if (!shouldSpinButtonRespondToMouseEvents())
return false;
if (DateTimeEditElement* edit = dateTimeEditElement())
return edit->hasFocusedField();
return false;
}
void MultipleFieldsTemporalInputTypeView::spinButtonStepDown()
{
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->stepDown();
}
void MultipleFieldsTemporalInputTypeView::spinButtonStepUp()
{
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->stepUp();
}
void MultipleFieldsTemporalInputTypeView::spinButtonDidReleaseMouseCapture(SpinButtonElement::EventDispatch eventDispatch)
{
if (eventDispatch == SpinButtonElement::EventDispatchAllowed)
element().dispatchFormControlChangeEvent();
}
bool MultipleFieldsTemporalInputTypeView::isPickerIndicatorOwnerDisabledOrReadOnly() const
{
return element().isDisabledOrReadOnly();
}
void MultipleFieldsTemporalInputTypeView::pickerIndicatorChooseValue(const String& value)
{
if (element().isValidValue(value)) {
element().setValue(value, DispatchInputAndChangeEvent);
return;
}
DateTimeEditElement* edit = this->dateTimeEditElement();
if (!edit)
return;
EventQueueScope scope;
DateComponents date;
unsigned end;
if (date.parseDate(value, 0, end) && end == value.length())
edit->setOnlyYearMonthDay(date);
element().dispatchFormControlChangeEvent();
}
void MultipleFieldsTemporalInputTypeView::pickerIndicatorChooseValue(double value)
{
DCHECK(std::isfinite(value) || std::isnan(value));
if (std::isnan(value))
element().setValue(emptyString(), DispatchInputAndChangeEvent);
else
element().setValueAsNumber(value, ASSERT_NO_EXCEPTION, DispatchInputAndChangeEvent);
}
Element& MultipleFieldsTemporalInputTypeView::pickerOwnerElement() const
{
return element();
}
bool MultipleFieldsTemporalInputTypeView::setupDateTimeChooserParameters(DateTimeChooserParameters& parameters)
{
return element().setupDateTimeChooserParameters(parameters);
}
MultipleFieldsTemporalInputTypeView::MultipleFieldsTemporalInputTypeView(HTMLInputElement& element, BaseTemporalInputType& inputType)
: InputTypeView(element)
, m_inputType(inputType)
, m_isDestroyingShadowSubtree(false)
, m_pickerIndicatorIsVisible(false)
, m_pickerIndicatorIsAlwaysVisible(false)
{
}
MultipleFieldsTemporalInputTypeView* MultipleFieldsTemporalInputTypeView::create(HTMLInputElement& element, BaseTemporalInputType& inputType)
{
return new MultipleFieldsTemporalInputTypeView(element, inputType);
}
MultipleFieldsTemporalInputTypeView::~MultipleFieldsTemporalInputTypeView()
{
}
DEFINE_TRACE(MultipleFieldsTemporalInputTypeView)
{
visitor->trace(m_inputType);
InputTypeView::trace(visitor);
}
void MultipleFieldsTemporalInputTypeView::blur()
{
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->blurByOwner();
}
PassRefPtr<ComputedStyle> MultipleFieldsTemporalInputTypeView::customStyleForLayoutObject(PassRefPtr<ComputedStyle> originalStyle)
{
EDisplay originalDisplay = originalStyle->display();
EDisplay newDisplay = originalDisplay;
if (originalDisplay == EDisplay::Inline || originalDisplay == EDisplay::InlineBlock)
newDisplay = EDisplay::InlineFlex;
else if (originalDisplay == EDisplay::Block)
newDisplay = EDisplay::Flex;
TextDirection contentDirection = computedTextDirection();
if (originalStyle->direction() == contentDirection && originalDisplay == newDisplay)
return originalStyle;
RefPtr<ComputedStyle> style = ComputedStyle::clone(*originalStyle);
style->setDirection(contentDirection);
style->setDisplay(newDisplay);
style->setUnique();
return style.release();
}
void MultipleFieldsTemporalInputTypeView::createShadowSubtree()
{
DCHECK(element().shadow());
// Element must not have a layoutObject here, because if it did
// DateTimeEditElement::customStyleForLayoutObject() is called in appendChild()
// before the field wrapper element is created.
// FIXME: This code should not depend on such craziness.
DCHECK(!element().layoutObject());
Document& document = element().document();
ContainerNode* container = element().userAgentShadowRoot();
container->appendChild(DateTimeEditElement::create(document, *this));
element().updateView();
container->appendChild(ClearButtonElement::create(document, *this));
container->appendChild(SpinButtonElement::create(document, *this));
if (LayoutTheme::theme().supportsCalendarPicker(m_inputType->formControlType()))
m_pickerIndicatorIsAlwaysVisible = true;
container->appendChild(PickerIndicatorElement::create(document, *this));
m_pickerIndicatorIsVisible = true;
updatePickerIndicatorVisibility();
}
void MultipleFieldsTemporalInputTypeView::destroyShadowSubtree()
{
DCHECK(!m_isDestroyingShadowSubtree);
m_isDestroyingShadowSubtree = true;
if (SpinButtonElement* element = spinButtonElement())
element->removeSpinButtonOwner();
if (ClearButtonElement* element = clearButtonElement())
element->removeClearButtonOwner();
if (DateTimeEditElement* element = dateTimeEditElement())
element->removeEditControlOwner();
if (PickerIndicatorElement* element = pickerIndicatorElement())
element->removePickerIndicatorOwner();
// If a field element has focus, set focus back to the <input> itself before
// deleting the field. This prevents unnecessary focusout/blur events.
if (containsFocusedShadowElement())
element().focus();
InputTypeView::destroyShadowSubtree();
m_isDestroyingShadowSubtree = false;
}
void MultipleFieldsTemporalInputTypeView::handleFocusInEvent(Element* oldFocusedElement, WebFocusType type)
{
DateTimeEditElement* edit = dateTimeEditElement();
if (!edit || m_isDestroyingShadowSubtree)
return;
if (type == WebFocusTypeBackward) {
if (element().document().page())
element().document().page()->focusController().advanceFocus(type);
} else if (type == WebFocusTypeNone || type == WebFocusTypeMouse || type == WebFocusTypePage) {
edit->focusByOwner(oldFocusedElement);
} else {
edit->focusByOwner();
}
}
void MultipleFieldsTemporalInputTypeView::forwardEvent(Event* event)
{
if (SpinButtonElement* element = spinButtonElement()) {
element->forwardEvent(event);
if (event->defaultHandled())
return;
}
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->defaultEventHandler(event);
}
void MultipleFieldsTemporalInputTypeView::disabledAttributeChanged()
{
EventQueueScope scope;
spinButtonElement()->releaseCapture();
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->disabledStateChanged();
}
void MultipleFieldsTemporalInputTypeView::requiredAttributeChanged()
{
updateClearButtonVisibility();
}
void MultipleFieldsTemporalInputTypeView::handleKeydownEvent(KeyboardEvent* event)
{
if (!element().focused())
return;
if (m_pickerIndicatorIsVisible
&& ((event->key() == "ArrowDown" && event->getModifierState("Alt")) || (LayoutTheme::theme().shouldOpenPickerWithF4Key() && event->key() == "F4"))) {
if (PickerIndicatorElement* element = pickerIndicatorElement())
element->openPopup();
event->setDefaultHandled();
} else {
forwardEvent(event);
}
}
bool MultipleFieldsTemporalInputTypeView::hasBadInput() const
{
DateTimeEditElement* edit = dateTimeEditElement();
return element().value().isEmpty() && edit && edit->anyEditableFieldsHaveValues();
}
AtomicString MultipleFieldsTemporalInputTypeView::localeIdentifier() const
{
return element().computeInheritedLanguage();
}
void MultipleFieldsTemporalInputTypeView::editControlDidChangeValueByKeyboard()
{
element().dispatchFormControlChangeEvent();
}
void MultipleFieldsTemporalInputTypeView::minOrMaxAttributeChanged()
{
updateView();
}
void MultipleFieldsTemporalInputTypeView::readonlyAttributeChanged()
{
EventQueueScope scope;
spinButtonElement()->releaseCapture();
if (DateTimeEditElement* edit = dateTimeEditElement())
edit->readOnlyStateChanged();
}
void MultipleFieldsTemporalInputTypeView::restoreFormControlState(const FormControlState& state)
{
DateTimeEditElement* edit = dateTimeEditElement();
if (!edit)
return;
DateTimeFieldsState dateTimeFieldsState = DateTimeFieldsState::restoreFormControlState(state);
edit->setValueAsDateTimeFieldsState(dateTimeFieldsState);
element().setValueInternal(m_inputType->sanitizeValue(edit->value()), DispatchNoEvent);
updateClearButtonVisibility();
}
FormControlState MultipleFieldsTemporalInputTypeView::saveFormControlState() const
{
if (DateTimeEditElement* edit = dateTimeEditElement())
return edit->valueAsDateTimeFieldsState().saveFormControlState();
return FormControlState();
}
void MultipleFieldsTemporalInputTypeView::didSetValue(const String& sanitizedValue, bool valueChanged)
{
DateTimeEditElement* edit = dateTimeEditElement();
if (valueChanged || (sanitizedValue.isEmpty() && edit && edit->anyEditableFieldsHaveValues())) {
element().updateView();
element().setNeedsValidityCheck();
}
}
void MultipleFieldsTemporalInputTypeView::stepAttributeChanged()
{
updateView();
}
void MultipleFieldsTemporalInputTypeView::updateView()
{
DateTimeEditElement* edit = dateTimeEditElement();
if (!edit)
return;
DateTimeEditElement::LayoutParameters layoutParameters(element().locale(), m_inputType->createStepRange(AnyIsDefaultStep));
DateComponents date;
bool hasValue = false;
if (!element().suggestedValue().isNull())
hasValue = m_inputType->parseToDateComponents(element().suggestedValue(), &date);
else
hasValue = m_inputType->parseToDateComponents(element().value(), &date);
if (!hasValue)
m_inputType->setMillisecondToDateComponents(layoutParameters.stepRange.minimum().toDouble(), &date);
m_inputType->setupLayoutParameters(layoutParameters, date);
DEFINE_STATIC_LOCAL(AtomicString, datetimeformatAttr, ("datetimeformat"));
edit->setAttribute(datetimeformatAttr, AtomicString(layoutParameters.dateTimeFormat), ASSERT_NO_EXCEPTION);
const AtomicString pattern = edit->fastGetAttribute(HTMLNames::patternAttr);
if (!pattern.isEmpty())
layoutParameters.dateTimeFormat = pattern;
if (!DateTimeFormatValidator().validateFormat(layoutParameters.dateTimeFormat, *m_inputType))
layoutParameters.dateTimeFormat = layoutParameters.fallbackDateTimeFormat;
if (hasValue)
edit->setValueAsDate(layoutParameters, date);
else
edit->setEmptyValue(layoutParameters, date);
updateClearButtonVisibility();
}
void MultipleFieldsTemporalInputTypeView::closePopupView()
{
if (PickerIndicatorElement* picker = pickerIndicatorElement())
picker->closePopup();
}
void MultipleFieldsTemporalInputTypeView::valueAttributeChanged()
{
if (!element().hasDirtyValue())
updateView();
}
void MultipleFieldsTemporalInputTypeView::listAttributeTargetChanged()
{
updatePickerIndicatorVisibility();
}
void MultipleFieldsTemporalInputTypeView::updatePickerIndicatorVisibility()
{
if (m_pickerIndicatorIsAlwaysVisible) {
showPickerIndicator();
return;
}
if (element().hasValidDataListOptions())
showPickerIndicator();
else
hidePickerIndicator();
}
void MultipleFieldsTemporalInputTypeView::hidePickerIndicator()
{
if (!m_pickerIndicatorIsVisible)
return;
m_pickerIndicatorIsVisible = false;
DCHECK(pickerIndicatorElement());
pickerIndicatorElement()->setInlineStyleProperty(CSSPropertyDisplay, CSSValueNone);
}
void MultipleFieldsTemporalInputTypeView::showPickerIndicator()
{
if (m_pickerIndicatorIsVisible)
return;
m_pickerIndicatorIsVisible = true;
DCHECK(pickerIndicatorElement());
pickerIndicatorElement()->removeInlineStyleProperty(CSSPropertyDisplay);
}
void MultipleFieldsTemporalInputTypeView::focusAndSelectClearButtonOwner()
{
element().focus();
}
bool MultipleFieldsTemporalInputTypeView::shouldClearButtonRespondToMouseEvents()
{
return !element().isDisabledOrReadOnly() && !element().isRequired();
}
void MultipleFieldsTemporalInputTypeView::clearValue()
{
element().setValue("", DispatchInputAndChangeEvent);
element().updateClearButtonVisibility();
}
void MultipleFieldsTemporalInputTypeView::updateClearButtonVisibility()
{
ClearButtonElement* clearButton = clearButtonElement();
if (!clearButton)
return;
if (element().isRequired() || !dateTimeEditElement()->anyEditableFieldsHaveValues()) {
clearButton->setInlineStyleProperty(CSSPropertyOpacity, 0.0, CSSPrimitiveValue::UnitType::Number);
clearButton->setInlineStyleProperty(CSSPropertyPointerEvents, CSSValueNone);
} else {
clearButton->removeInlineStyleProperty(CSSPropertyOpacity);
clearButton->removeInlineStyleProperty(CSSPropertyPointerEvents);
}
}
TextDirection MultipleFieldsTemporalInputTypeView::computedTextDirection()
{
return element().locale().isRTL() ? RTL : LTR;
}
AXObject* MultipleFieldsTemporalInputTypeView::popupRootAXObject()
{
if (PickerIndicatorElement* picker = pickerIndicatorElement())
return picker->popupRootAXObject();
return nullptr;
}
} // namespace blink