1/*
2 * Copyright (C) 2005, 2011 Apple Inc. All rights reserved.
3 * Copyright (C) 2010 Google Inc. All rights reserved.
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB.  If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 *
20 */
21
22#include "config.h"
23#include "core/html/forms/RadioInputType.h"
24
25#include "core/HTMLNames.h"
26#include "core/InputTypeNames.h"
27#include "core/dom/Document.h"
28#include "core/dom/ElementTraversal.h"
29#include "core/events/KeyboardEvent.h"
30#include "core/events/MouseEvent.h"
31#include "core/html/HTMLInputElement.h"
32#include "core/page/SpatialNavigation.h"
33#include "platform/text/PlatformLocale.h"
34#include "wtf/PassOwnPtr.h"
35
36namespace WebCore {
37
38using namespace HTMLNames;
39
40PassRefPtrWillBeRawPtr<InputType> RadioInputType::create(HTMLInputElement& element)
41{
42    return adoptRefWillBeNoop(new RadioInputType(element));
43}
44
45const AtomicString& RadioInputType::formControlType() const
46{
47    return InputTypeNames::radio;
48}
49
50bool RadioInputType::valueMissing(const String&) const
51{
52    return element().isInRequiredRadioButtonGroup() && !element().checkedRadioButtonForGroup();
53}
54
55String RadioInputType::valueMissingText() const
56{
57    return locale().queryString(blink::WebLocalizedString::ValidationValueMissingForRadio);
58}
59
60void RadioInputType::handleClickEvent(MouseEvent* event)
61{
62    event->setDefaultHandled();
63}
64
65void RadioInputType::handleKeydownEvent(KeyboardEvent* event)
66{
67    BaseCheckableInputType::handleKeydownEvent(event);
68    if (event->defaultHandled())
69        return;
70    const String& key = event->keyIdentifier();
71    if (key != "Up" && key != "Down" && key != "Left" && key != "Right")
72        return;
73
74    // Left and up mean "previous radio button".
75    // Right and down mean "next radio button".
76    // Tested in WinIE, and even for RTL, left still means previous radio button
77    // (and so moves to the right). Seems strange, but we'll match it. However,
78    // when using Spatial Navigation, we need to be able to navigate without
79    // changing the selection.
80    Document& document = element().document();
81    if (isSpatialNavigationEnabled(document.frame()))
82        return;
83    bool forward = (key == "Down" || key == "Right");
84
85    // We can only stay within the form's children if the form hasn't been demoted to a leaf because
86    // of malformed HTML.
87    HTMLElement* htmlElement = &element();
88    while ((htmlElement = (forward ? Traversal<HTMLElement>::next(*htmlElement) : Traversal<HTMLElement>::previous(*htmlElement)))) {
89        // Once we encounter a form element, we know we're through.
90        if (isHTMLFormElement(*htmlElement))
91            break;
92        // Look for more radio buttons.
93        if (!isHTMLInputElement(*htmlElement))
94            continue;
95        HTMLInputElement* inputElement = toHTMLInputElement(htmlElement);
96        if (inputElement->form() != element().form())
97            break;
98        if (inputElement->isRadioButton() && inputElement->name() == element().name() && inputElement->isFocusable()) {
99            RefPtrWillBeRawPtr<HTMLInputElement> protector(inputElement);
100            document.setFocusedElement(inputElement);
101            inputElement->dispatchSimulatedClick(event, SendNoEvents);
102            event->setDefaultHandled();
103            return;
104        }
105    }
106}
107
108void RadioInputType::handleKeyupEvent(KeyboardEvent* event)
109{
110    const String& key = event->keyIdentifier();
111    if (key != "U+0020")
112        return;
113    // If an unselected radio is tabbed into (because the entire group has nothing
114    // checked, or because of some explicit .focus() call), then allow space to check it.
115    if (element().checked())
116        return;
117    dispatchSimulatedClickIfActive(event);
118}
119
120bool RadioInputType::isKeyboardFocusable() const
121{
122    if (!InputType::isKeyboardFocusable())
123        return false;
124
125    // When using Spatial Navigation, every radio button should be focusable.
126    if (isSpatialNavigationEnabled(element().document().frame()))
127        return true;
128
129    // Never allow keyboard tabbing to leave you in the same radio group. Always
130    // skip any other elements in the group.
131    Element* currentFocusedElement = element().document().focusedElement();
132    if (isHTMLInputElement(currentFocusedElement)) {
133        HTMLInputElement& focusedInput = toHTMLInputElement(*currentFocusedElement);
134        if (focusedInput.isRadioButton() && focusedInput.form() == element().form() && focusedInput.name() == element().name())
135            return false;
136    }
137
138    // Allow keyboard focus if we're checked or if nothing in the group is checked.
139    return element().checked() || !element().checkedRadioButtonForGroup();
140}
141
142bool RadioInputType::shouldSendChangeEventAfterCheckedChanged()
143{
144    // Don't send a change event for a radio button that's getting unchecked.
145    // This was done to match the behavior of other browsers.
146    return element().checked();
147}
148
149PassOwnPtrWillBeRawPtr<ClickHandlingState> RadioInputType::willDispatchClick()
150{
151    // An event handler can use preventDefault or "return false" to reverse the selection we do here.
152    // The ClickHandlingState object contains what we need to undo what we did here in didDispatchClick.
153
154    // We want radio groups to end up in sane states, i.e., to have something checked.
155    // Therefore if nothing is currently selected, we won't allow the upcoming action to be "undone", since
156    // we want some object in the radio group to actually get selected.
157
158    OwnPtrWillBeRawPtr<ClickHandlingState> state = adoptPtrWillBeNoop(new ClickHandlingState);
159
160    state->checked = element().checked();
161    state->checkedRadioButton = element().checkedRadioButtonForGroup();
162    element().setChecked(true, DispatchChangeEvent);
163
164    return state.release();
165}
166
167void RadioInputType::didDispatchClick(Event* event, const ClickHandlingState& state)
168{
169    if (event->defaultPrevented() || event->defaultHandled()) {
170        // Restore the original selected radio button if possible.
171        // Make sure it is still a radio button and only do the restoration if it still belongs to our group.
172        HTMLInputElement* checkedRadioButton = state.checkedRadioButton.get();
173        if (checkedRadioButton
174            && checkedRadioButton->isRadioButton()
175            && checkedRadioButton->form() == element().form()
176            && checkedRadioButton->name() == element().name()) {
177            checkedRadioButton->setChecked(true);
178        }
179    }
180
181    // The work we did in willDispatchClick was default handling.
182    event->setDefaultHandled();
183}
184
185bool RadioInputType::isRadioButton() const
186{
187    return true;
188}
189
190bool RadioInputType::supportsIndeterminateAppearance() const
191{
192    return false;
193}
194
195} // namespace WebCore
196