1/*
2 * Copyright (C) 2007-2018 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
28namespace WebCore {
29
30class RadioButtonGroup {
31 WTF_MAKE_FAST_ALLOCATED;
32public:
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 requiredStateChanged(HTMLInputElement&);
39 void remove(HTMLInputElement&);
40 bool contains(HTMLInputElement&) const;
41 Vector<HTMLInputElement*> members() const;
42
43private:
44 void setNeedsStyleRecalcForAllButtons();
45 void updateValidityForAllButtons();
46 bool isValid() const;
47 void setCheckedButton(HTMLInputElement*);
48
49 HashSet<HTMLInputElement*> m_members;
50 HTMLInputElement* m_checkedButton { nullptr };
51 size_t m_requiredCount { 0 };
52};
53
54inline bool RadioButtonGroup::isValid() const
55{
56 return !isRequired() || m_checkedButton;
57}
58
59Vector<HTMLInputElement*> RadioButtonGroup::members() const
60{
61 auto members = copyToVector(m_members);
62 std::sort(members.begin(), members.end(), documentOrderComparator);
63 return members;
64}
65
66void RadioButtonGroup::setCheckedButton(HTMLInputElement* button)
67{
68 RefPtr<HTMLInputElement> oldCheckedButton = m_checkedButton;
69 if (oldCheckedButton == button)
70 return;
71
72 bool hadCheckedButton = m_checkedButton;
73 bool willHaveCheckedButton = button;
74 if (hadCheckedButton != willHaveCheckedButton)
75 setNeedsStyleRecalcForAllButtons();
76
77 m_checkedButton = button;
78 if (oldCheckedButton)
79 oldCheckedButton->setChecked(false);
80}
81
82void RadioButtonGroup::add(HTMLInputElement& button)
83{
84 ASSERT(button.isRadioButton());
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 updateValidityForAllButtons();
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.updateValidity();
100 }
101}
102
103void RadioButtonGroup::updateCheckedState(HTMLInputElement& button)
104{
105 ASSERT(button.isRadioButton());
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 setCheckedButton(nullptr);
113 }
114 if (wasValid != isValid())
115 updateValidityForAllButtons();
116}
117
118void RadioButtonGroup::requiredStateChanged(HTMLInputElement& button)
119{
120 ASSERT(button.isRadioButton());
121 ASSERT(m_members.contains(&button));
122 bool wasValid = isValid();
123 if (button.isRequired())
124 ++m_requiredCount;
125 else {
126 ASSERT(m_requiredCount);
127 --m_requiredCount;
128 }
129 if (wasValid != isValid())
130 updateValidityForAllButtons();
131}
132
133void RadioButtonGroup::remove(HTMLInputElement& button)
134{
135 ASSERT(button.isRadioButton());
136 auto it = m_members.find(&button);
137 if (it == m_members.end())
138 return;
139
140 bool wasValid = isValid();
141 m_members.remove(it);
142 if (button.isRequired()) {
143 ASSERT(m_requiredCount);
144 --m_requiredCount;
145 }
146 if (m_checkedButton) {
147 button.invalidateStyleForSubtree();
148 if (m_checkedButton == &button) {
149 m_checkedButton = nullptr;
150 setNeedsStyleRecalcForAllButtons();
151 }
152 }
153
154 if (m_members.isEmpty()) {
155 ASSERT(!m_requiredCount);
156 ASSERT(!m_checkedButton);
157 } else if (wasValid != isValid())
158 updateValidityForAllButtons();
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.updateValidity();
163 }
164}
165
166void RadioButtonGroup::setNeedsStyleRecalcForAllButtons()
167{
168 for (auto& button : m_members) {
169 ASSERT(button->isRadioButton());
170 button->invalidateStyleForSubtree();
171 }
172}
173
174void RadioButtonGroup::updateValidityForAllButtons()
175{
176 for (auto& button : m_members) {
177 ASSERT(button->isRadioButton());
178 button->updateValidity();
179 }
180}
181
182bool RadioButtonGroup::contains(HTMLInputElement& button) const
183{
184 return m_members.contains(&button);
185}
186
187// ----------------------------------------------------------------
188
189// Explicitly define default constructor and destructor here outside the header
190// so we can compile the header without including the definition of RadioButtonGroup.
191RadioButtonGroups::RadioButtonGroups() = default;
192RadioButtonGroups::~RadioButtonGroups() = default;
193
194void RadioButtonGroups::addButton(HTMLInputElement& element)
195{
196 ASSERT(element.isRadioButton());
197 if (element.name().isEmpty())
198 return;
199
200 if (!m_nameToGroupMap)
201 m_nameToGroupMap = std::make_unique<NameToGroupMap>();
202
203 auto& group = m_nameToGroupMap->add(element.name().impl(), nullptr).iterator->value;
204 if (!group)
205 group = std::make_unique<RadioButtonGroup>();
206 group->add(element);
207}
208
209Vector<HTMLInputElement*> RadioButtonGroups::groupMembers(const HTMLInputElement& element) const
210{
211 ASSERT(element.isRadioButton());
212 if (!element.isRadioButton())
213 return { };
214
215 auto* name = element.name().impl();
216 if (!name)
217 return { };
218
219 if (!m_nameToGroupMap)
220 return { };
221
222 auto* group = m_nameToGroupMap->get(name);
223 if (!group)
224 return { };
225 return group->members();
226}
227
228void RadioButtonGroups::updateCheckedState(HTMLInputElement& element)
229{
230 ASSERT(element.isRadioButton());
231 if (element.name().isEmpty())
232 return;
233 ASSERT(m_nameToGroupMap);
234 if (!m_nameToGroupMap)
235 return;
236 m_nameToGroupMap->get(element.name().impl())->updateCheckedState(element);
237}
238
239void RadioButtonGroups::requiredStateChanged(HTMLInputElement& element)
240{
241 ASSERT(element.isRadioButton());
242 if (element.name().isEmpty())
243 return;
244 ASSERT(m_nameToGroupMap);
245 if (!m_nameToGroupMap)
246 return;
247 auto* group = m_nameToGroupMap->get(element.name().impl());
248 ASSERT(group);
249 group->requiredStateChanged(element);
250}
251
252HTMLInputElement* RadioButtonGroups::checkedButtonForGroup(const AtomString& name) const
253{
254 if (!m_nameToGroupMap)
255 return nullptr;
256 m_nameToGroupMap->checkConsistency();
257 RadioButtonGroup* group = m_nameToGroupMap->get(name.impl());
258 return group ? group->checkedButton() : nullptr;
259}
260
261bool RadioButtonGroups::hasCheckedButton(const HTMLInputElement& element) const
262{
263 ASSERT(element.isRadioButton());
264 const AtomString& name = element.name();
265 if (name.isEmpty() || !m_nameToGroupMap)
266 return element.checked();
267 return m_nameToGroupMap->get(name.impl())->checkedButton();
268}
269
270bool RadioButtonGroups::isInRequiredGroup(HTMLInputElement& element) const
271{
272 ASSERT(element.isRadioButton());
273 if (element.name().isEmpty())
274 return false;
275 if (!m_nameToGroupMap)
276 return false;
277 auto* group = m_nameToGroupMap->get(element.name().impl());
278 return group && group->isRequired() && group->contains(element);
279}
280
281void RadioButtonGroups::removeButton(HTMLInputElement& element)
282{
283 ASSERT(element.isRadioButton());
284 if (element.name().isEmpty())
285 return;
286 if (!m_nameToGroupMap)
287 return;
288
289 m_nameToGroupMap->checkConsistency();
290 auto it = m_nameToGroupMap->find(element.name().impl());
291 if (it == m_nameToGroupMap->end())
292 return;
293 it->value->remove(element);
294 if (it->value->isEmpty()) {
295 // FIXME: We may skip deallocating the empty RadioButtonGroup for
296 // performance improvement. If we do so, we need to change the key type
297 // of m_nameToGroupMap from AtomStringImpl* to RefPtr<AtomStringImpl>.
298 m_nameToGroupMap->remove(it);
299 if (m_nameToGroupMap->isEmpty())
300 m_nameToGroupMap = nullptr;
301 }
302}
303
304} // namespace
305