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    static PassOwnPtr<RadioButtonGroup> create();
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    RadioButtonGroup();
44    void setNeedsValidityCheckForAllButtons();
45    bool isValid() const;
46    void setCheckedButton(HTMLInputElement*);
47
48    HashSet<HTMLInputElement*> m_members;
49    HTMLInputElement* m_checkedButton;
50    size_t m_requiredCount;
51};
52
53RadioButtonGroup::RadioButtonGroup()
54    : m_checkedButton(0)
55    , m_requiredCount(0)
56{
57}
58
59PassOwnPtr<RadioButtonGroup> RadioButtonGroup::create()
60{
61    return adoptPtr(new RadioButtonGroup);
62}
63
64inline bool RadioButtonGroup::isValid() const
65{
66    return !isRequired() || m_checkedButton;
67}
68
69void RadioButtonGroup::setCheckedButton(HTMLInputElement* button)
70{
71    HTMLInputElement* oldCheckedButton = m_checkedButton;
72    if (oldCheckedButton == button)
73        return;
74    m_checkedButton = button;
75    if (oldCheckedButton)
76        oldCheckedButton->setChecked(false);
77}
78
79void RadioButtonGroup::add(HTMLInputElement* button)
80{
81    ASSERT(button->isRadioButton());
82    if (!m_members.add(button).isNewEntry)
83        return;
84    bool groupWasValid = isValid();
85    if (button->isRequired())
86        ++m_requiredCount;
87    if (button->checked())
88        setCheckedButton(button);
89
90    bool groupIsValid = isValid();
91    if (groupWasValid != groupIsValid)
92        setNeedsValidityCheckForAllButtons();
93    else if (!groupIsValid) {
94        // A radio button not in a group is always valid. We need to make it
95        // invalid only if the group is invalid.
96        button->setNeedsValidityCheck();
97    }
98}
99
100void RadioButtonGroup::updateCheckedState(HTMLInputElement* button)
101{
102    ASSERT(button->isRadioButton());
103    ASSERT(m_members.contains(button));
104    bool wasValid = isValid();
105    if (button->checked())
106        setCheckedButton(button);
107    else {
108        if (m_checkedButton == button)
109            m_checkedButton = 0;
110    }
111    if (wasValid != isValid())
112        setNeedsValidityCheckForAllButtons();
113}
114
115void RadioButtonGroup::requiredAttributeChanged(HTMLInputElement* button)
116{
117    ASSERT(button->isRadioButton());
118    ASSERT(m_members.contains(button));
119    bool wasValid = isValid();
120    if (button->isRequired())
121        ++m_requiredCount;
122    else {
123        ASSERT(m_requiredCount);
124        --m_requiredCount;
125    }
126    if (wasValid != isValid())
127        setNeedsValidityCheckForAllButtons();
128}
129
130void RadioButtonGroup::remove(HTMLInputElement* button)
131{
132    ASSERT(button->isRadioButton());
133    HashSet<HTMLInputElement*>::iterator it = m_members.find(button);
134    if (it == m_members.end())
135        return;
136    bool wasValid = isValid();
137    m_members.remove(it);
138    if (button->isRequired()) {
139        ASSERT(m_requiredCount);
140        --m_requiredCount;
141    }
142    if (m_checkedButton == button)
143        m_checkedButton = 0;
144
145    if (m_members.isEmpty()) {
146        ASSERT(!m_requiredCount);
147        ASSERT(!m_checkedButton);
148    } else if (wasValid != isValid())
149        setNeedsValidityCheckForAllButtons();
150    if (!wasValid) {
151        // A radio button not in a group is always valid. We need to make it
152        // valid only if the group was invalid.
153        button->setNeedsValidityCheck();
154    }
155}
156
157void RadioButtonGroup::setNeedsValidityCheckForAllButtons()
158{
159    typedef HashSet<HTMLInputElement*>::const_iterator Iterator;
160    Iterator end = m_members.end();
161    for (Iterator it = m_members.begin(); it != end; ++it) {
162        HTMLInputElement* button = *it;
163        ASSERT(button->isRadioButton());
164        button->setNeedsValidityCheck();
165    }
166}
167
168bool RadioButtonGroup::contains(HTMLInputElement* button) const
169{
170    return m_members.contains(button);
171}
172
173// ----------------------------------------------------------------
174
175// Explicity define empty constructor and destructor in order to prevent the
176// compiler from generating them as inlines. So we don't need to to define
177// RadioButtonGroup in the header.
178CheckedRadioButtons::CheckedRadioButtons()
179{
180}
181
182CheckedRadioButtons::~CheckedRadioButtons()
183{
184}
185
186void CheckedRadioButtons::addButton(HTMLInputElement* element)
187{
188    ASSERT(element->isRadioButton());
189    if (element->name().isEmpty())
190        return;
191
192    if (!m_nameToGroupMap)
193        m_nameToGroupMap = adoptPtr(new NameToGroupMap);
194
195    OwnPtr<RadioButtonGroup>& group = m_nameToGroupMap->add(element->name().impl(), PassOwnPtr<RadioButtonGroup>()).iterator->value;
196    if (!group)
197        group = RadioButtonGroup::create();
198    group->add(element);
199}
200
201void CheckedRadioButtons::updateCheckedState(HTMLInputElement* element)
202{
203    ASSERT(element->isRadioButton());
204    if (element->name().isEmpty())
205        return;
206    ASSERT(m_nameToGroupMap);
207    if (!m_nameToGroupMap)
208        return;
209    RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
210    ASSERT(group);
211    group->updateCheckedState(element);
212}
213
214void CheckedRadioButtons::requiredAttributeChanged(HTMLInputElement* element)
215{
216    ASSERT(element->isRadioButton());
217    if (element->name().isEmpty())
218        return;
219    ASSERT(m_nameToGroupMap);
220    if (!m_nameToGroupMap)
221        return;
222    RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
223    ASSERT(group);
224    group->requiredAttributeChanged(element);
225}
226
227HTMLInputElement* CheckedRadioButtons::checkedButtonForGroup(const AtomicString& name) const
228{
229    if (!m_nameToGroupMap)
230        return 0;
231    m_nameToGroupMap->checkConsistency();
232    RadioButtonGroup* group = m_nameToGroupMap->get(name.impl());
233    return group ? group->checkedButton() : 0;
234}
235
236bool CheckedRadioButtons::isInRequiredGroup(HTMLInputElement* element) const
237{
238    ASSERT(element->isRadioButton());
239    if (element->name().isEmpty())
240        return false;
241    if (!m_nameToGroupMap)
242        return false;
243    RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
244    return group && group->isRequired() && group->contains(element);
245}
246
247void CheckedRadioButtons::removeButton(HTMLInputElement* element)
248{
249    ASSERT(element->isRadioButton());
250    if (element->name().isEmpty())
251        return;
252    if (!m_nameToGroupMap)
253        return;
254
255    m_nameToGroupMap->checkConsistency();
256    NameToGroupMap::iterator it = m_nameToGroupMap->find(element->name().impl());
257    if (it == m_nameToGroupMap->end())
258        return;
259    it->value->remove(element);
260    if (it->value->isEmpty()) {
261        // FIXME: We may skip deallocating the empty RadioButtonGroup for
262        // performance improvement. If we do so, we need to change the key type
263        // of m_nameToGroupMap from AtomicStringImpl* to RefPtr<AtomicStringImpl>.
264        m_nameToGroupMap->remove(it);
265        if (m_nameToGroupMap->isEmpty())
266            m_nameToGroupMap.clear();
267    }
268}
269
270} // namespace
271