/*
* Copyright (C) 1999 Lars Knoll (knoll@kde.org)
* (C) 1999 Antti Koivisto (koivisto@kde.org)
* (C) 2001 Dirk Mueller (mueller@kde.org)
* Copyright (C) 2004, 2005, 2006, 2007, 2008 Apple Inc. All rights reserved.
* (C) 2006 Alexey Proskuryakov (ap@nypop.com)
* Copyright (C) 2007 Samuel Weinig (sam@webkit.org)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Library General Public
* License as published by the Free Software Foundation; either
* version 2 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Library General Public License for more details.
*
* You should have received a copy of the GNU Library General Public License
* along with this library; see the file COPYING.LIB. If not, write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
* Boston, MA 02110-1301, USA.
*
*/
#include "config.h"
#include "HTMLInputElement.h"
#include "AXObjectCache.h"
#include "CSSPropertyNames.h"
#include "ChromeClient.h"
#include "DateComponents.h"
#include "Document.h"
#include "Editor.h"
#include "Event.h"
#include "EventHandler.h"
#include "EventNames.h"
#include "ExceptionCode.h"
#include "File.h"
#include "FileList.h"
#include "FocusController.h"
#include "FormDataList.h"
#include "Frame.h"
#include "HTMLDataListElement.h"
#include "HTMLFormElement.h"
#include "HTMLImageLoader.h"
#include "HTMLNames.h"
#include "HTMLOptionElement.h"
#include "ScriptEventListener.h"
#include "KeyboardEvent.h"
#include "LocalizedStrings.h"
#include "MappedAttribute.h"
#include "MouseEvent.h"
#include "Page.h"
#include "RegularExpression.h"
#include "RenderButton.h"
#include "RenderFileUploadControl.h"
#include "RenderImage.h"
#include "RenderSlider.h"
#include "RenderText.h"
#include "RenderTextControlSingleLine.h"
#include "RenderTheme.h"
#include "StringHash.h"
#include "TextEvent.h"
#ifdef ANDROID_ACCEPT_CHANGES_TO_FOCUSED_TEXTFIELDS
#include "WebViewCore.h"
#endif
#include <wtf/HashMap.h>
#include <wtf/MathExtras.h>
#include <wtf/StdLibExtras.h>
#include <wtf/dtoa.h>
using namespace std;
namespace WebCore {
using namespace HTMLNames;
const int maxSavedResults = 256;
// Constant values for getAllowedValueStep().
static const double dateDefaultStep = 1.0;
static const double dateStepScaleFactor = 86400000.0;
static const double dateTimeDefaultStep = 60.0;
static const double dateTimeStepScaleFactor = 1000.0;
static const double monthDefaultStep = 1.0;
static const double monthStepScaleFactor = 1.0;
static const double numberDefaultStep = 1.0;
static const double numberStepScaleFactor = 1.0;
static const double timeDefaultStep = 60.0;
static const double timeStepScaleFactor = 1000.0;
static const double weekDefaultStep = 1.0;
static const double weekStepScaleFactor = 604800000.0;
// Constant values for minimum().
static const double dateDefaultMinimum = -12219292800000.0; // This means 1582-10-15T00:00Z.
static const double dateTimeDefaultMinimum = -12219292800000.0; // ditto.
static const double monthDefaultMinimum = (1582.0 - 1970) * 12 + 10 - 1; // 1582-10
static const double numberDefaultMinimum = -DBL_MAX;
static const double rangeDefaultMinimum = 0.0;
static const double timeDefaultMinimum = 0.0; // 00:00:00.000
static const double weekDefaultMinimum = -12212380800000.0; // 1583-01-03, the first Monday of 1583.
// Constant values for maximum().
static const double dateDefaultMaximum = DBL_MAX;
static const double dateTimeDefaultMaximum = DBL_MAX;
// DateComponents::m_year can't represent a year greater than INT_MAX.
static const double monthDefaultMaximum = (INT_MAX - 1970) * 12.0 + 12 - 1;
static const double numberDefaultMaximum = DBL_MAX;
static const double rangeDefaultMaximum = 100.0;
static const double timeDefaultMaximum = 86399999.0; // 23:59:59.999
static const double weekDefaultMaximum = DBL_MAX;
static const double defaultStepBase = 0.0;
static const double weekDefaultStepBase = -259200000.0; // The first day of 1970-W01.
static const double msecPerMinute = 60 * 1000;
static const double msecPerSecond = 1000;
HTMLInputElement::HTMLInputElement(const QualifiedName& tagName, Document* doc, HTMLFormElement* f)
: HTMLTextFormControlElement(tagName, doc, f)
, m_xPos(0)
, m_yPos(0)
, m_maxResults(-1)
, m_type(TEXT)
, m_checked(false)
, m_defaultChecked(false)
, m_useDefaultChecked(true)
, m_indeterminate(false)
, m_haveType(false)
, m_activeSubmit(false)
, m_autocomplete(Uninitialized)
, m_autofilled(false)
, m_inited(false)
{
ASSERT(hasTagName(inputTag) || hasTagName(isindexTag));
}
HTMLInputElement::~HTMLInputElement()
{
if (needsActivationCallback())
document()->unregisterForDocumentActivationCallbacks(this);
document()->checkedRadioButtons().removeButton(this);
// Need to remove this from the form while it is still an HTMLInputElement,
// so can't wait for the base class's destructor to do it.
removeFromForm();
}
const AtomicString& HTMLInputElement::formControlName() const
{
return m_data.name();
}
bool HTMLInputElement::autoComplete() const
{
if (m_autocomplete != Uninitialized)
return m_autocomplete == On;
// Assuming we're still in a Form, respect the Form's setting
if (HTMLFormElement* form = this->form())
return form->autoComplete();
// The default is true
return true;
}
bool HTMLInputElement::valueMissing() const
{
if (!isRequiredFormControl() || readOnly() || disabled())
return false;
switch (inputType()) {
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case FILE:
case MONTH:
case NUMBER:
case PASSWORD:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
return value().isEmpty();
case CHECKBOX:
return !checked();
case RADIO:
return !document()->checkedRadioButtons().checkedButtonForGroup(name());
case COLOR:
return false;
case BUTTON:
case HIDDEN:
case IMAGE:
case ISINDEX:
case RANGE:
case RESET:
case SUBMIT:
break;
}
ASSERT_NOT_REACHED();
return false;
}
bool HTMLInputElement::patternMismatch() const
{
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case MONTH:
case NUMBER:
case RADIO:
case RANGE:
case RESET:
case SUBMIT:
case TIME:
case WEEK:
return false;
case EMAIL:
case PASSWORD:
case SEARCH:
case TELEPHONE:
case TEXT:
case URL:
const AtomicString& pattern = getAttribute(patternAttr);
String value = this->value();
// Empty values can't be mismatched
if (pattern.isEmpty() || value.isEmpty())
return false;
RegularExpression patternRegExp(pattern, TextCaseSensitive);
int matchLength = 0;
int valueLength = value.length();
int matchOffset = patternRegExp.match(value, 0, &matchLength);
return matchOffset != 0 || matchLength != valueLength;
}
ASSERT_NOT_REACHED();
return false;
}
bool HTMLInputElement::tooLong() const
{
switch (inputType()) {
case EMAIL:
case PASSWORD:
case SEARCH:
case TELEPHONE:
case TEXT:
case URL: {
int max = maxLength();
if (max < 0)
return false;
// Return false for the default value even if it is longer than maxLength.
bool userEdited = !m_data.value().isNull();
if (!userEdited)
return false;
return value().numGraphemeClusters() > static_cast<unsigned>(max);
}
case BUTTON:
case CHECKBOX:
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case MONTH:
case NUMBER:
case RADIO:
case RANGE:
case RESET:
case SUBMIT:
case TIME:
case WEEK:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
bool HTMLInputElement::rangeUnderflow() const
{
const double nan = numeric_limits<double>::quiet_NaN();
switch (inputType()) {
case DATE:
case DATETIME:
case DATETIMELOCAL:
case MONTH:
case NUMBER:
case RANGE:
case TIME:
case WEEK: {
double doubleValue = parseToDouble(value(), nan);
return isfinite(doubleValue) && doubleValue < minimum();
}
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
break;
}
return false;
}
bool HTMLInputElement::rangeOverflow() const
{
const double nan = numeric_limits<double>::quiet_NaN();
switch (inputType()) {
case DATE:
case DATETIME:
case DATETIMELOCAL:
case MONTH:
case NUMBER:
case RANGE:
case TIME:
case WEEK: {
double doubleValue = parseToDouble(value(), nan);
return isfinite(doubleValue) && doubleValue > maximum();
}
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
break;
}
return false;
}
double HTMLInputElement::minimum() const
{
switch (inputType()) {
case DATE:
return parseToDouble(getAttribute(minAttr), dateDefaultMinimum);
case DATETIME:
case DATETIMELOCAL:
return parseToDouble(getAttribute(minAttr), dateTimeDefaultMinimum);
case MONTH:
return parseToDouble(getAttribute(minAttr), monthDefaultMinimum);
case NUMBER:
return parseToDouble(getAttribute(minAttr), numberDefaultMinimum);
case RANGE:
return parseToDouble(getAttribute(minAttr), rangeDefaultMinimum);
case TIME:
return parseToDouble(getAttribute(minAttr), timeDefaultMinimum);
case WEEK:
return parseToDouble(getAttribute(minAttr), weekDefaultMinimum);
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
break;
}
ASSERT_NOT_REACHED();
return 0;
}
double HTMLInputElement::maximum() const
{
switch (inputType()) {
case DATE:
return parseToDouble(getAttribute(maxAttr), dateDefaultMaximum);
case DATETIME:
case DATETIMELOCAL:
return parseToDouble(getAttribute(maxAttr), dateTimeDefaultMaximum);
case MONTH:
return parseToDouble(getAttribute(maxAttr), monthDefaultMaximum);
case NUMBER:
return parseToDouble(getAttribute(maxAttr), numberDefaultMaximum);
case RANGE: {
double max = parseToDouble(getAttribute(maxAttr), rangeDefaultMaximum);
// A remedy for the inconsistent min/max values for RANGE.
// Sets the maximum to the default or the minimum value.
double min = minimum();
if (max < min)
max = std::max(min, rangeDefaultMaximum);
return max;
}
case TIME:
return parseToDouble(getAttribute(maxAttr), timeDefaultMaximum);
case WEEK:
return parseToDouble(getAttribute(maxAttr), weekDefaultMaximum);
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
break;
}
ASSERT_NOT_REACHED();
return 0;
}
double HTMLInputElement::stepBase() const
{
switch (inputType()) {
case RANGE:
return minimum();
case DATE:
case DATETIME:
case DATETIMELOCAL:
case MONTH:
case NUMBER:
case TIME:
return parseToDouble(getAttribute(minAttr), defaultStepBase);
case WEEK:
return parseToDouble(getAttribute(minAttr), weekDefaultStepBase);
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
break;
}
ASSERT_NOT_REACHED();
return 0.0;
}
bool HTMLInputElement::stepMismatch() const
{
double step;
if (!getAllowedValueStep(&step))
return false;
switch (inputType()) {
case RANGE:
// stepMismatch doesn't occur for RANGE. RenderSlider guarantees the
// value matches to step.
return false;
case NUMBER: {
double doubleValue;
if (!formStringToDouble(value(), &doubleValue))
return false;
doubleValue = fabs(doubleValue - stepBase());
if (isinf(doubleValue))
return false;
// double's fractional part size is DBL_MAN_DIG-bit. If the current
// value is greater than step*2^DBL_MANT_DIG, the following fmod() makes
// no sense.
if (doubleValue / pow(2.0, DBL_MANT_DIG) > step)
return false;
double remainder = fmod(doubleValue, step);
// Accepts errors in lower 7-bit.
double acceptableError = step / pow(2.0, DBL_MANT_DIG - 7);
return acceptableError < remainder && remainder < (step - acceptableError);
}
case DATE:
case DATETIME:
case DATETIMELOCAL:
case MONTH:
case TIME:
case WEEK: {
const double nan = numeric_limits<double>::quiet_NaN();
double doubleValue = parseToDouble(value(), nan);
doubleValue = fabs(doubleValue - stepBase());
if (!isfinite(doubleValue))
return false;
ASSERT(round(doubleValue) == doubleValue);
ASSERT(round(step) == step);
return fmod(doubleValue, step);
}
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
break;
}
// Non-supported types should be rejected by getAllowedValueStep().
ASSERT_NOT_REACHED();
return false;
}
bool HTMLInputElement::getStepParameters(double* defaultStep, double* stepScaleFactor) const
{
ASSERT(defaultStep);
ASSERT(stepScaleFactor);
switch (inputType()) {
case NUMBER:
case RANGE:
*defaultStep = numberDefaultStep;
*stepScaleFactor = numberStepScaleFactor;
return true;
case DATE:
*defaultStep = dateDefaultStep;
*stepScaleFactor = dateStepScaleFactor;
return true;
case DATETIME:
case DATETIMELOCAL:
*defaultStep = dateTimeDefaultStep;
*stepScaleFactor = dateTimeStepScaleFactor;
return true;
case MONTH:
*defaultStep = monthDefaultStep;
*stepScaleFactor = monthStepScaleFactor;
return true;
case TIME:
*defaultStep = timeDefaultStep;
*stepScaleFactor = timeStepScaleFactor;
return true;
case WEEK:
*defaultStep = weekDefaultStep;
*stepScaleFactor = weekStepScaleFactor;
return true;
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
bool HTMLInputElement::getAllowedValueStep(double* step) const
{
ASSERT(step);
double defaultStep;
double stepScaleFactor;
if (!getStepParameters(&defaultStep, &stepScaleFactor))
return false;
const AtomicString& stepString = getAttribute(stepAttr);
if (stepString.isEmpty()) {
*step = defaultStep * stepScaleFactor;
return true;
}
if (equalIgnoringCase(stepString, "any"))
return false;
double parsed;
if (!formStringToDouble(stepString, &parsed) || parsed <= 0.0) {
*step = defaultStep * stepScaleFactor;
return true;
}
// For DATE, MONTH, WEEK, the parsed value should be an integer.
if (inputType() == DATE || inputType() == MONTH || inputType() == WEEK)
parsed = max(round(parsed), 1.0);
double result = parsed * stepScaleFactor;
// For DATETIME, DATETIMELOCAL, TIME, the result should be an integer.
if (inputType() == DATETIME || inputType() == DATETIMELOCAL || inputType() == TIME)
result = max(round(result), 1.0);
ASSERT(result > 0);
*step = result;
return true;
}
void HTMLInputElement::applyStep(double count, ExceptionCode& ec)
{
double step;
if (!getAllowedValueStep(&step)) {
ec = INVALID_STATE_ERR;
return;
}
const double nan = numeric_limits<double>::quiet_NaN();
double current = parseToDouble(value(), nan);
if (!isfinite(current)) {
ec = INVALID_STATE_ERR;
return;
}
double newValue = current + step * count;
if (isinf(newValue)) {
ec = INVALID_STATE_ERR;
return;
}
if (newValue < minimum()) {
ec = INVALID_STATE_ERR;
return;
}
double base = stepBase();
newValue = base + round((newValue - base) / step) * step;
if (newValue > maximum()) {
ec = INVALID_STATE_ERR;
return;
}
setValueAsNumber(newValue, ec);
}
void HTMLInputElement::stepUp(int n, ExceptionCode& ec)
{
applyStep(n, ec);
}
void HTMLInputElement::stepDown(int n, ExceptionCode& ec)
{
applyStep(-n, ec);
}
static inline CheckedRadioButtons& checkedRadioButtons(const HTMLInputElement *element)
{
if (HTMLFormElement* form = element->form())
return form->checkedRadioButtons();
return element->document()->checkedRadioButtons();
}
bool HTMLInputElement::isKeyboardFocusable(KeyboardEvent* event) const
{
// If text fields can be focused, then they should always be keyboard focusable
if (isTextField())
return HTMLFormControlElementWithState::isFocusable();
// If the base class says we can't be focused, then we can stop now.
if (!HTMLFormControlElementWithState::isKeyboardFocusable(event))
return false;
if (inputType() == RADIO) {
// Never allow keyboard tabbing to leave you in the same radio group. Always
// skip any other elements in the group.
Node* currentFocusedNode = document()->focusedNode();
if (currentFocusedNode && currentFocusedNode->hasTagName(inputTag)) {
HTMLInputElement* focusedInput = static_cast<HTMLInputElement*>(currentFocusedNode);
if (focusedInput->inputType() == RADIO && focusedInput->form() == form() &&
focusedInput->name() == name())
return false;
}
// Allow keyboard focus if we're checked or if nothing in the group is checked.
return checked() || !checkedRadioButtons(this).checkedButtonForGroup(name());
}
return true;
}
bool HTMLInputElement::isMouseFocusable() const
{
if (isTextField())
return HTMLFormControlElementWithState::isFocusable();
return HTMLFormControlElementWithState::isMouseFocusable();
}
void HTMLInputElement::updateFocusAppearance(bool restorePreviousSelection)
{
if (isTextField())
InputElement::updateFocusAppearance(m_data, this, this, restorePreviousSelection);
else
HTMLFormControlElementWithState::updateFocusAppearance(restorePreviousSelection);
}
void HTMLInputElement::aboutToUnload()
{
InputElement::aboutToUnload(this, this);
}
bool HTMLInputElement::shouldUseInputMethod() const
{
return m_type == TEXT || m_type == SEARCH || m_type == ISINDEX;
}
void HTMLInputElement::handleFocusEvent()
{
InputElement::dispatchFocusEvent(this, this);
if (isTextField())
m_autofilled = false;
}
void HTMLInputElement::handleBlurEvent()
{
InputElement::dispatchBlurEvent(this, this);
}
void HTMLInputElement::setType(const String& t)
{
if (t.isEmpty()) {
int exccode;
removeAttribute(typeAttr, exccode);
} else
setAttribute(typeAttr, t);
}
typedef HashMap<String, HTMLInputElement::InputType, CaseFoldingHash> InputTypeMap;
static const InputTypeMap* createTypeMap()
{
InputTypeMap* map = new InputTypeMap;
map->add("button", HTMLInputElement::BUTTON);
map->add("checkbox", HTMLInputElement::CHECKBOX);
map->add("color", HTMLInputElement::COLOR);
map->add("date", HTMLInputElement::DATE);
map->add("datetime", HTMLInputElement::DATETIME);
map->add("datetime-local", HTMLInputElement::DATETIMELOCAL);
map->add("email", HTMLInputElement::EMAIL);
map->add("file", HTMLInputElement::FILE);
map->add("hidden", HTMLInputElement::HIDDEN);
map->add("image", HTMLInputElement::IMAGE);
map->add("khtml_isindex", HTMLInputElement::ISINDEX);
map->add("month", HTMLInputElement::MONTH);
map->add("number", HTMLInputElement::NUMBER);
map->add("password", HTMLInputElement::PASSWORD);
map->add("radio", HTMLInputElement::RADIO);
map->add("range", HTMLInputElement::RANGE);
map->add("reset", HTMLInputElement::RESET);
map->add("search", HTMLInputElement::SEARCH);
map->add("submit", HTMLInputElement::SUBMIT);
map->add("tel", HTMLInputElement::TELEPHONE);
map->add("time", HTMLInputElement::TIME);
map->add("url", HTMLInputElement::URL);
map->add("week", HTMLInputElement::WEEK);
// No need to register "text" because it is the default type.
return map;
}
void HTMLInputElement::setInputType(const String& t)
{
static const InputTypeMap* typeMap = createTypeMap();
InputType newType = t.isNull() ? TEXT : typeMap->get(t);
#ifdef ANDROID_ACCEPT_CHANGES_TO_FOCUSED_TEXTFIELDS
if (newType == PASSWORD && document()->focusedNode() == this)
android::WebViewCore::getWebViewCore(document()->view())->updateTextfield(this, true, String());
#endif
// IMPORTANT: Don't allow the type to be changed to FILE after the first
// type change, otherwise a JavaScript programmer would be able to set a text
// field's value to something like /etc/passwd and then change it to a file field.
if (inputType() != newType) {
bool oldWillValidate = willValidate();
if (newType == FILE && m_haveType)
// Set the attribute back to the old value.
// Useful in case we were called from inside parseMappedAttribute.
setAttribute(typeAttr, type());
else {
checkedRadioButtons(this).removeButton(this);
if (newType == FILE && !m_fileList)
m_fileList = FileList::create();
bool wasAttached = attached();
if (wasAttached)
detach();
bool didStoreValue = storesValueSeparateFromAttribute();
bool wasPasswordField = inputType() == PASSWORD;
bool didRespectHeightAndWidth = respectHeightAndWidthAttrs();
m_type = newType;
bool willStoreValue = storesValueSeparateFromAttribute();
bool isPasswordField = inputType() == PASSWORD;
bool willRespectHeightAndWidth = respectHeightAndWidthAttrs();
if (didStoreValue && !willStoreValue && !m_data.value().isNull()) {
setAttribute(valueAttr, m_data.value());
m_data.setValue(String());
}
if (!didStoreValue && willStoreValue)
m_data.setValue(sanitizeValue(getAttribute(valueAttr)));
else
InputElement::updateValueIfNeeded(m_data, this);
if (wasPasswordField && !isPasswordField)
unregisterForActivationCallbackIfNeeded();
else if (!wasPasswordField && isPasswordField)
registerForActivationCallbackIfNeeded();
if (didRespectHeightAndWidth != willRespectHeightAndWidth) {
NamedMappedAttrMap* map = mappedAttributes();
ASSERT(map);
if (Attribute* height = map->getAttributeItem(heightAttr))
attributeChanged(height, false);
if (Attribute* width = map->getAttributeItem(widthAttr))
attributeChanged(width, false);
if (Attribute* align = map->getAttributeItem(alignAttr))
attributeChanged(align, false);
}
if (wasAttached) {
attach();
if (document()->focusedNode() == this)
updateFocusAppearance(true);
}
checkedRadioButtons(this).addButton(this);
}
setNeedsValidityCheck();
if (oldWillValidate != willValidate())
setNeedsWillValidateCheck();
InputElement::notifyFormStateChanged(this);
}
m_haveType = true;
if (inputType() != IMAGE && m_imageLoader)
m_imageLoader.clear();
}
static const AtomicString* createFormControlTypes()
{
AtomicString* types = new AtomicString[HTMLInputElement::numberOfTypes];
// The values must be lowercased because they will be the return values of
// input.type and it must be lowercase according to DOM Level 2.
types[HTMLInputElement::BUTTON] = "button";
types[HTMLInputElement::CHECKBOX] = "checkbox";
types[HTMLInputElement::COLOR] = "color";
types[HTMLInputElement::DATE] = "date";
types[HTMLInputElement::DATETIME] = "datetime";
types[HTMLInputElement::DATETIMELOCAL] = "datetime-local";
types[HTMLInputElement::EMAIL] = "email";
types[HTMLInputElement::FILE] = "file";
types[HTMLInputElement::HIDDEN] = "hidden";
types[HTMLInputElement::IMAGE] = "image";
types[HTMLInputElement::ISINDEX] = emptyAtom;
types[HTMLInputElement::MONTH] = "month";
types[HTMLInputElement::NUMBER] = "number";
types[HTMLInputElement::PASSWORD] = "password";
types[HTMLInputElement::RADIO] = "radio";
types[HTMLInputElement::RANGE] = "range";
types[HTMLInputElement::RESET] = "reset";
types[HTMLInputElement::SEARCH] = "search";
types[HTMLInputElement::SUBMIT] = "submit";
types[HTMLInputElement::TELEPHONE] = "tel";
types[HTMLInputElement::TEXT] = "text";
types[HTMLInputElement::TIME] = "time";
types[HTMLInputElement::URL] = "url";
types[HTMLInputElement::WEEK] = "week";
return types;
}
const AtomicString& HTMLInputElement::formControlType() const
{
static const AtomicString* formControlTypes = createFormControlTypes();
return formControlTypes[inputType()];
}
bool HTMLInputElement::saveFormControlState(String& result) const
{
if (!autoComplete())
return false;
switch (inputType()) {
case BUTTON:
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case MONTH:
case NUMBER:
case RANGE:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
result = value();
return true;
case CHECKBOX:
case RADIO:
result = checked() ? "on" : "off";
return true;
case PASSWORD:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
void HTMLInputElement::restoreFormControlState(const String& state)
{
ASSERT(inputType() != PASSWORD); // should never save/restore password fields
switch (inputType()) {
case BUTTON:
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case MONTH:
case NUMBER:
case RANGE:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
setValue(state);
break;
case CHECKBOX:
case RADIO:
setChecked(state == "on");
break;
case PASSWORD:
break;
}
}
bool HTMLInputElement::canStartSelection() const
{
if (!isTextField())
return false;
return HTMLFormControlElementWithState::canStartSelection();
}
bool HTMLInputElement::canHaveSelection() const
{
return isTextField();
}
void HTMLInputElement::accessKeyAction(bool sendToAnyElement)
{
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case FILE:
case IMAGE:
case RADIO:
case RANGE:
case RESET:
case SUBMIT:
focus(false);
// send the mouse button events iff the caller specified sendToAnyElement
dispatchSimulatedClick(0, sendToAnyElement);
break;
case HIDDEN:
// a no-op for this type
break;
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
// should never restore previous selection here
focus(false);
break;
}
}
bool HTMLInputElement::mapToEntry(const QualifiedName& attrName, MappedAttributeEntry& result) const
{
if (((attrName == heightAttr || attrName == widthAttr) && respectHeightAndWidthAttrs()) ||
attrName == vspaceAttr ||
attrName == hspaceAttr) {
result = eUniversal;
return false;
}
if (attrName == alignAttr) {
if (inputType() == IMAGE) {
// Share with <img> since the alignment behavior is the same.
result = eReplaced;
return false;
}
}
return HTMLElement::mapToEntry(attrName, result);
}
void HTMLInputElement::parseMappedAttribute(MappedAttribute *attr)
{
if (attr->name() == nameAttr) {
checkedRadioButtons(this).removeButton(this);
m_data.setName(attr->value());
checkedRadioButtons(this).addButton(this);
HTMLFormControlElementWithState::parseMappedAttribute(attr);
} else if (attr->name() == autocompleteAttr) {
if (equalIgnoringCase(attr->value(), "off")) {
m_autocomplete = Off;
registerForActivationCallbackIfNeeded();
} else {
bool needsToUnregister = m_autocomplete == Off;
if (attr->isEmpty())
m_autocomplete = Uninitialized;
else
m_autocomplete = On;
if (needsToUnregister)
unregisterForActivationCallbackIfNeeded();
}
} else if (attr->name() == typeAttr) {
setInputType(attr->value());
} else if (attr->name() == valueAttr) {
// We only need to setChanged if the form is looking at the default value right now.
if (m_data.value().isNull())
setNeedsStyleRecalc();
setFormControlValueMatchesRenderer(false);
setNeedsValidityCheck();
} else if (attr->name() == checkedAttr) {
m_defaultChecked = !attr->isNull();
if (m_useDefaultChecked) {
setChecked(m_defaultChecked);
m_useDefaultChecked = true;
}
setNeedsValidityCheck();
} else if (attr->name() == maxlengthAttr) {
InputElement::parseMaxLengthAttribute(m_data, this, this, attr);
setNeedsValidityCheck();
} else if (attr->name() == sizeAttr)
InputElement::parseSizeAttribute(m_data, this, attr);
else if (attr->name() == altAttr) {
if (renderer() && inputType() == IMAGE)
toRenderImage(renderer())->updateAltText();
} else if (attr->name() == srcAttr) {
if (renderer() && inputType() == IMAGE) {
if (!m_imageLoader)
m_imageLoader.set(new HTMLImageLoader(this));
m_imageLoader->updateFromElementIgnoringPreviousError();
}
} else if (attr->name() == usemapAttr ||
attr->name() == accesskeyAttr) {
// FIXME: ignore for the moment
} else if (attr->name() == vspaceAttr) {
addCSSLength(attr, CSSPropertyMarginTop, attr->value());
addCSSLength(attr, CSSPropertyMarginBottom, attr->value());
} else if (attr->name() == hspaceAttr) {
addCSSLength(attr, CSSPropertyMarginLeft, attr->value());
addCSSLength(attr, CSSPropertyMarginRight, attr->value());
} else if (attr->name() == alignAttr) {
if (inputType() == IMAGE)
addHTMLAlignment(attr);
} else if (attr->name() == widthAttr) {
if (respectHeightAndWidthAttrs())
addCSSLength(attr, CSSPropertyWidth, attr->value());
} else if (attr->name() == heightAttr) {
if (respectHeightAndWidthAttrs())
addCSSLength(attr, CSSPropertyHeight, attr->value());
}
// Search field and slider attributes all just cause updateFromElement to be called through style
// recalcing.
else if (attr->name() == onsearchAttr) {
setAttributeEventListener(eventNames().searchEvent, createAttributeEventListener(this, attr));
} else if (attr->name() == resultsAttr) {
int oldResults = m_maxResults;
m_maxResults = !attr->isNull() ? std::min(attr->value().toInt(), maxSavedResults) : -1;
// FIXME: Detaching just for maxResults change is not ideal. We should figure out the right
// time to relayout for this change.
if (m_maxResults != oldResults && (m_maxResults <= 0 || oldResults <= 0) && attached()) {
detach();
attach();
}
setNeedsStyleRecalc();
} else if (attr->name() == autosaveAttr
|| attr->name() == incrementalAttr)
setNeedsStyleRecalc();
else if (attr->name() == minAttr
|| attr->name() == maxAttr
|| attr->name() == multipleAttr
|| attr->name() == patternAttr
|| attr->name() == precisionAttr
|| attr->name() == stepAttr)
setNeedsValidityCheck();
#if ENABLE(DATALIST)
else if (attr->name() == listAttr)
m_hasNonEmptyList = !attr->isEmpty();
// FIXME: we need to tell this change to a renderer if the attribute affects the appearance.
#endif
else
HTMLTextFormControlElement::parseMappedAttribute(attr);
}
bool HTMLInputElement::rendererIsNeeded(RenderStyle *style)
{
if (inputType() == HIDDEN)
return false;
return HTMLFormControlElementWithState::rendererIsNeeded(style);
}
RenderObject *HTMLInputElement::createRenderer(RenderArena *arena, RenderStyle *style)
{
switch (inputType()) {
case BUTTON:
case RESET:
case SUBMIT:
return new (arena) RenderButton(this);
case CHECKBOX:
case RADIO:
return RenderObject::createObject(this, style);
case FILE:
return new (arena) RenderFileUploadControl(this);
case HIDDEN:
break;
case IMAGE:
return new (arena) RenderImage(this);
case RANGE:
return new (arena) RenderSlider(this);
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
return new (arena) RenderTextControlSingleLine(this, placeholderShouldBeVisible());
}
ASSERT(false);
return 0;
}
void HTMLInputElement::attach()
{
if (!m_inited) {
if (!m_haveType)
setInputType(getAttribute(typeAttr));
m_inited = true;
}
HTMLFormControlElementWithState::attach();
if (inputType() == IMAGE) {
if (!m_imageLoader)
m_imageLoader.set(new HTMLImageLoader(this));
m_imageLoader->updateFromElement();
if (renderer() && m_imageLoader->haveFiredBeforeLoadEvent()) {
RenderImage* imageObj = toRenderImage(renderer());
imageObj->setCachedImage(m_imageLoader->image());
// If we have no image at all because we have no src attribute, set
// image height and width for the alt text instead.
if (!m_imageLoader->image() && !imageObj->cachedImage())
imageObj->setImageSizeForAltText();
}
}
if (document()->focusedNode() == this)
document()->updateFocusAppearanceSoon(true /* restore selection */);
}
void HTMLInputElement::detach()
{
HTMLFormControlElementWithState::detach();
setFormControlValueMatchesRenderer(false);
}
String HTMLInputElement::altText() const
{
// http://www.w3.org/TR/1998/REC-html40-19980424/appendix/notes.html#altgen
// also heavily discussed by Hixie on bugzilla
// note this is intentionally different to HTMLImageElement::altText()
String alt = getAttribute(altAttr);
// fall back to title attribute
if (alt.isNull())
alt = getAttribute(titleAttr);
if (alt.isNull())
alt = getAttribute(valueAttr);
if (alt.isEmpty())
alt = inputElementAltText();
return alt;
}
bool HTMLInputElement::isSuccessfulSubmitButton() const
{
// HTML spec says that buttons must have names to be considered successful.
// However, other browsers do not impose this constraint. So we do likewise.
return !disabled() && (inputType() == IMAGE || inputType() == SUBMIT);
}
bool HTMLInputElement::isActivatedSubmit() const
{
return m_activeSubmit;
}
void HTMLInputElement::setActivatedSubmit(bool flag)
{
m_activeSubmit = flag;
}
bool HTMLInputElement::appendFormData(FormDataList& encoding, bool multipart)
{
// image generates its own names, but for other types there is no form data unless there's a name
if (name().isEmpty() && inputType() != IMAGE)
return false;
switch (inputType()) {
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case HIDDEN:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case RANGE:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
// always successful
encoding.appendData(name(), value());
return true;
case CHECKBOX:
case RADIO:
if (checked()) {
encoding.appendData(name(), value());
return true;
}
break;
case BUTTON:
case RESET:
// these types of buttons are never successful
return false;
case IMAGE:
if (m_activeSubmit) {
encoding.appendData(name().isEmpty() ? "x" : (name() + ".x"), m_xPos);
encoding.appendData(name().isEmpty() ? "y" : (name() + ".y"), m_yPos);
if (!name().isEmpty() && !value().isEmpty())
encoding.appendData(name(), value());
return true;
}
break;
case SUBMIT:
if (m_activeSubmit) {
String enc_str = valueWithDefault();
encoding.appendData(name(), enc_str);
return true;
}
break;
case FILE: {
unsigned numFiles = m_fileList->length();
if (!multipart) {
// Send only the basenames.
// 4.10.16.4 and 4.10.16.6 sections in HTML5.
// Unlike the multipart case, we have no special
// handling for the empty fileList because Netscape
// doesn't support for non-multipart submission of
// file inputs, and Firefox doesn't add "name=" query
// parameter.
for (unsigned i = 0; i < numFiles; ++i) {
encoding.appendData(name(), m_fileList->item(i)->fileName());
}
return true;
}
// If no filename at all is entered, return successful but empty.
// Null would be more logical, but Netscape posts an empty file. Argh.
if (!numFiles) {
encoding.appendFile(name(), File::create(""));
return true;
}
for (unsigned i = 0; i < numFiles; ++i)
encoding.appendFile(name(), m_fileList->item(i));
return true;
}
}
return false;
}
void HTMLInputElement::reset()
{
if (storesValueSeparateFromAttribute())
setValue(String());
setChecked(m_defaultChecked);
m_useDefaultChecked = true;
}
bool HTMLInputElement::isTextField() const
{
switch (inputType()) {
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
return true;
case BUTTON:
case CHECKBOX:
case FILE:
case HIDDEN:
case IMAGE:
case RADIO:
case RANGE:
case RESET:
case SUBMIT:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
void HTMLInputElement::setChecked(bool nowChecked, bool sendChangeEvent)
{
if (checked() == nowChecked)
return;
checkedRadioButtons(this).removeButton(this);
m_useDefaultChecked = false;
m_checked = nowChecked;
setNeedsStyleRecalc();
checkedRadioButtons(this).addButton(this);
if (renderer() && renderer()->style()->hasAppearance())
renderer()->theme()->stateChanged(renderer(), CheckedState);
// Ideally we'd do this from the render tree (matching
// RenderTextView), but it's not possible to do it at the moment
// because of the way the code is structured.
if (renderer() && AXObjectCache::accessibilityEnabled())
renderer()->document()->axObjectCache()->postNotification(renderer(), AXObjectCache::AXCheckedStateChanged, true);
// Only send a change event for items in the document (avoid firing during
// parsing) and don't send a change event for a radio button that's getting
// unchecked to match other browsers. DOM is not a useful standard for this
// because it says only to fire change events at "lose focus" time, which is
// definitely wrong in practice for these types of elements.
if (sendChangeEvent && inDocument() && (inputType() != RADIO || nowChecked))
dispatchFormControlChangeEvent();
}
void HTMLInputElement::setIndeterminate(bool _indeterminate)
{
// Only checkboxes honor indeterminate.
if (inputType() != CHECKBOX || indeterminate() == _indeterminate)
return;
m_indeterminate = _indeterminate;
setNeedsStyleRecalc();
if (renderer() && renderer()->style()->hasAppearance())
renderer()->theme()->stateChanged(renderer(), CheckedState);
}
int HTMLInputElement::size() const
{
return m_data.size();
}
void HTMLInputElement::copyNonAttributeProperties(const Element* source)
{
const HTMLInputElement* sourceElement = static_cast<const HTMLInputElement*>(source);
m_data.setValue(sourceElement->m_data.value());
setChecked(sourceElement->m_checked);
m_defaultChecked = sourceElement->m_defaultChecked;
m_useDefaultChecked = sourceElement->m_useDefaultChecked;
m_indeterminate = sourceElement->m_indeterminate;
HTMLFormControlElementWithState::copyNonAttributeProperties(source);
}
String HTMLInputElement::value() const
{
// The HTML5 spec (as of the 10/24/08 working draft) says that the value attribute isn't applicable to the file upload control
// but we don't want to break existing websites, who may be relying on being able to get the file name as a value.
if (inputType() == FILE) {
if (!m_fileList->isEmpty())
return m_fileList->item(0)->fileName();
return String();
}
String value = m_data.value();
if (value.isNull()) {
value = sanitizeValue(getAttribute(valueAttr));
// If no attribute exists, then just use "on" or "" based off the checked() state of the control.
if (value.isNull() && (inputType() == CHECKBOX || inputType() == RADIO))
return checked() ? "on" : "";
}
return value;
}
String HTMLInputElement::valueWithDefault() const
{
String v = value();
if (v.isNull()) {
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case RADIO:
case RANGE:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
break;
case RESET:
v = resetButtonDefaultLabel();
break;
case SUBMIT:
v = submitButtonDefaultLabel();
break;
}
}
return v;
}
void HTMLInputElement::setValueForUser(const String& value)
{
// Call setValue and make it send a change event.
setValue(value, true);
}
const String& HTMLInputElement::suggestedValue() const
{
return m_data.suggestedValue();
}
void HTMLInputElement::setSuggestedValue(const String& value)
{
if (inputType() != TEXT)
return;
setFormControlValueMatchesRenderer(false);
m_data.setSuggestedValue(sanitizeValue(value));
updatePlaceholderVisibility(false);
if (renderer())
renderer()->updateFromElement();
setNeedsStyleRecalc();
}
void HTMLInputElement::setValue(const String& value, bool sendChangeEvent)
{
// For security reasons, we don't allow setting the filename, but we do allow clearing it.
// The HTML5 spec (as of the 10/24/08 working draft) says that the value attribute isn't applicable to the file upload control
// but we don't want to break existing websites, who may be relying on this method to clear things.
if (inputType() == FILE && !value.isEmpty())
return;
setFormControlValueMatchesRenderer(false);
if (storesValueSeparateFromAttribute()) {
if (inputType() == FILE)
m_fileList->clear();
else {
m_data.setValue(sanitizeValue(value));
if (isTextField()) {
updatePlaceholderVisibility(false);
if (inDocument())
document()->updateStyleIfNeeded();
}
}
if (renderer())
renderer()->updateFromElement();
setNeedsStyleRecalc();
} else
setAttribute(valueAttr, sanitizeValue(value));
if (isTextField()) {
unsigned max = m_data.value().length();
#ifdef ANDROID_ACCEPT_CHANGES_TO_FOCUSED_TEXTFIELDS
// Make sure our UI side textfield changes to match the RenderTextControl
android::WebViewCore::getWebViewCore(document()->view())->updateTextfield(this, false, value);
#endif
if (document()->focusedNode() == this)
InputElement::updateSelectionRange(this, this, max, max);
else
cacheSelection(max, max);
m_data.setSuggestedValue(String());
}
// Don't dispatch the change event when focused, it will be dispatched
// when the control loses focus.
if (sendChangeEvent && document()->focusedNode() != this)
dispatchFormControlChangeEvent();
InputElement::notifyFormStateChanged(this);
setNeedsValidityCheck();
}
double HTMLInputElement::parseToDouble(const String& src, double defaultValue) const
{
switch (inputType()) {
case DATE:
case DATETIME:
case DATETIMELOCAL:
case TIME:
case WEEK: {
DateComponents date;
if (!formStringToDateComponents(inputType(), src, &date))
return defaultValue;
double msec = date.millisecondsSinceEpoch();
ASSERT(isfinite(msec));
return msec;
}
case MONTH: {
DateComponents date;
if (!formStringToDateComponents(inputType(), src, &date))
return defaultValue;
double months = date.monthsSinceEpoch();
ASSERT(isfinite(months));
return months;
}
case NUMBER:
case RANGE: {
double numberValue;
if (!formStringToDouble(src, &numberValue))
return defaultValue;
ASSERT(isfinite(numberValue));
return numberValue;
}
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
return defaultValue;
}
ASSERT_NOT_REACHED();
return defaultValue;
}
double HTMLInputElement::valueAsDate() const
{
switch (inputType()) {
case DATE:
case DATETIME:
case TIME:
case WEEK:
return parseToDouble(value(), DateComponents::invalidMilliseconds());
case MONTH: {
DateComponents date;
if (!formStringToDateComponents(inputType(), value(), &date))
return DateComponents::invalidMilliseconds();
double msec = date.millisecondsSinceEpoch();
ASSERT(isfinite(msec));
return msec;
}
case BUTTON:
case CHECKBOX:
case COLOR:
case DATETIMELOCAL: // valueAsDate doesn't work for the DATETIMELOCAL type according to the standard.
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case NUMBER:
case PASSWORD:
case RADIO:
case RANGE:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
return DateComponents::invalidMilliseconds();
}
ASSERT_NOT_REACHED();
return DateComponents::invalidMilliseconds();
}
void HTMLInputElement::setValueAsDate(double value, ExceptionCode& ec)
{
DateComponents date;
bool success;
switch (inputType()) {
case DATE:
success = date.setMillisecondsSinceEpochForDate(value);
break;
case DATETIME:
success = date.setMillisecondsSinceEpochForDateTime(value);
break;
case MONTH:
success = date.setMillisecondsSinceEpochForMonth(value);
break;
case TIME:
success = date.setMillisecondsSinceMidnight(value);
break;
case WEEK:
success = date.setMillisecondsSinceEpochForWeek(value);
break;
case BUTTON:
case CHECKBOX:
case COLOR:
case DATETIMELOCAL: // valueAsDate doesn't work for the DATETIMELOCAL type according to the standard.
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case NUMBER:
case PASSWORD:
case RADIO:
case RANGE:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
ec = INVALID_STATE_ERR;
return;
default:
ASSERT_NOT_REACHED();
success = false;
}
if (!success) {
setValue(String());
return;
}
setDateValue(date);
}
void HTMLInputElement::setDateValue(const DateComponents& date)
{
double step;
if (!getAllowedValueStep(&step)) {
setValue(date.toString());
return;
}
if (!fmod(step, msecPerMinute)) {
setValue(date.toString(DateComponents::None));
return;
}
if (!fmod(step, msecPerSecond)) {
setValue(date.toString(DateComponents::Second));
return;
}
setValue(date.toString(DateComponents::Millisecond));
}
double HTMLInputElement::valueAsNumber() const
{
const double nan = numeric_limits<double>::quiet_NaN();
switch (inputType()) {
case DATE:
case DATETIME:
case DATETIMELOCAL:
case MONTH:
case NUMBER:
case RANGE:
case TIME:
case WEEK:
return parseToDouble(value(), nan);
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
return nan;
}
ASSERT_NOT_REACHED();
return nan;
}
void HTMLInputElement::setValueAsNumber(double newValue, ExceptionCode& ec)
{
if (!isfinite(newValue)) {
ec = NOT_SUPPORTED_ERR;
return;
}
switch (inputType()) {
case DATE:
case DATETIME:
case TIME:
case WEEK:
setValueAsDate(newValue, ec);
return;
case MONTH: {
DateComponents date;
if (!date.setMonthsSinceEpoch(newValue)) {
setValue(String());
return;
}
setValue(date.toString());
return;
}
case DATETIMELOCAL: {
DateComponents date;
if (!date.setMillisecondsSinceEpochForDateTimeLocal(newValue)) {
setValue(String());
return;
}
setDateValue(date);
return;
}
case NUMBER:
case RANGE:
setValue(formStringFromDouble(newValue));
return;
case BUTTON:
case CHECKBOX:
case COLOR:
case EMAIL:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SEARCH:
case SUBMIT:
case TELEPHONE:
case TEXT:
case URL:
ec = INVALID_STATE_ERR;
return;
}
ASSERT_NOT_REACHED();
return;
}
String HTMLInputElement::placeholder() const
{
return getAttribute(placeholderAttr).string();
}
void HTMLInputElement::setPlaceholder(const String& value)
{
setAttribute(placeholderAttr, value);
}
bool HTMLInputElement::searchEventsShouldBeDispatched() const
{
return hasAttribute(incrementalAttr);
}
void HTMLInputElement::setValueFromRenderer(const String& value)
{
// File upload controls will always use setFileListFromRenderer.
ASSERT(inputType() != FILE);
m_data.setSuggestedValue(String());
updatePlaceholderVisibility(false);
InputElement::setValueFromRenderer(m_data, this, this, value);
setNeedsValidityCheck();
}
void HTMLInputElement::setFileListFromRenderer(const Vector<String>& paths)
{
m_fileList->clear();
int size = paths.size();
for (int i = 0; i < size; i++)
m_fileList->append(File::create(paths[i]));
setFormControlValueMatchesRenderer(true);
InputElement::notifyFormStateChanged(this);
setNeedsValidityCheck();
}
bool HTMLInputElement::storesValueSeparateFromAttribute() const
{
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case HIDDEN:
case IMAGE:
case RADIO:
case RESET:
case SUBMIT:
return false;
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case FILE:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case RANGE:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
return true;
}
return false;
}
void* HTMLInputElement::preDispatchEventHandler(Event *evt)
{
// preventDefault or "return false" are used to reverse the automatic checking/selection we do here.
// This result gives us enough info to perform the "undo" in postDispatch of the action we take here.
void* result = 0;
if ((inputType() == CHECKBOX || inputType() == RADIO) && evt->isMouseEvent()
&& evt->type() == eventNames().clickEvent && static_cast<MouseEvent*>(evt)->button() == LeftButton) {
if (inputType() == CHECKBOX) {
// As a way to store the state, we return 0 if we were unchecked, 1 if we were checked, and 2 for
// indeterminate.
if (indeterminate()) {
result = (void*)0x2;
setIndeterminate(false);
} else {
if (checked())
result = (void*)0x1;
setChecked(!checked(), true);
}
} else {
// For radio buttons, store the current selected radio object.
// We really want radio groups to end up in sane states, i.e., to have something checked.
// Therefore if nothing is currently selected, we won't allow this action to be "undone", since
// we want some object in the radio group to actually get selected.
HTMLInputElement* currRadio = checkedRadioButtons(this).checkedButtonForGroup(name());
if (currRadio) {
// We have a radio button selected that is not us. Cache it in our result field and ref it so
// that it can't be destroyed.
currRadio->ref();
result = currRadio;
}
setChecked(true, true);
}
}
return result;
}
void HTMLInputElement::postDispatchEventHandler(Event *evt, void* data)
{
if ((inputType() == CHECKBOX || inputType() == RADIO) && evt->isMouseEvent()
&& evt->type() == eventNames().clickEvent && static_cast<MouseEvent*>(evt)->button() == LeftButton) {
if (inputType() == CHECKBOX) {
// Reverse the checking we did in preDispatch.
if (evt->defaultPrevented() || evt->defaultHandled()) {
if (data == (void*)0x2)
setIndeterminate(true);
else
setChecked(data);
}
} else if (data) {
HTMLInputElement* input = static_cast<HTMLInputElement*>(data);
if (evt->defaultPrevented() || evt->defaultHandled()) {
// Restore the original selected radio button if possible.
// Make sure it is still a radio button and only do the restoration if it still
// belongs to our group.
if (input->form() == form() && input->inputType() == RADIO && input->name() == name()) {
// Ok, the old radio button is still in our form and in our group and is still a
// radio button, so it's safe to restore selection to it.
input->setChecked(true);
}
}
input->deref();
}
// Left clicks on radio buttons and check boxes already performed default actions in preDispatchEventHandler().
evt->setDefaultHandled();
}
}
void HTMLInputElement::defaultEventHandler(Event* evt)
{
// FIXME: It would be better to refactor this for the different types of input element.
// Having them all in one giant function makes this hard to read, and almost all the handling is type-specific.
bool clickDefaultFormButton = false;
if (isTextField() && evt->type() == eventNames().textInputEvent && evt->isTextEvent() && static_cast<TextEvent*>(evt)->data() == "\n")
clickDefaultFormButton = true;
if (inputType() == IMAGE && evt->isMouseEvent() && evt->type() == eventNames().clickEvent) {
// record the mouse position for when we get the DOMActivate event
MouseEvent* me = static_cast<MouseEvent*>(evt);
// FIXME: We could just call offsetX() and offsetY() on the event,
// but that's currently broken, so for now do the computation here.
if (me->isSimulated() || !renderer()) {
m_xPos = 0;
m_yPos = 0;
} else {
// FIXME: This doesn't work correctly with transforms.
// FIXME: pageX/pageY need adjusting for pageZoomFactor(). Use actualPageLocation()?
IntPoint absOffset = roundedIntPoint(renderer()->localToAbsolute());
m_xPos = me->pageX() - absOffset.x();
m_yPos = me->pageY() - absOffset.y();
}
}
if (isTextField()
&& evt->type() == eventNames().keydownEvent
&& evt->isKeyboardEvent()
&& focused()
&& document()->frame()
&& document()->frame()->doTextFieldCommandFromEvent(this, static_cast<KeyboardEvent*>(evt))) {
evt->setDefaultHandled();
return;
}
if (inputType() == RADIO
&& evt->isMouseEvent()
&& evt->type() == eventNames().clickEvent
&& static_cast<MouseEvent*>(evt)->button() == LeftButton) {
evt->setDefaultHandled();
return;
}
// Call the base event handler before any of our own event handling for almost all events in text fields.
// Makes editing keyboard handling take precedence over the keydown and keypress handling in this function.
bool callBaseClassEarly = isTextField() && !clickDefaultFormButton
&& (evt->type() == eventNames().keydownEvent || evt->type() == eventNames().keypressEvent);
if (callBaseClassEarly) {
HTMLFormControlElementWithState::defaultEventHandler(evt);
if (evt->defaultHandled())
return;
}
// DOMActivate events cause the input to be "activated" - in the case of image and submit inputs, this means
// actually submitting the form. For reset inputs, the form is reset. These events are sent when the user clicks
// on the element, or presses enter while it is the active element. JavaScript code wishing to activate the element
// must dispatch a DOMActivate event - a click event will not do the job.
if (evt->type() == eventNames().DOMActivateEvent && !disabled()) {
if (inputType() == IMAGE || inputType() == SUBMIT || inputType() == RESET) {
if (!form())
return;
if (inputType() == RESET)
form()->reset();
else {
m_activeSubmit = true;
// FIXME: Would be cleaner to get m_xPos and m_yPos out of the underlying mouse
// event (if any) here instead of relying on the variables set above when
// processing the click event. Even better, appendFormData could pass the
// event in, and then we could get rid of m_xPos and m_yPos altogether!
if (!form()->prepareSubmit(evt)) {
m_xPos = 0;
m_yPos = 0;
}
m_activeSubmit = false;
}
} else if (inputType() == FILE && renderer())
toRenderFileUploadControl(renderer())->click();
}
// Use key press event here since sending simulated mouse events
// on key down blocks the proper sending of the key press event.
if (evt->type() == eventNames().keypressEvent && evt->isKeyboardEvent()) {
bool clickElement = false;
int charCode = static_cast<KeyboardEvent*>(evt)->charCode();
if (charCode == '\r') {
switch (inputType()) {
case CHECKBOX:
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case HIDDEN:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case RANGE:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
// Simulate mouse click on the default form button for enter for these types of elements.
clickDefaultFormButton = true;
break;
case BUTTON:
case FILE:
case IMAGE:
case RESET:
case SUBMIT:
// Simulate mouse click for enter for these types of elements.
clickElement = true;
break;
case RADIO:
break; // Don't do anything for enter on a radio button.
}
} else if (charCode == ' ') {
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case FILE:
case IMAGE:
case RESET:
case SUBMIT:
case RADIO:
// Prevent scrolling down the page.
evt->setDefaultHandled();
return;
default:
break;
}
}
if (clickElement) {
dispatchSimulatedClick(evt);
evt->setDefaultHandled();
return;
}
}
if (evt->type() == eventNames().keydownEvent && evt->isKeyboardEvent()) {
String key = static_cast<KeyboardEvent*>(evt)->keyIdentifier();
if (key == "U+0020") {
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case FILE:
case IMAGE:
case RESET:
case SUBMIT:
case RADIO:
setActive(true, true);
// No setDefaultHandled(), because IE dispatches a keypress in this case
// and the caller will only dispatch a keypress if we don't call setDefaultHandled.
return;
default:
break;
}
}
// allow enter to change state of radio
if (inputType() == RADIO && (key == "Up" || key == "Down" || key == "Left" || key == "Right")) {
// Left and up mean "previous radio button".
// Right and down mean "next radio button".
// Tested in WinIE, and even for RTL, left still means previous radio button (and so moves
// to the right). Seems strange, but we'll match it.
bool forward = (key == "Down" || key == "Right");
// We can only stay within the form's children if the form hasn't been demoted to a leaf because
// of malformed HTML.
Node* n = this;
while ((n = (forward ? n->traverseNextNode() : n->traversePreviousNode()))) {
// Once we encounter a form element, we know we're through.
if (n->hasTagName(formTag))
break;
// Look for more radio buttons.
if (n->hasTagName(inputTag)) {
HTMLInputElement* elt = static_cast<HTMLInputElement*>(n);
if (elt->form() != form())
break;
if (n->hasTagName(inputTag)) {
HTMLInputElement* inputElt = static_cast<HTMLInputElement*>(n);
if (inputElt->inputType() == RADIO && inputElt->name() == name() && inputElt->isFocusable()) {
inputElt->setChecked(true);
document()->setFocusedNode(inputElt);
inputElt->dispatchSimulatedClick(evt, false, false);
evt->setDefaultHandled();
break;
}
}
}
}
}
}
if (evt->type() == eventNames().keyupEvent && evt->isKeyboardEvent()) {
bool clickElement = false;
String key = static_cast<KeyboardEvent*>(evt)->keyIdentifier();
if (key == "U+0020") {
switch (inputType()) {
case BUTTON:
case CHECKBOX:
case FILE:
case IMAGE:
case RESET:
case SUBMIT:
// Simulate mouse click for spacebar for these types of elements.
// The AppKit already does this for some, but not all, of them.
clickElement = true;
break;
case RADIO:
// If an unselected radio is tabbed into (because the entire group has nothing
// checked, or because of some explicit .focus() call), then allow space to check it.
if (!checked())
clickElement = true;
break;
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case HIDDEN:
case ISINDEX:
case MONTH:
case NUMBER:
case PASSWORD:
case RANGE:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
break;
}
}
if (clickElement) {
if (active())
dispatchSimulatedClick(evt);
evt->setDefaultHandled();
return;
}
}
if (clickDefaultFormButton) {
if (isSearchField()) {
addSearchResult();
onSearch();
}
// Fire onChange for text fields.
RenderObject* r = renderer();
if (r && r->isTextField() && toRenderTextControl(r)->wasChangedSinceLastChangeEvent()) {
dispatchFormControlChangeEvent();
// Refetch the renderer since arbitrary JS code run during onchange can do anything, including destroying it.
r = renderer();
if (r && r->isTextField())
toRenderTextControl(r)->setChangedSinceLastChangeEvent(false);
}
RefPtr<HTMLFormElement> formForSubmission = form();
// If there is no form and the element is an <isindex>, then create a temporary form just to be used for submission.
if (!formForSubmission && inputType() == ISINDEX)
formForSubmission = createTemporaryFormForIsIndex();
// Form may never have been present, or may have been destroyed by code responding to the change event.
if (formForSubmission)
formForSubmission->submitClick(evt);
evt->setDefaultHandled();
return;
}
if (evt->isBeforeTextInsertedEvent())
InputElement::handleBeforeTextInsertedEvent(m_data, this, this, evt);
if (isTextField() && renderer() && (evt->isMouseEvent() || evt->isDragEvent() || evt->isWheelEvent() || evt->type() == eventNames().blurEvent || evt->type() == eventNames().focusEvent))
toRenderTextControlSingleLine(renderer())->forwardEvent(evt);
if (inputType() == RANGE && renderer() && (evt->isMouseEvent() || evt->isDragEvent() || evt->isWheelEvent()))
toRenderSlider(renderer())->forwardEvent(evt);
if (!callBaseClassEarly && !evt->defaultHandled())
HTMLFormControlElementWithState::defaultEventHandler(evt);
}
PassRefPtr<HTMLFormElement> HTMLInputElement::createTemporaryFormForIsIndex()
{
RefPtr<HTMLFormElement> form = new HTMLFormElement(formTag, document());
form->registerFormElement(this);
form->setMethod("GET");
if (!document()->baseURL().isEmpty()) {
// We treat the href property of the <base> element as the form action, as per section 7.5
// "Queries and Indexes" of the HTML 2.0 spec. <http://www.w3.org/MarkUp/html-spec/html-spec_7.html#SEC7.5>.
form->setAction(document()->baseURL().string());
}
return form.release();
}
bool HTMLInputElement::isURLAttribute(Attribute *attr) const
{
return (attr->name() == srcAttr);
}
String HTMLInputElement::defaultValue() const
{
return getAttribute(valueAttr);
}
void HTMLInputElement::setDefaultValue(const String &value)
{
setAttribute(valueAttr, value);
}
bool HTMLInputElement::defaultChecked() const
{
return !getAttribute(checkedAttr).isNull();
}
void HTMLInputElement::setDefaultChecked(bool defaultChecked)
{
setAttribute(checkedAttr, defaultChecked ? "" : 0);
}
void HTMLInputElement::setDefaultName(const AtomicString& name)
{
m_data.setName(name);
}
String HTMLInputElement::accept() const
{
return getAttribute(acceptAttr);
}
void HTMLInputElement::setAccept(const String &value)
{
setAttribute(acceptAttr, value);
}
String HTMLInputElement::accessKey() const
{
return getAttribute(accesskeyAttr);
}
void HTMLInputElement::setAccessKey(const String &value)
{
setAttribute(accesskeyAttr, value);
}
String HTMLInputElement::align() const
{
return getAttribute(alignAttr);
}
void HTMLInputElement::setAlign(const String &value)
{
setAttribute(alignAttr, value);
}
String HTMLInputElement::alt() const
{
return getAttribute(altAttr);
}
void HTMLInputElement::setAlt(const String &value)
{
setAttribute(altAttr, value);
}
int HTMLInputElement::maxLength() const
{
return m_data.maxLength();
}
void HTMLInputElement::setMaxLength(int maxLength, ExceptionCode& ec)
{
if (maxLength < 0)
ec = INDEX_SIZE_ERR;
else
setAttribute(maxlengthAttr, String::number(maxLength));
}
bool HTMLInputElement::multiple() const
{
return !getAttribute(multipleAttr).isNull();
}
void HTMLInputElement::setMultiple(bool multiple)
{
setAttribute(multipleAttr, multiple ? "" : 0);
}
void HTMLInputElement::setSize(unsigned _size)
{
setAttribute(sizeAttr, String::number(_size));
}
KURL HTMLInputElement::src() const
{
return document()->completeURL(getAttribute(srcAttr));
}
void HTMLInputElement::setSrc(const String &value)
{
setAttribute(srcAttr, value);
}
String HTMLInputElement::useMap() const
{
return getAttribute(usemapAttr);
}
void HTMLInputElement::setUseMap(const String &value)
{
setAttribute(usemapAttr, value);
}
void HTMLInputElement::setAutofilled(bool b)
{
if (b == m_autofilled)
return;
m_autofilled = b;
setNeedsStyleRecalc();
}
FileList* HTMLInputElement::files()
{
if (inputType() != FILE)
return 0;
return m_fileList.get();
}
String HTMLInputElement::sanitizeValue(const String& proposedValue) const
{
if (isTextField())
return InputElement::sanitizeValue(this, proposedValue);
return proposedValue;
}
bool HTMLInputElement::needsActivationCallback()
{
return inputType() == PASSWORD || m_autocomplete == Off;
}
void HTMLInputElement::registerForActivationCallbackIfNeeded()
{
if (needsActivationCallback())
document()->registerForDocumentActivationCallbacks(this);
}
void HTMLInputElement::unregisterForActivationCallbackIfNeeded()
{
if (!needsActivationCallback())
document()->unregisterForDocumentActivationCallbacks(this);
}
bool HTMLInputElement::isRequiredFormControl() const
{
if (!required())
return false;
switch (inputType()) {
case CHECKBOX:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case FILE:
case MONTH:
case NUMBER:
case PASSWORD:
case RADIO:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK:
return true;
case BUTTON:
case COLOR:
case HIDDEN:
case IMAGE:
case ISINDEX:
case RANGE:
case RESET:
case SUBMIT:
return false;
}
ASSERT_NOT_REACHED();
return false;
}
void HTMLInputElement::cacheSelection(int start, int end)
{
m_data.setCachedSelectionStart(start);
m_data.setCachedSelectionEnd(end);
}
void HTMLInputElement::addSearchResult()
{
ASSERT(isSearchField());
if (renderer())
toRenderTextControlSingleLine(renderer())->addSearchResult();
}
void HTMLInputElement::onSearch()
{
ASSERT(isSearchField());
if (renderer())
toRenderTextControlSingleLine(renderer())->stopSearchEventTimer();
dispatchEvent(Event::create(eventNames().searchEvent, true, false));
}
void HTMLInputElement::documentDidBecomeActive()
{
ASSERT(needsActivationCallback());
reset();
}
void HTMLInputElement::willMoveToNewOwnerDocument()
{
// Always unregister for cache callbacks when leaving a document, even if we would otherwise like to be registered
if (needsActivationCallback())
document()->unregisterForDocumentActivationCallbacks(this);
document()->checkedRadioButtons().removeButton(this);
HTMLFormControlElementWithState::willMoveToNewOwnerDocument();
}
void HTMLInputElement::didMoveToNewOwnerDocument()
{
registerForActivationCallbackIfNeeded();
HTMLFormControlElementWithState::didMoveToNewOwnerDocument();
}
void HTMLInputElement::addSubresourceAttributeURLs(ListHashSet<KURL>& urls) const
{
HTMLFormControlElementWithState::addSubresourceAttributeURLs(urls);
addSubresourceURL(urls, src());
}
bool HTMLInputElement::willValidate() const
{
// FIXME: This shall check for new WF2 input types too
return HTMLFormControlElementWithState::willValidate() && inputType() != HIDDEN &&
inputType() != BUTTON && inputType() != RESET;
}
String HTMLInputElement::formStringFromDouble(double number)
{
// According to HTML5, "the best representation of the number n as a floating
// point number" is a string produced by applying ToString() to n.
DtoaBuffer buffer;
unsigned length;
doubleToStringInJavaScriptFormat(number, buffer, &length);
return String(buffer, length);
}
bool HTMLInputElement::formStringToDouble(const String& src, double* out)
{
// See HTML5 2.4.4.3 `Real numbers.'
if (src.isEmpty())
return false;
// String::toDouble() accepts leading + \t \n \v \f \r and SPACE, which are invalid in HTML5.
// So, check the first character.
if (src[0] != '-' && (src[0] < '0' || src[0] > '9'))
return false;
bool valid = false;
double value = src.toDouble(&valid);
if (!valid)
return false;
// NaN and Infinity are not valid numbers according to the standard.
if (!isfinite(value))
return false;
// -0 -> 0
if (!value)
value = 0;
if (out)
*out = value;
return true;
}
bool HTMLInputElement::formStringToDateComponents(InputType type, const String& formString, DateComponents* out)
{
if (formString.isEmpty())
return false;
DateComponents ignoredResult;
if (!out)
out = &ignoredResult;
const UChar* characters = formString.characters();
unsigned length = formString.length();
unsigned end;
switch (type) {
case DATE:
return out->parseDate(characters, length, 0, end) && end == length;
case DATETIME:
return out->parseDateTime(characters, length, 0, end) && end == length;
case DATETIMELOCAL:
return out->parseDateTimeLocal(characters, length, 0, end) && end == length;
case MONTH:
return out->parseMonth(characters, length, 0, end) && end == length;
case WEEK:
return out->parseWeek(characters, length, 0, end) && end == length;
case TIME:
return out->parseTime(characters, length, 0, end) && end == length;
default:
ASSERT_NOT_REACHED();
return false;
}
}
#if ENABLE(DATALIST)
HTMLElement* HTMLInputElement::list() const
{
return dataList();
}
HTMLDataListElement* HTMLInputElement::dataList() const
{
if (!m_hasNonEmptyList)
return 0;
switch (inputType()) {
case COLOR:
case DATE:
case DATETIME:
case DATETIMELOCAL:
case EMAIL:
case MONTH:
case NUMBER:
case RANGE:
case SEARCH:
case TELEPHONE:
case TEXT:
case TIME:
case URL:
case WEEK: {
Element* element = document()->getElementById(getAttribute(listAttr));
if (element && element->hasTagName(datalistTag))
return static_cast<HTMLDataListElement*>(element);
break;
}
case BUTTON:
case CHECKBOX:
case FILE:
case HIDDEN:
case IMAGE:
case ISINDEX:
case PASSWORD:
case RADIO:
case RESET:
case SUBMIT:
break;
}
return 0;
}
HTMLOptionElement* HTMLInputElement::selectedOption() const
{
String currentValue = value();
// The empty value never matches to a datalist option because it
// doesn't represent a suggestion according to the standard.
if (currentValue.isEmpty())
return 0;
HTMLDataListElement* sourceElement = dataList();
if (!sourceElement)
return 0;
RefPtr<HTMLCollection> options = sourceElement->options();
for (unsigned i = 0; options && i < options->length(); ++i) {
HTMLOptionElement* option = static_cast<HTMLOptionElement*>(options->item(i));
if (!option->disabled() && currentValue == option->value())
return option;
}
return 0;
}
#endif // ENABLE(DATALIST)
} // namespace