blob: 73b2271d731cebd4f833f4079b97ce72129d74fe [file] [log] [blame]
/*
* Copyright (C) 2007, 2008, 2009 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 "core/html/forms/RadioButtonGroupScope.h"
#include "core/InputTypeNames.h"
#include "core/html/HTMLInputElement.h"
#include "wtf/HashMap.h"
namespace blink {
class RadioButtonGroup : public GarbageCollected<RadioButtonGroup> {
public:
static RadioButtonGroup* create();
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;
unsigned size() const;
DECLARE_TRACE();
private:
RadioButtonGroup();
void setNeedsValidityCheckForAllButtons();
bool isValid() const;
void setCheckedButton(HTMLInputElement*);
// The map records the 'required' state of each (button) element.
using Members = HeapHashMap<Member<HTMLInputElement>, bool>;
using MemberKeyValue = WTF::KeyValuePair<Member<HTMLInputElement>, bool>;
void updateRequiredButton(MemberKeyValue&, bool isRequired);
Members m_members;
Member<HTMLInputElement> m_checkedButton;
size_t m_requiredCount;
};
RadioButtonGroup::RadioButtonGroup()
: m_checkedButton(nullptr), m_requiredCount(0) {}
RadioButtonGroup* RadioButtonGroup::create() {
return new RadioButtonGroup;
}
inline bool RadioButtonGroup::isValid() const {
return !isRequired() || m_checkedButton;
}
void RadioButtonGroup::setCheckedButton(HTMLInputElement* button) {
HTMLInputElement* oldCheckedButton = m_checkedButton;
if (oldCheckedButton == button)
return;
m_checkedButton = button;
if (oldCheckedButton)
oldCheckedButton->setChecked(false);
}
void RadioButtonGroup::updateRequiredButton(MemberKeyValue& it,
bool isRequired) {
if (it.value == isRequired)
return;
it.value = isRequired;
if (isRequired) {
m_requiredCount++;
} else {
DCHECK_GT(m_requiredCount, 0u);
m_requiredCount--;
}
}
void RadioButtonGroup::add(HTMLInputElement* button) {
DCHECK_EQ(button->type(), InputTypeNames::radio);
auto addResult = m_members.add(button, false);
if (!addResult.isNewEntry)
return;
bool groupWasValid = isValid();
updateRequiredButton(*addResult.storedValue, button->isRequired());
if (button->checked())
setCheckedButton(button);
bool groupIsValid = isValid();
if (groupWasValid != groupIsValid) {
setNeedsValidityCheckForAllButtons();
} 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->setNeedsValidityCheck();
}
}
void RadioButtonGroup::updateCheckedState(HTMLInputElement* button) {
DCHECK_EQ(button->type(), InputTypeNames::radio);
DCHECK(m_members.contains(button));
bool wasValid = isValid();
if (button->checked()) {
setCheckedButton(button);
} else {
if (m_checkedButton == button)
m_checkedButton = nullptr;
}
if (wasValid != isValid())
setNeedsValidityCheckForAllButtons();
for (auto& member : m_members) {
HTMLInputElement* const inputElement = member.key;
inputElement->pseudoStateChanged(CSSSelector::PseudoIndeterminate);
}
}
void RadioButtonGroup::requiredAttributeChanged(HTMLInputElement* button) {
DCHECK_EQ(button->type(), InputTypeNames::radio);
auto it = m_members.find(button);
DCHECK_NE(it, m_members.end());
bool wasValid = isValid();
// Synchronize the 'required' flag for the button, along with
// updating the overall count.
updateRequiredButton(*it, button->isRequired());
if (wasValid != isValid())
setNeedsValidityCheckForAllButtons();
}
void RadioButtonGroup::remove(HTMLInputElement* button) {
DCHECK_EQ(button->type(), InputTypeNames::radio);
auto it = m_members.find(button);
if (it == m_members.end())
return;
bool wasValid = isValid();
DCHECK_EQ(it->value, button->isRequired());
updateRequiredButton(*it, false);
m_members.remove(it);
if (m_checkedButton == button)
m_checkedButton = nullptr;
if (m_members.isEmpty()) {
DCHECK(!m_requiredCount);
DCHECK(!m_checkedButton);
} else if (wasValid != isValid()) {
setNeedsValidityCheckForAllButtons();
}
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->setNeedsValidityCheck();
}
// Send notification to update AX attributes for AXObjects which radiobutton
// group has.
if (!m_members.isEmpty()) {
HTMLInputElement* input = m_members.begin()->key;
if (AXObjectCache* cache = input->document().existingAXObjectCache())
cache->radiobuttonRemovedFromGroup(input);
}
}
void RadioButtonGroup::setNeedsValidityCheckForAllButtons() {
for (auto& element : m_members) {
HTMLInputElement* const button = element.key;
DCHECK_EQ(button->type(), InputTypeNames::radio);
button->setNeedsValidityCheck();
}
}
bool RadioButtonGroup::contains(HTMLInputElement* button) const {
return m_members.contains(button);
}
unsigned RadioButtonGroup::size() const {
return m_members.size();
}
DEFINE_TRACE(RadioButtonGroup) {
visitor->trace(m_members);
visitor->trace(m_checkedButton);
}
// ----------------------------------------------------------------
// 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.
RadioButtonGroupScope::RadioButtonGroupScope() {}
RadioButtonGroupScope::~RadioButtonGroupScope() {}
void RadioButtonGroupScope::addButton(HTMLInputElement* element) {
DCHECK_EQ(element->type(), InputTypeNames::radio);
if (element->name().isEmpty())
return;
if (!m_nameToGroupMap)
m_nameToGroupMap = new NameToGroupMap;
auto keyValue = m_nameToGroupMap->add(element->name(), nullptr).storedValue;
if (!keyValue->value) {
keyValue->value = RadioButtonGroup::create();
} else {
if (keyValue->key == element->name())
UseCounter::count(element->document(),
UseCounter::RadioNameMatchingStrict);
else if (equalIgnoringASCIICase(keyValue->key, element->name()))
UseCounter::count(element->document(),
UseCounter::RadioNameMatchingASCIICaseless);
else
UseCounter::count(element->document(),
UseCounter::RadioNameMatchingCaseFolding);
}
keyValue->value->add(element);
}
void RadioButtonGroupScope::updateCheckedState(HTMLInputElement* element) {
DCHECK_EQ(element->type(), InputTypeNames::radio);
if (element->name().isEmpty())
return;
DCHECK(m_nameToGroupMap);
if (!m_nameToGroupMap)
return;
RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
DCHECK(group);
group->updateCheckedState(element);
}
void RadioButtonGroupScope::requiredAttributeChanged(
HTMLInputElement* element) {
DCHECK_EQ(element->type(), InputTypeNames::radio);
if (element->name().isEmpty())
return;
DCHECK(m_nameToGroupMap);
if (!m_nameToGroupMap)
return;
RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
DCHECK(group);
group->requiredAttributeChanged(element);
}
HTMLInputElement* RadioButtonGroupScope::checkedButtonForGroup(
const AtomicString& name) const {
if (!m_nameToGroupMap)
return nullptr;
RadioButtonGroup* group = m_nameToGroupMap->get(name);
return group ? group->checkedButton() : nullptr;
}
bool RadioButtonGroupScope::isInRequiredGroup(HTMLInputElement* element) const {
DCHECK_EQ(element->type(), InputTypeNames::radio);
if (element->name().isEmpty())
return false;
if (!m_nameToGroupMap)
return false;
RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
return group && group->isRequired() && group->contains(element);
}
unsigned RadioButtonGroupScope::groupSizeFor(
const HTMLInputElement* element) const {
if (!m_nameToGroupMap)
return 0;
RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
if (!group)
return 0;
return group->size();
}
void RadioButtonGroupScope::removeButton(HTMLInputElement* element) {
DCHECK_EQ(element->type(), InputTypeNames::radio);
if (element->name().isEmpty())
return;
if (!m_nameToGroupMap)
return;
RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
if (!group)
return;
group->remove(element);
if (group->isEmpty()) {
// We don't remove an empty RadioButtonGroup from m_nameToGroupMap for
// better performance.
DCHECK(!group->isRequired());
SECURITY_DCHECK(!group->checkedButton());
}
}
DEFINE_TRACE(RadioButtonGroupScope) {
visitor->trace(m_nameToGroupMap);
}
} // namespace blink