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 "RadioInputType.h"
24
25#include "Frame.h"
26#include "HTMLInputElement.h"
27#include "HTMLNames.h"
28#include "InputTypeNames.h"
29#include "KeyboardEvent.h"
30#include "LocalizedStrings.h"
31#include "MouseEvent.h"
32#include "NodeTraversal.h"
33#include "Settings.h"
34#include "SpatialNavigation.h"
35#include <wtf/PassOwnPtr.h>
36
37namespace WebCore {
38
39using namespace HTMLNames;
40
41PassOwnPtr<InputType> RadioInputType::create(HTMLInputElement* element)
42{
43    return adoptPtr(new RadioInputType(element));
44}
45
46const AtomicString& RadioInputType::formControlType() const
47{
48    return InputTypeNames::radio();
49}
50
51bool RadioInputType::valueMissing(const String&) const
52{
53    return element()->isInRequiredRadioButtonGroup() && !element()->checkedRadioButtonForGroup();
54}
55
56String RadioInputType::valueMissingText() const
57{
58    return validationMessageValueMissingForRadioText();
59}
60
61void RadioInputType::handleClickEvent(MouseEvent* event)
62{
63    event->setDefaultHandled();
64}
65
66void RadioInputType::handleKeydownEvent(KeyboardEvent* event)
67{
68    BaseCheckableInputType::handleKeydownEvent(event);
69    if (event->defaultHandled())
70        return;
71    const String& key = event->keyIdentifier();
72    if (key != "Up" && key != "Down" && key != "Left" && key != "Right")
73        return;
74
75    // Left and up mean "previous radio button".
76    // Right and down mean "next radio button".
77    // Tested in WinIE, and even for RTL, left still means previous radio button (and so moves
78    // to the right).  Seems strange, but we'll match it.
79    // However, when using Spatial Navigation, we need to be able to navigate without 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    Node* node = element();
88    while ((node = (forward ? NodeTraversal::next(node) : NodeTraversal::previous(node)))) {
89        // Once we encounter a form element, we know we're through.
90        if (node->hasTagName(formTag))
91            break;
92        // Look for more radio buttons.
93        if (!node->hasTagName(inputTag))
94            continue;
95        RefPtr<HTMLInputElement> inputElement = static_cast<HTMLInputElement*>(node);
96        if (inputElement->form() != element()->form())
97            break;
98        if (inputElement->isRadioButton() && inputElement->name() == element()->name() && inputElement->isFocusable()) {
99            document->setFocusedElement(inputElement);
100            inputElement->dispatchSimulatedClick(event, SendNoEvents, DoNotShowPressedLook);
101            event->setDefaultHandled();
102            return;
103        }
104    }
105}
106
107void RadioInputType::handleKeyupEvent(KeyboardEvent* event)
108{
109    const String& key = event->keyIdentifier();
110    if (key != "U+0020")
111        return;
112    // If an unselected radio is tabbed into (because the entire group has nothing
113    // checked, or because of some explicit .focus() call), then allow space to check it.
114    if (element()->checked())
115        return;
116    dispatchSimulatedClickIfActive(event);
117}
118
119bool RadioInputType::isKeyboardFocusable(KeyboardEvent* event) const
120{
121    if (!InputType::isKeyboardFocusable(event))
122        return false;
123
124    // When using Spatial Navigation, every radio button should be focusable.
125    if (isSpatialNavigationEnabled(element()->document()->frame()))
126        return true;
127
128    // Never allow keyboard tabbing to leave you in the same radio group.  Always
129    // skip any other elements in the group.
130    Element* currentFocusedNode = element()->document()->focusedElement();
131    if (currentFocusedNode && currentFocusedNode->hasTagName(inputTag)) {
132        HTMLInputElement* focusedInput = static_cast<HTMLInputElement*>(currentFocusedNode);
133        if (focusedInput->isRadioButton() && focusedInput->form() == element()->form() && focusedInput->name() == element()->name())
134            return false;
135    }
136
137    // Allow keyboard focus if we're checked or if nothing in the group is checked.
138    return element()->checked() || !element()->checkedRadioButtonForGroup();
139}
140
141bool RadioInputType::shouldSendChangeEventAfterCheckedChanged()
142{
143    // Don't send a change event for a radio button that's getting unchecked.
144    // This was done to match the behavior of other browsers.
145    return element()->checked();
146}
147
148PassOwnPtr<ClickHandlingState> RadioInputType::willDispatchClick()
149{
150    // An event handler can use preventDefault or "return false" to reverse the selection we do here.
151    // The ClickHandlingState object contains what we need to undo what we did here in didDispatchClick.
152
153    // We want radio groups to end up in sane states, i.e., to have something checked.
154    // Therefore if nothing is currently selected, we won't allow the upcoming action to be "undone", since
155    // we want some object in the radio group to actually get selected.
156
157    OwnPtr<ClickHandlingState> state = adoptPtr(new ClickHandlingState);
158
159    state->checked = element()->checked();
160    state->checkedRadioButton = element()->checkedRadioButtonForGroup();
161
162#if PLATFORM(IOS)
163    state->indeterminate = element()->indeterminate();
164
165    if (element()->indeterminate())
166        element()->setIndeterminate(false);
167#endif
168
169    element()->setChecked(true, DispatchChangeEvent);
170
171    return state.release();
172}
173
174void RadioInputType::didDispatchClick(Event* event, const ClickHandlingState& state)
175{
176    if (event->defaultPrevented() || event->defaultHandled()) {
177        // Restore the original selected radio button if possible.
178        // Make sure it is still a radio button and only do the restoration if it still belongs to our group.
179        HTMLInputElement* checkedRadioButton = state.checkedRadioButton.get();
180        if (checkedRadioButton
181                && checkedRadioButton->isRadioButton()
182                && checkedRadioButton->form() == element()->form()
183                && checkedRadioButton->name() == element()->name()) {
184            checkedRadioButton->setChecked(true);
185        }
186
187#if PLATFORM(IOS)
188        element()->setIndeterminate(state.indeterminate);
189#endif
190
191    }
192
193    // The work we did in willDispatchClick was default handling.
194    event->setDefaultHandled();
195}
196
197bool RadioInputType::isRadioButton() const
198{
199    return true;
200}
201
202bool RadioInputType::supportsIndeterminateAppearance() const
203{
204#if PLATFORM(IOS)
205    return true;
206#else
207    return false;
208#endif
209}
210
211} // namespace WebCore
212