778ab1a651c0cf305fe2c6ed95cb747bce604b58
[WebKit-https.git] / Source / WebCore / dom / RadioButtonGroups.cpp
1 /*
2  * Copyright (C) 2007, 2008, 2009, 2016 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 "RadioButtonGroups.h"
23
24 #include "HTMLInputElement.h"
25 #include "Range.h"
26 #include <wtf/HashSet.h>
27
28 namespace WebCore {
29
30 class RadioButtonGroup {
31     WTF_MAKE_FAST_ALLOCATED;
32 public:
33     RadioButtonGroup();
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     Vector<HTMLInputElement*> members() const;
43
44 private:
45     void setNeedsStyleRecalcForAllButtons();
46     void updateValidityForAllButtons();
47     bool isValid() const;
48     void changeCheckedButton(HTMLInputElement*);
49     void setCheckedButton(HTMLInputElement*);
50
51     HashSet<HTMLInputElement*> m_members;
52     HTMLInputElement* m_checkedButton;
53     size_t m_requiredCount;
54 };
55
56 RadioButtonGroup::RadioButtonGroup()
57     : m_checkedButton(nullptr)
58     , m_requiredCount(0)
59 {
60 }
61
62 inline bool RadioButtonGroup::isValid() const
63 {
64     return !isRequired() || m_checkedButton;
65 }
66
67 Vector<HTMLInputElement*> RadioButtonGroup::members() const
68 {
69     auto members = copyToVector(m_members);
70     std::sort(members.begin(), members.end(), documentOrderComparator);
71     return members;
72 }
73
74 void RadioButtonGroup::setCheckedButton(HTMLInputElement* button)
75 {
76     RefPtr<HTMLInputElement> oldCheckedButton = m_checkedButton;
77     if (oldCheckedButton == button)
78         return;
79
80     bool hadCheckedButton = m_checkedButton;
81     bool willHaveCheckedButton = button;
82     if (hadCheckedButton != willHaveCheckedButton)
83         setNeedsStyleRecalcForAllButtons();
84
85     m_checkedButton = button;
86     if (oldCheckedButton)
87         oldCheckedButton->setChecked(false);
88 }
89
90 void RadioButtonGroup::add(HTMLInputElement* button)
91 {
92     ASSERT(button->isRadioButton());
93     if (!m_members.add(button).isNewEntry)
94         return;
95     bool groupWasValid = isValid();
96     if (button->isRequired())
97         ++m_requiredCount;
98     if (button->checked())
99         setCheckedButton(button);
100
101     bool groupIsValid = isValid();
102     if (groupWasValid != groupIsValid)
103         updateValidityForAllButtons();
104     else if (!groupIsValid) {
105         // A radio button not in a group is always valid. We need to make it
106         // invalid only if the group is invalid.
107         button->updateValidity();
108     }
109 }
110
111 void RadioButtonGroup::updateCheckedState(HTMLInputElement* button)
112 {
113     ASSERT(button->isRadioButton());
114     ASSERT(m_members.contains(button));
115     bool wasValid = isValid();
116     if (button->checked())
117         setCheckedButton(button);
118     else {
119         if (m_checkedButton == button)
120             setCheckedButton(nullptr);
121     }
122     if (wasValid != isValid())
123         updateValidityForAllButtons();
124 }
125
126 void RadioButtonGroup::requiredAttributeChanged(HTMLInputElement* button)
127 {
128     ASSERT(button->isRadioButton());
129     ASSERT(m_members.contains(button));
130     bool wasValid = isValid();
131     if (button->isRequired())
132         ++m_requiredCount;
133     else {
134         ASSERT(m_requiredCount);
135         --m_requiredCount;
136     }
137     if (wasValid != isValid())
138         updateValidityForAllButtons();
139 }
140
141 void RadioButtonGroup::remove(HTMLInputElement* button)
142 {
143     ASSERT(button->isRadioButton());
144     HashSet<HTMLInputElement*>::iterator it = m_members.find(button);
145     if (it == m_members.end())
146         return;
147
148     bool wasValid = isValid();
149     m_members.remove(it);
150     if (button->isRequired()) {
151         ASSERT(m_requiredCount);
152         --m_requiredCount;
153     }
154     if (m_checkedButton) {
155         button->invalidateStyleForSubtree();
156         if (m_checkedButton == button) {
157             m_checkedButton = nullptr;
158             setNeedsStyleRecalcForAllButtons();
159         }
160     }
161
162     if (m_members.isEmpty()) {
163         ASSERT(!m_requiredCount);
164         ASSERT(!m_checkedButton);
165     } else if (wasValid != isValid())
166         updateValidityForAllButtons();
167     if (!wasValid) {
168         // A radio button not in a group is always valid. We need to make it
169         // valid only if the group was invalid.
170         button->updateValidity();
171     }
172 }
173
174 void RadioButtonGroup::setNeedsStyleRecalcForAllButtons()
175 {
176     for (auto& button : m_members) {
177         ASSERT(button->isRadioButton());
178         button->invalidateStyleForSubtree();
179     }
180 }
181
182 void RadioButtonGroup::updateValidityForAllButtons()
183 {
184     for (auto& button : m_members) {
185         ASSERT(button->isRadioButton());
186         button->updateValidity();
187     }
188 }
189
190 bool RadioButtonGroup::contains(HTMLInputElement* button) const
191 {
192     return m_members.contains(button);
193 }
194
195 // ----------------------------------------------------------------
196
197 // Explicity define empty constructor and destructor in order to prevent the
198 // compiler from generating them as inlines. So we don't need to to define
199 // RadioButtonGroup in the header.
200 RadioButtonGroups::RadioButtonGroups() = default;
201
202 RadioButtonGroups::~RadioButtonGroups() = default;
203
204 void RadioButtonGroups::addButton(HTMLInputElement* element)
205 {
206     ASSERT(element->isRadioButton());
207     if (element->name().isEmpty())
208         return;
209
210     if (!m_nameToGroupMap)
211         m_nameToGroupMap = std::make_unique<NameToGroupMap>();
212
213     auto& group = m_nameToGroupMap->add(element->name().impl(), nullptr).iterator->value;
214     if (!group)
215         group = std::make_unique<RadioButtonGroup>();
216     group->add(element);
217 }
218
219 Vector<HTMLInputElement*> RadioButtonGroups::groupMembers(const HTMLInputElement& element) const
220 {
221     ASSERT(element.isRadioButton());
222     if (!element.isRadioButton())
223         return { };
224
225     auto* name = element.name().impl();
226     if (!name)
227         return { };
228
229     if (!m_nameToGroupMap)
230         return { };
231     
232     auto* group = m_nameToGroupMap->get(name);
233     if (!group)
234         return { };
235     return group->members();
236 }
237
238 void RadioButtonGroups::updateCheckedState(HTMLInputElement* element)
239 {
240     ASSERT(element->isRadioButton());
241     if (element->name().isEmpty())
242         return;
243     ASSERT(m_nameToGroupMap);
244     if (!m_nameToGroupMap)
245         return;
246     RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
247     ASSERT(group);
248     group->updateCheckedState(element);
249 }
250
251 void RadioButtonGroups::requiredAttributeChanged(HTMLInputElement* element)
252 {
253     ASSERT(element->isRadioButton());
254     if (element->name().isEmpty())
255         return;
256     ASSERT(m_nameToGroupMap);
257     if (!m_nameToGroupMap)
258         return;
259     RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
260     ASSERT(group);
261     group->requiredAttributeChanged(element);
262 }
263
264 HTMLInputElement* RadioButtonGroups::checkedButtonForGroup(const AtomicString& name) const
265 {
266     if (!m_nameToGroupMap)
267         return 0;
268     m_nameToGroupMap->checkConsistency();
269     RadioButtonGroup* group = m_nameToGroupMap->get(name.impl());
270     return group ? group->checkedButton() : nullptr;
271 }
272
273 bool RadioButtonGroups::hasCheckedButton(const HTMLInputElement* element) const
274 {
275     ASSERT(element->isRadioButton());
276     const AtomicString& name = element->name();
277     if (name.isEmpty() || !m_nameToGroupMap)
278         return element->checked();
279
280     const RadioButtonGroup* group = m_nameToGroupMap->get(name.impl());
281     return group->checkedButton();
282 }
283
284 bool RadioButtonGroups::isInRequiredGroup(HTMLInputElement* element) const
285 {
286     ASSERT(element->isRadioButton());
287     if (element->name().isEmpty())
288         return false;
289     if (!m_nameToGroupMap)
290         return false;
291     RadioButtonGroup* group = m_nameToGroupMap->get(element->name().impl());
292     return group && group->isRequired() && group->contains(element);
293 }
294
295 void RadioButtonGroups::removeButton(HTMLInputElement* element)
296 {
297     ASSERT(element->isRadioButton());
298     if (element->name().isEmpty())
299         return;
300     if (!m_nameToGroupMap)
301         return;
302
303     m_nameToGroupMap->checkConsistency();
304     NameToGroupMap::iterator it = m_nameToGroupMap->find(element->name().impl());
305     if (it == m_nameToGroupMap->end())
306         return;
307     it->value->remove(element);
308     if (it->value->isEmpty()) {
309         // FIXME: We may skip deallocating the empty RadioButtonGroup for
310         // performance improvement. If we do so, we need to change the key type
311         // of m_nameToGroupMap from AtomicStringImpl* to RefPtr<AtomicStringImpl>.
312         m_nameToGroupMap->remove(it);
313         if (m_nameToGroupMap->isEmpty())
314             m_nameToGroupMap = nullptr;
315     }
316 }
317
318 } // namespace