blob: 4e27676cda81b50e35a598110ee70aefeaf15ace [file] [log] [blame]
/*
* Copyright (C) 2012 Google Inc. All rights reserved.
* Copyright (C) 2020-2022 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.
*/
#include "config.h"
#include "DateTimeEditElement.h"
#if ENABLE(DATE_AND_TIME_INPUT_TYPES)
#include "DateComponents.h"
#include "DateTimeFieldElements.h"
#include "DateTimeFieldsState.h"
#include "DateTimeFormat.h"
#include "DateTimeSymbolicFieldElement.h"
#include "Document.h"
#include "Event.h"
#include "HTMLNames.h"
#include "PlatformLocale.h"
#include "ShadowPseudoIds.h"
#include "Text.h"
#include <wtf/IsoMallocInlines.h>
#include <wtf/text/StringBuilder.h>
namespace WebCore {
using namespace HTMLNames;
WTF_MAKE_ISO_ALLOCATED_IMPL(DateTimeEditElement);
class DateTimeEditBuilder final : private DateTimeFormat::TokenHandler {
WTF_MAKE_NONCOPYABLE(DateTimeEditBuilder);
public:
DateTimeEditBuilder(DateTimeEditElement&, const DateTimeEditElement::LayoutParameters&);
bool build(const String&);
private:
// DateTimeFormat::TokenHandler functions:
void visitField(DateTimeFormat::FieldType, int);
void visitLiteral(String&&);
DateTimeEditElement& m_editElement;
const DateTimeEditElement::LayoutParameters& m_parameters;
};
DateTimeEditBuilder::DateTimeEditBuilder(DateTimeEditElement& element, const DateTimeEditElement::LayoutParameters& layoutParameters)
: m_editElement(element)
, m_parameters(layoutParameters)
{
}
bool DateTimeEditBuilder::build(const String& formatString)
{
m_editElement.resetFields();
return DateTimeFormat::parse(formatString, *this);
}
void DateTimeEditBuilder::visitField(DateTimeFormat::FieldType fieldType, int count)
{
Document& document = m_editElement.document();
switch (fieldType) {
case DateTimeFormat::FieldTypeDayOfMonth: {
m_editElement.addField(DateTimeDayFieldElement::create(document, m_editElement));
return;
}
case DateTimeFormat::FieldTypeFractionalSecond: {
m_editElement.addField(DateTimeMillisecondFieldElement::create(document, m_editElement));
return;
}
case DateTimeFormat::FieldTypeHour11: {
m_editElement.addField(DateTimeHourFieldElement::create(document, m_editElement, 0, 11));
return;
}
case DateTimeFormat::FieldTypeHour12: {
m_editElement.addField(DateTimeHourFieldElement::create(document, m_editElement, 1, 12));
return;
}
case DateTimeFormat::FieldTypeHour23: {
m_editElement.addField(DateTimeHourFieldElement::create(document, m_editElement, 0, 23));
return;
}
case DateTimeFormat::FieldTypeHour24: {
m_editElement.addField(DateTimeHourFieldElement::create(document, m_editElement, 1, 24));
return;
}
case DateTimeFormat::FieldTypeMinute: {
m_editElement.addField(DateTimeMinuteFieldElement::create(document, m_editElement));
return;
}
case DateTimeFormat::FieldTypeMonth:
case DateTimeFormat::FieldTypeMonthStandAlone: {
constexpr int countForAbbreviatedMonth = 3;
constexpr int countForFullMonth = 4;
constexpr int countForNarrowMonth = 5;
switch (count) {
case countForNarrowMonth:
case countForAbbreviatedMonth: {
auto field = DateTimeSymbolicMonthFieldElement::create(document, m_editElement, fieldType == DateTimeFormat::FieldTypeMonth ? m_parameters.locale.shortMonthLabels() : m_parameters.locale.shortStandAloneMonthLabels());
m_editElement.addField(field);
return;
}
case countForFullMonth: {
auto field = DateTimeSymbolicMonthFieldElement::create(document, m_editElement, fieldType == DateTimeFormat::FieldTypeMonth ? m_parameters.locale.monthLabels() : m_parameters.locale.standAloneMonthLabels());
m_editElement.addField(field);
return;
}
default:
m_editElement.addField(DateTimeMonthFieldElement::create(document, m_editElement));
return;
}
}
case DateTimeFormat::FieldTypePeriod: {
m_editElement.addField(DateTimeMeridiemFieldElement::create(document, m_editElement, m_parameters.locale.timeAMPMLabels()));
return;
}
case DateTimeFormat::FieldTypeSecond: {
m_editElement.addField(DateTimeSecondFieldElement::create(document, m_editElement));
if (m_parameters.shouldHaveMillisecondField) {
visitLiteral(m_parameters.locale.localizedDecimalSeparator());
visitField(DateTimeFormat::FieldTypeFractionalSecond, 3);
}
return;
}
case DateTimeFormat::FieldTypeYear: {
m_editElement.addField(DateTimeYearFieldElement::create(document, m_editElement));
return;
}
default:
return;
}
}
void DateTimeEditBuilder::visitLiteral(String&& text)
{
ASSERT(text.length());
auto element = HTMLDivElement::create(m_editElement.document());
element->setPseudo(ShadowPseudoIds::webkitDatetimeEditText());
// If the literal begins/ends with a space, the gap between two fields will appear
// exaggerated due to the presence of a 1px padding around each field. This can
// make spaces appear up to 2px larger between fields. This padding is necessary to
// prevent selected fields from appearing squished. To fix, pull fields closer
// together by applying a negative margin.
if (text.startsWith(' '))
element->setInlineStyleProperty(CSSPropertyMarginInlineStart, -1, CSSUnitType::CSS_PX);
if (text.endsWith(' '))
element->setInlineStyleProperty(CSSPropertyMarginInlineEnd, -1, CSSUnitType::CSS_PX);
element->appendChild(Text::create(m_editElement.document(), WTFMove(text)));
m_editElement.fieldsWrapperElement().appendChild(element);
}
DateTimeEditElement::EditControlOwner::~EditControlOwner() = default;
DateTimeEditElement::DateTimeEditElement(Document& document, EditControlOwner& editControlOwner)
: HTMLDivElement(divTag, document)
, m_editControlOwner(editControlOwner)
{
m_placeholderDate.setToCurrentLocalTime();
}
DateTimeEditElement::~DateTimeEditElement() = default;
inline Element& DateTimeEditElement::fieldsWrapperElement() const
{
ASSERT(firstChild());
return downcast<Element>(*firstChild());
}
void DateTimeEditElement::addField(Ref<DateTimeFieldElement> field)
{
if (m_fields.size() == m_fields.capacity())
return;
m_fields.append(field);
fieldsWrapperElement().appendChild(field);
}
size_t DateTimeEditElement::fieldIndexOf(const DateTimeFieldElement& fieldToFind) const
{
return m_fields.findIf([&] (auto& field) {
return field.ptr() == &fieldToFind;
});
}
DateTimeFieldElement* DateTimeEditElement::focusedFieldElement() const
{
auto* focusedElement = document().focusedElement();
auto fieldIndex = m_fields.findIf([&] (auto& field) {
return field.ptr() == focusedElement;
});
if (fieldIndex == notFound)
return nullptr;
return m_fields[fieldIndex].ptr();
}
Ref<DateTimeEditElement> DateTimeEditElement::create(Document& document, EditControlOwner& editControlOwner)
{
auto element = adoptRef(*new DateTimeEditElement(document, editControlOwner));
element->setPseudo(ShadowPseudoIds::webkitDatetimeEdit());
return element;
}
void DateTimeEditElement::layout(const LayoutParameters& layoutParameters)
{
if (!firstChild()) {
auto element = HTMLDivElement::create(document());
element->setPseudo(ShadowPseudoIds::webkitDatetimeEditFieldsWrapper());
appendChild(element);
}
Element& fieldsWrapper = fieldsWrapperElement();
auto* focusedField = focusedFieldElement();
DateTimeEditBuilder builder(*this, layoutParameters);
Node* lastChildToBeRemoved = fieldsWrapper.lastChild();
if (!builder.build(layoutParameters.dateTimeFormat) || m_fields.isEmpty()) {
lastChildToBeRemoved = fieldsWrapper.lastChild();
builder.build(layoutParameters.fallbackDateTimeFormat);
}
if (focusedField) {
auto& focusedFieldId = focusedField->shadowPseudoId();
auto foundFieldToFocus = false;
for (auto& field : m_fields) {
if (field->shadowPseudoId() == focusedFieldId) {
foundFieldToFocus = true;
field->focus();
break;
}
}
if (!foundFieldToFocus)
focusOnNextFocusableField(0);
}
if (lastChildToBeRemoved) {
while (auto* childNode = fieldsWrapper.firstChild()) {
fieldsWrapper.removeChild(*childNode);
if (childNode == lastChildToBeRemoved)
break;
}
}
}
void DateTimeEditElement::didBlurFromField(Event& event)
{
if (!m_editControlOwner)
return;
if (auto* newFocusedElement = event.relatedTarget()) {
bool didFocusSiblingField = notFound != m_fields.findIf([&] (auto& field) {
return field.ptr() == newFocusedElement;
});
if (didFocusSiblingField)
return;
}
m_editControlOwner->didBlurFromControl();
}
void DateTimeEditElement::fieldValueChanged()
{
if (m_editControlOwner)
m_editControlOwner->didChangeValueFromControl();
}
bool DateTimeEditElement::focusOnNextFocusableField(size_t startIndex)
{
for (size_t i = startIndex; i < m_fields.size(); ++i) {
if (m_fields[i]->isFocusable()) {
m_fields[i]->focus();
return true;
}
}
return false;
}
void DateTimeEditElement::focusByOwner()
{
focusOnNextFocusableField(0);
}
bool DateTimeEditElement::focusOnNextField(const DateTimeFieldElement& field)
{
auto startFieldIndex = fieldIndexOf(field);
if (startFieldIndex == notFound)
return false;
return focusOnNextFocusableField(startFieldIndex + 1);
}
bool DateTimeEditElement::focusOnPreviousField(const DateTimeFieldElement& field)
{
auto startFieldIndex = fieldIndexOf(field);
if (startFieldIndex == notFound)
return false;
auto fieldIndex = startFieldIndex;
while (fieldIndex > 0) {
--fieldIndex;
if (m_fields[fieldIndex]->isFocusable()) {
m_fields[fieldIndex]->focus();
return true;
}
}
return false;
}
bool DateTimeEditElement::isFieldOwnerDisabled() const
{
return m_editControlOwner && m_editControlOwner->isEditControlOwnerDisabled();
}
bool DateTimeEditElement::isFieldOwnerReadOnly() const
{
return m_editControlOwner && m_editControlOwner->isEditControlOwnerReadOnly();
}
AtomString DateTimeEditElement::localeIdentifier() const
{
return m_editControlOwner ? m_editControlOwner->localeIdentifier() : nullAtom();
}
const GregorianDateTime& DateTimeEditElement::placeholderDate() const
{
return m_placeholderDate;
}
void DateTimeEditElement::resetFields()
{
m_fields.shrink(0);
}
void DateTimeEditElement::setValueAsDate(const LayoutParameters& layoutParameters, const DateComponents& date)
{
layout(layoutParameters);
for (auto& field : m_fields)
field->setValueAsDate(date);
}
void DateTimeEditElement::setEmptyValue(const LayoutParameters& layoutParameters)
{
layout(layoutParameters);
for (auto& field : m_fields)
field->setEmptyValue();
}
String DateTimeEditElement::value() const
{
return m_editControlOwner ? m_editControlOwner->formatDateTimeFieldsState(valueAsDateTimeFieldsState()) : emptyString();
}
DateTimeFieldsState DateTimeEditElement::valueAsDateTimeFieldsState() const
{
DateTimeFieldsState dateTimeFieldsState;
for (auto& field : m_fields)
field->populateDateTimeFieldsState(dateTimeFieldsState);
return dateTimeFieldsState;
}
} // namespace WebCore
#endif // ENABLE(DATE_AND_TIME_INPUT_TYPES)