/* * Copyright (C) 2007, 2008, 2009, 2016 Apple Inc. All rights reserved. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * */ #include "config.h" #include "RadioButtonGroups.h" #include "HTMLInputElement.h" #include "Range.h" #include namespace WebCore { class RadioButtonGroup { WTF_MAKE_FAST_ALLOCATED; public: RadioButtonGroup(); bool isEmpty() const { return m_members.isEmpty(); } bool isRequired() const { return m_requiredCount; } HTMLInputElement* checkedButton() const { return m_checkedButton; } void add(HTMLInputElement*); void updateCheckedState(HTMLInputElement*); void requiredAttributeChanged(HTMLInputElement*); void remove(HTMLInputElement*); bool contains(HTMLInputElement*) const; Vector members() const; private: void setNeedsStyleRecalcForAllButtons(); void updateValidityForAllButtons(); bool isValid() const; void changeCheckedButton(HTMLInputElement*); void setCheckedButton(HTMLInputElement*); HashSet m_members; HTMLInputElement* m_checkedButton; size_t m_requiredCount; }; RadioButtonGroup::RadioButtonGroup() : m_checkedButton(nullptr) , m_requiredCount(0) { } inline bool RadioButtonGroup::isValid() const { return !isRequired() || m_checkedButton; } Vector RadioButtonGroup::members() const { Vector members; copyToVector(m_members, members); std::sort(members.begin(), members.end(), documentOrderComparator); return members; } void RadioButtonGroup::setCheckedButton(HTMLInputElement* button) { HTMLInputElement* oldCheckedButton = m_checkedButton; if (oldCheckedButton == button) return; bool hadCheckedButton = m_checkedButton; bool willHaveCheckedButton = button; if (hadCheckedButton != willHaveCheckedButton) setNeedsStyleRecalcForAllButtons(); m_checkedButton = button; if (oldCheckedButton) oldCheckedButton->setChecked(false); } void RadioButtonGroup::add(HTMLInputElement* button) { ASSERT(button->isRadioButton()); if (!m_members.add(button).isNewEntry) return; bool groupWasValid = isValid(); if (button->isRequired()) ++m_requiredCount; if (button->checked()) setCheckedButton(button); bool groupIsValid = isValid(); if (groupWasValid != groupIsValid) updateValidityForAllButtons(); else if (!groupIsValid) { // A radio button not in a group is always valid. We need to make it // invalid only if the group is invalid. button->updateValidity(); } } void RadioButtonGroup::updateCheckedState(HTMLInputElement* button) { ASSERT(button->isRadioButton()); ASSERT(m_members.contains(button)); bool wasValid = isValid(); if (button->checked()) setCheckedButton(button); else { if (m_checkedButton == button) setCheckedButton(nullptr); } if (wasValid != isValid()) updateValidityForAllButtons(); } void RadioButtonGroup::requiredAttributeChanged(HTMLInputElement* button) { ASSERT(button->isRadioButton()); ASSERT(m_members.contains(button)); bool wasValid = isValid(); if (button->isRequired()) ++m_requiredCount; else { ASSERT(m_requiredCount); --m_requiredCount; } if (wasValid != isValid()) updateValidityForAllButtons(); } void RadioButtonGroup::remove(HTMLInputElement* button) { ASSERT(button->isRadioButton()); HashSet::iterator it = m_members.find(button); if (it == m_members.end()) return; bool wasValid = isValid(); m_members.remove(it); if (button->isRequired()) { ASSERT(m_requiredCount); --m_requiredCount; } if (m_checkedButton) { button->invalidateStyleForSubtree(); if (m_checkedButton == button) { m_checkedButton = nullptr; setNeedsStyleRecalcForAllButtons(); } } if (m_members.isEmpty()) { ASSERT(!m_requiredCount); ASSERT(!m_checkedButton); } else if (wasValid != isValid()) updateValidityForAllButtons(); if (!wasValid) { // A radio button not in a group is always valid. We need to make it // valid only if the group was invalid. button->updateValidity(); } } void RadioButtonGroup::setNeedsStyleRecalcForAllButtons() { for (auto& button : m_members) { ASSERT(button->isRadioButton()); button->invalidateStyleForSubtree(); } } void RadioButtonGroup::updateValidityForAllButtons() { for (auto& button : m_members) { ASSERT(button->isRadioButton()); button->updateValidity(); } } bool RadioButtonGroup::contains(HTMLInputElement* button) const { return m_members.contains(button); } // ---------------------------------------------------------------- // Explicity define empty constructor and destructor in order to prevent the // compiler from generating them as inlines. So we don't need to to define // RadioButtonGroup in the header. RadioButtonGroups::RadioButtonGroups() { } RadioButtonGroups::~RadioButtonGroups() { } void RadioButtonGroups::addButton(HTMLInputElement* element) { ASSERT(element->isRadioButton()); if (element->name().isEmpty()) return; if (!m_nameToGroupMap) m_nameToGroupMap = std::make_unique(); auto& group = m_nameToGroupMap->add(element->name().impl(), nullptr).iterator->value; if (!group) group = std::make_unique(); group->add(element); } Vector RadioButtonGroups::groupMembers(const HTMLInputElement& element) const { ASSERT(element.isRadioButton()); if (!element.isRadioButton()) return { }; auto* name = element.name().impl(); if (!name) return { }; if (!m_nameToGroupMap) return { }; auto* group = m_nameToGroupMap->get(name); if (!group) return { }; return group->members(); } void RadioButtonGroups::updateCheckedState(HTMLInputElement* element) { ASSERT(element->isRadioButton()); if (element->name().isEmpty()) return; ASSERT(m_nameToGroupMap); if (!m_nameToGroupMap) return; RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl()); ASSERT(group); group->updateCheckedState(element); } void RadioButtonGroups::requiredAttributeChanged(HTMLInputElement* element) { ASSERT(element->isRadioButton()); if (element->name().isEmpty()) return; ASSERT(m_nameToGroupMap); if (!m_nameToGroupMap) return; RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl()); ASSERT(group); group->requiredAttributeChanged(element); } HTMLInputElement* RadioButtonGroups::checkedButtonForGroup(const AtomicString& name) const { if (!m_nameToGroupMap) return 0; m_nameToGroupMap->checkConsistency(); RadioButtonGroup* group = m_nameToGroupMap->get(name.impl()); return group ? group->checkedButton() : nullptr; } bool RadioButtonGroups::hasCheckedButton(const HTMLInputElement* element) const { ASSERT(element->isRadioButton()); const AtomicString& name = element->name(); if (name.isEmpty() || !m_nameToGroupMap) return element->checked(); const RadioButtonGroup* group = m_nameToGroupMap->get(name.impl()); return group->checkedButton(); } bool RadioButtonGroups::isInRequiredGroup(HTMLInputElement* element) const { ASSERT(element->isRadioButton()); if (element->name().isEmpty()) return false; if (!m_nameToGroupMap) return false; RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl()); return group && group->isRequired() && group->contains(element); } void RadioButtonGroups::removeButton(HTMLInputElement* element) { ASSERT(element->isRadioButton()); if (element->name().isEmpty()) return; if (!m_nameToGroupMap) return; m_nameToGroupMap->checkConsistency(); NameToGroupMap::iterator it = m_nameToGroupMap->find(element->name().impl()); if (it == m_nameToGroupMap->end()) return; it->value->remove(element); if (it->value->isEmpty()) { // FIXME: We may skip deallocating the empty RadioButtonGroup for // performance improvement. If we do so, we need to change the key type // of m_nameToGroupMap from AtomicStringImpl* to RefPtr. m_nameToGroupMap->remove(it); if (m_nameToGroupMap->isEmpty()) m_nameToGroupMap = nullptr; } } } // namespace