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 "core/html/forms/RadioButtonGroupScope.h"
23
24#include "core/InputTypeNames.h"
25#include "core/html/HTMLInputElement.h"
26#include "wtf/HashSet.h"
27
28namespace blink {
29
30class RadioButtonGroup : public NoBaseWillBeGarbageCollected<RadioButtonGroup> {
31    WTF_MAKE_FAST_ALLOCATED_WILL_BE_REMOVED;
32public:
33    static PassOwnPtrWillBeRawPtr<RadioButtonGroup> create();
34    bool isEmpty() const { return m_members.isEmpty(); }
35    bool isRequired() const { return m_requiredCount; }
36    HTMLInputElement* checkedButton() const { return m_checkedButton; }
37    void add(HTMLInputElement*);
38    void updateCheckedState(HTMLInputElement*);
39    void requiredAttributeChanged(HTMLInputElement*);
40    void remove(HTMLInputElement*);
41    bool contains(HTMLInputElement*) const;
42
43    void trace(Visitor*);
44
45private:
46    RadioButtonGroup();
47    void setNeedsValidityCheckForAllButtons();
48    bool isValid() const;
49    void setCheckedButton(HTMLInputElement*);
50
51    WillBeHeapHashSet<RawPtrWillBeMember<HTMLInputElement> > m_members;
52    RawPtrWillBeMember<HTMLInputElement> m_checkedButton;
53    size_t m_requiredCount;
54};
55
56RadioButtonGroup::RadioButtonGroup()
57    : m_checkedButton(nullptr)
58    , m_requiredCount(0)
59{
60}
61
62PassOwnPtrWillBeRawPtr<RadioButtonGroup> RadioButtonGroup::create()
63{
64    return adoptPtrWillBeNoop(new RadioButtonGroup);
65}
66
67inline bool RadioButtonGroup::isValid() const
68{
69    return !isRequired() || m_checkedButton;
70}
71
72void RadioButtonGroup::setCheckedButton(HTMLInputElement* button)
73{
74    HTMLInputElement* oldCheckedButton = m_checkedButton;
75    if (oldCheckedButton == button)
76        return;
77    m_checkedButton = button;
78    if (oldCheckedButton)
79        oldCheckedButton->setChecked(false);
80}
81
82void RadioButtonGroup::add(HTMLInputElement* button)
83{
84    ASSERT(button->type() == InputTypeNames::radio);
85    if (!m_members.add(button).isNewEntry)
86        return;
87    bool groupWasValid = isValid();
88    if (button->isRequired())
89        ++m_requiredCount;
90    if (button->checked())
91        setCheckedButton(button);
92
93    bool groupIsValid = isValid();
94    if (groupWasValid != groupIsValid) {
95        setNeedsValidityCheckForAllButtons();
96    } else if (!groupIsValid) {
97        // A radio button not in a group is always valid. We need to make it
98        // invalid only if the group is invalid.
99        button->setNeedsValidityCheck();
100    }
101}
102
103void RadioButtonGroup::updateCheckedState(HTMLInputElement* button)
104{
105    ASSERT(button->type() == InputTypeNames::radio);
106    ASSERT(m_members.contains(button));
107    bool wasValid = isValid();
108    if (button->checked()) {
109        setCheckedButton(button);
110    } else {
111        if (m_checkedButton == button)
112            m_checkedButton = nullptr;
113    }
114    if (wasValid != isValid())
115        setNeedsValidityCheckForAllButtons();
116    typedef WillBeHeapHashSet<RawPtrWillBeMember<HTMLInputElement> >::const_iterator Iterator;
117    Iterator end = m_members.end();
118    for (Iterator it = m_members.begin(); it != end; ++it) {
119        (*it)->pseudoStateChanged(CSSSelector::PseudoIndeterminate);
120    }
121}
122
123void RadioButtonGroup::requiredAttributeChanged(HTMLInputElement* button)
124{
125    ASSERT(button->type() == InputTypeNames::radio);
126    ASSERT(m_members.contains(button));
127    bool wasValid = isValid();
128    if (button->isRequired()) {
129        ++m_requiredCount;
130    } else {
131        ASSERT(m_requiredCount);
132        --m_requiredCount;
133    }
134    if (wasValid != isValid())
135        setNeedsValidityCheckForAllButtons();
136}
137
138void RadioButtonGroup::remove(HTMLInputElement* button)
139{
140    ASSERT(button->type() == InputTypeNames::radio);
141    WillBeHeapHashSet<RawPtrWillBeMember<HTMLInputElement> >::iterator it = m_members.find(button);
142    if (it == m_members.end())
143        return;
144    bool wasValid = isValid();
145    m_members.remove(it);
146    if (button->isRequired()) {
147        ASSERT(m_requiredCount);
148        --m_requiredCount;
149    }
150    if (m_checkedButton == button)
151        m_checkedButton = nullptr;
152
153    if (m_members.isEmpty()) {
154        ASSERT(!m_requiredCount);
155        ASSERT(!m_checkedButton);
156    } else if (wasValid != isValid()) {
157        setNeedsValidityCheckForAllButtons();
158    }
159    if (!wasValid) {
160        // A radio button not in a group is always valid. We need to make it
161        // valid only if the group was invalid.
162        button->setNeedsValidityCheck();
163    }
164}
165
166void RadioButtonGroup::setNeedsValidityCheckForAllButtons()
167{
168    typedef WillBeHeapHashSet<RawPtrWillBeMember<HTMLInputElement> >::const_iterator Iterator;
169    Iterator end = m_members.end();
170    for (Iterator it = m_members.begin(); it != end; ++it) {
171        HTMLInputElement* button = *it;
172        ASSERT(button->type() == InputTypeNames::radio);
173        button->setNeedsValidityCheck();
174    }
175}
176
177bool RadioButtonGroup::contains(HTMLInputElement* button) const
178{
179    return m_members.contains(button);
180}
181
182void RadioButtonGroup::trace(Visitor* visitor)
183{
184#if ENABLE(OILPAN)
185    visitor->trace(m_members);
186    visitor->trace(m_checkedButton);
187#endif
188}
189
190// ----------------------------------------------------------------
191
192// Explicity define empty constructor and destructor in order to prevent the
193// compiler from generating them as inlines. So we don't need to to define
194// RadioButtonGroup in the header.
195RadioButtonGroupScope::RadioButtonGroupScope()
196{
197}
198
199RadioButtonGroupScope::~RadioButtonGroupScope()
200{
201}
202
203void RadioButtonGroupScope::addButton(HTMLInputElement* element)
204{
205    ASSERT(element->type() == InputTypeNames::radio);
206    if (element->name().isEmpty())
207        return;
208
209    if (!m_nameToGroupMap)
210        m_nameToGroupMap = adoptPtrWillBeNoop(new NameToGroupMap);
211
212    OwnPtrWillBeMember<RadioButtonGroup>& group = m_nameToGroupMap->add(element->name(), nullptr).storedValue->value;
213    if (!group)
214        group = RadioButtonGroup::create();
215    group->add(element);
216}
217
218void RadioButtonGroupScope::updateCheckedState(HTMLInputElement* element)
219{
220    ASSERT(element->type() == InputTypeNames::radio);
221    if (element->name().isEmpty())
222        return;
223    ASSERT(m_nameToGroupMap);
224    if (!m_nameToGroupMap)
225        return;
226    RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
227    ASSERT(group);
228    group->updateCheckedState(element);
229}
230
231void RadioButtonGroupScope::requiredAttributeChanged(HTMLInputElement* element)
232{
233    ASSERT(element->type() == InputTypeNames::radio);
234    if (element->name().isEmpty())
235        return;
236    ASSERT(m_nameToGroupMap);
237    if (!m_nameToGroupMap)
238        return;
239    RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
240    ASSERT(group);
241    group->requiredAttributeChanged(element);
242}
243
244HTMLInputElement* RadioButtonGroupScope::checkedButtonForGroup(const AtomicString& name) const
245{
246    if (!m_nameToGroupMap)
247        return 0;
248    RadioButtonGroup* group = m_nameToGroupMap->get(name);
249    return group ? group->checkedButton() : 0;
250}
251
252bool RadioButtonGroupScope::isInRequiredGroup(HTMLInputElement* element) const
253{
254    ASSERT(element->type() == InputTypeNames::radio);
255    if (element->name().isEmpty())
256        return false;
257    if (!m_nameToGroupMap)
258        return false;
259    RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
260    return group && group->isRequired() && group->contains(element);
261}
262
263void RadioButtonGroupScope::removeButton(HTMLInputElement* element)
264{
265    ASSERT(element->type() == InputTypeNames::radio);
266    if (element->name().isEmpty())
267        return;
268    if (!m_nameToGroupMap)
269        return;
270
271    RadioButtonGroup* group = m_nameToGroupMap->get(element->name());
272    if (!group)
273        return;
274    group->remove(element);
275    if (group->isEmpty()) {
276        // We don't remove an empty RadioButtonGroup from m_nameToGroupMap for
277        // better performance.
278        ASSERT(!group->isRequired());
279        ASSERT_WITH_SECURITY_IMPLICATION(!group->checkedButton());
280    }
281}
282
283void RadioButtonGroupScope::trace(Visitor* visitor)
284{
285#if ENABLE(OILPAN)
286    visitor->trace(m_nameToGroupMap);
287#endif
288}
289
290} // namespace
291