1/*
2 * Copyright (C) 2007, 2008, 2009 Apple Inc. All rights reserved.
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public License
15 * along with this library; see the file COPYING.LIB.  If not, write to
16 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 *
19 */
20
21#include "config.h"
22#include "CheckedRadioButtons.h"
23
24#include "HTMLInputElement.h"
25#include <wtf/HashSet.h>
26
27namespace WebCore {
28
29class RadioButtonGroup {
30    WTF_MAKE_FAST_ALLOCATED;
31public:
32    RadioButtonGroup();
33    bool isEmpty() const { return m_members.isEmpty(); }
34    bool isRequired() const { return m_requiredCount; }
35    HTMLInputElement* checkedButton() const { return m_checkedButton; }
36    void add(HTMLInputElement*);
37    void updateCheckedState(HTMLInputElement*);
38    void requiredAttributeChanged(HTMLInputElement*);
39    void remove(HTMLInputElement*);
40    bool contains(HTMLInputElement*) const;
41
42private:
43    void setNeedsValidityCheckForAllButtons();
44    bool isValid() const;
45    void setCheckedButton(HTMLInputElement*);
46
47    HashSet<HTMLInputElement*> m_members;
48    HTMLInputElement* m_checkedButton;
49    size_t m_requiredCount;
50};
51
52RadioButtonGroup::RadioButtonGroup()
53    : m_checkedButton(nullptr)
54    , m_requiredCount(0)
55{
56}
57
58inline bool RadioButtonGroup::isValid() const
59{
60    return !isRequired() || m_checkedButton;
61}
62
63void RadioButtonGroup::setCheckedButton(HTMLInputElement* button)
64{
65    HTMLInputElement* oldCheckedButton = m_checkedButton;
66    if (oldCheckedButton == button)
67        return;
68    m_checkedButton = button;
69    if (oldCheckedButton)
70        oldCheckedButton->setChecked(false);
71}
72
73void RadioButtonGroup::add(HTMLInputElement* button)
74{
75    ASSERT(button->isRadioButton());
76    if (!m_members.add(button).isNewEntry)
77        return;
78    bool groupWasValid = isValid();
79    if (button->isRequired())
80        ++m_requiredCount;
81    if (button->checked())
82        setCheckedButton(button);
83
84    bool groupIsValid = isValid();
85    if (groupWasValid != groupIsValid)
86        setNeedsValidityCheckForAllButtons();
87    else if (!groupIsValid) {
88        // A radio button not in a group is always valid. We need to make it
89        // invalid only if the group is invalid.
90        button->setNeedsValidityCheck();
91    }
92}
93
94void RadioButtonGroup::updateCheckedState(HTMLInputElement* button)
95{
96    ASSERT(button->isRadioButton());
97    ASSERT(m_members.contains(button));
98    bool wasValid = isValid();
99    if (button->checked())
100        setCheckedButton(button);
101    else {
102        if (m_checkedButton == button)
103            m_checkedButton = 0;
104    }
105    if (wasValid != isValid())
106        setNeedsValidityCheckForAllButtons();
107}
108
109void RadioButtonGroup::requiredAttributeChanged(HTMLInputElement* button)
110{
111    ASSERT(button->isRadioButton());
112    ASSERT(m_members.contains(button));
113    bool wasValid = isValid();
114    if (button->isRequired())
115        ++m_requiredCount;
116    else {
117        ASSERT(m_requiredCount);
118        --m_requiredCount;
119    }
120    if (wasValid != isValid())
121        setNeedsValidityCheckForAllButtons();
122}
123
124void RadioButtonGroup::remove(HTMLInputElement* button)
125{
126    ASSERT(button->isRadioButton());
127    HashSet<HTMLInputElement*>::iterator it = m_members.find(button);
128    if (it == m_members.end())
129        return;
130    bool wasValid = isValid();
131    m_members.remove(it);
132    if (button->isRequired()) {
133        ASSERT(m_requiredCount);
134        --m_requiredCount;
135    }
136    if (m_checkedButton == button)
137        m_checkedButton = nullptr;
138
139    if (m_members.isEmpty()) {
140        ASSERT(!m_requiredCount);
141        ASSERT(!m_checkedButton);
142    } else if (wasValid != isValid())
143        setNeedsValidityCheckForAllButtons();
144    if (!wasValid) {
145        // A radio button not in a group is always valid. We need to make it
146        // valid only if the group was invalid.
147        button->setNeedsValidityCheck();
148    }
149}
150
151void RadioButtonGroup::setNeedsValidityCheckForAllButtons()
152{
153    typedef HashSet<HTMLInputElement*>::const_iterator Iterator;
154    Iterator end = m_members.end();
155    for (Iterator it = m_members.begin(); it != end; ++it) {
156        HTMLInputElement* button = *it;
157        ASSERT(button->isRadioButton());
158        button->setNeedsValidityCheck();
159    }
160}
161
162bool RadioButtonGroup::contains(HTMLInputElement* button) const
163{
164    return m_members.contains(button);
165}
166
167// ----------------------------------------------------------------
168
169// Explicity define empty constructor and destructor in order to prevent the
170// compiler from generating them as inlines. So we don't need to to define
171// RadioButtonGroup in the header.
172CheckedRadioButtons::CheckedRadioButtons()
173{
174}
175
176CheckedRadioButtons::~CheckedRadioButtons()
177{
178}
179
180void CheckedRadioButtons::addButton(HTMLInputElement* element)
181{
182    ASSERT(element->isRadioButton());
183    if (element->name().isEmpty())
184        return;
185
186    if (!m_nameToGroupMap)
187        m_nameToGroupMap = std::make_unique<NameToGroupMap>();
188
189    auto& group = m_nameToGroupMap->add(element->name().impl(), nullptr).iterator->value;
190    if (!group)
191        group = std::make_unique<RadioButtonGroup>();
192    group->add(element);
193}
194
195void CheckedRadioButtons::updateCheckedState(HTMLInputElement* element)
196{
197    ASSERT(element->isRadioButton());
198    if (element->name().isEmpty())
199        return;
200    ASSERT(m_nameToGroupMap);
201    if (!m_nameToGroupMap)
202        return;
203    RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
204    ASSERT(group);
205    group->updateCheckedState(element);
206}
207
208void CheckedRadioButtons::requiredAttributeChanged(HTMLInputElement* element)
209{
210    ASSERT(element->isRadioButton());
211    if (element->name().isEmpty())
212        return;
213    ASSERT(m_nameToGroupMap);
214    if (!m_nameToGroupMap)
215        return;
216    RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
217    ASSERT(group);
218    group->requiredAttributeChanged(element);
219}
220
221HTMLInputElement* CheckedRadioButtons::checkedButtonForGroup(const AtomicString& name) const
222{
223    if (!m_nameToGroupMap)
224        return 0;
225    m_nameToGroupMap->checkConsistency();
226    RadioButtonGroup* group = m_nameToGroupMap->get(name.impl());
227    return group ? group->checkedButton() : 0;
228}
229
230bool CheckedRadioButtons::isInRequiredGroup(HTMLInputElement* element) const
231{
232    ASSERT(element->isRadioButton());
233    if (element->name().isEmpty())
234        return false;
235    if (!m_nameToGroupMap)
236        return false;
237    RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
238    return group && group->isRequired() && group->contains(element);
239}
240
241void CheckedRadioButtons::removeButton(HTMLInputElement* element)
242{
243    ASSERT(element->isRadioButton());
244    if (element->name().isEmpty())
245        return;
246    if (!m_nameToGroupMap)
247        return;
248
249    m_nameToGroupMap->checkConsistency();
250    NameToGroupMap::iterator it = m_nameToGroupMap->find(element->name().impl());
251    if (it == m_nameToGroupMap->end())
252        return;
253    it->value->remove(element);
254    if (it->value->isEmpty()) {
255        // FIXME: We may skip deallocating the empty RadioButtonGroup for
256        // performance improvement. If we do so, we need to change the key type
257        // of m_nameToGroupMap from AtomicStringImpl* to RefPtr<AtomicStringImpl>.
258        m_nameToGroupMap->remove(it);
259        if (m_nameToGroupMap->isEmpty())
260            m_nameToGroupMap = nullptr;
261    }
262}
263
264} // namespace
265