| /* |
| * 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) |