/* * 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 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., 59 Temple Place - Suite 330, * Boston, MA 02111-1307, USA. * */ #include "config.h" #include "HTMLInputElement.h" #include "BeforeTextInsertedEvent.h" #include "CSSPropertyNames.h" #include "Document.h" #include "Editor.h" #include "Event.h" #include "EventHandler.h" #include "EventNames.h" #include "FocusController.h" #include "FormDataList.h" #include "Frame.h" #include "HTMLFormElement.h" #include "HTMLImageLoader.h" #include "HTMLNames.h" #include "KeyboardEvent.h" #include "LocalizedStrings.h" #include "MouseEvent.h" #include "Page.h" #include "RenderButton.h" #include "RenderFileUploadControl.h" #include "RenderImage.h" #include "RenderText.h" #include "RenderTextControl.h" #include "RenderTheme.h" #include "RenderSlider.h" #include "SelectionController.h" #include "TextBreakIterator.h" #include "TextEvent.h" using namespace std; namespace WebCore { using namespace EventNames; using namespace HTMLNames; const int maxSavedResults = 256; // FIXME: According to HTML4, the length attribute's value can be arbitrarily // large. However, due to http://bugs.webkit.org/show_bugs.cgi?id=14536 things // get rather sluggish when a text field has a larger number of characters than // this, even when just clicking in the text field. static const int cMaxLen = 524288; static int numGraphemeClusters(const StringImpl* s) { if (!s) return 0; TextBreakIterator* it = characterBreakIterator(s->characters(), s->length()); if (!it) return 0; int num = 0; while (textBreakNext(it) != TextBreakDone) ++num; return num; } static int numCharactersInGraphemeClusters(const StringImpl* s, int numGraphemeClusters) { if (!s) return 0; TextBreakIterator* it = characterBreakIterator(s->characters(), s->length()); if (!it) return 0; for (int i = 0; i < numGraphemeClusters; ++i) if (textBreakNext(it) == TextBreakDone) return s->length(); return textBreakCurrent(it); } HTMLInputElement::HTMLInputElement(Document* doc, HTMLFormElement* f) : HTMLFormControlElementWithState(inputTag, doc, f) { init(); } HTMLInputElement::HTMLInputElement(const QualifiedName& tagName, Document* doc, HTMLFormElement* f) : HTMLFormControlElementWithState(tagName, doc, f) { init(); } void HTMLInputElement::init() { m_imageLoader = 0; m_type = TEXT; m_maxLen = cMaxLen; m_size = 20; m_checked = false; m_defaultChecked = false; m_useDefaultChecked = true; m_indeterminate = false; m_haveType = false; m_activeSubmit = false; m_autocomplete = true; m_inited = false; m_autofilled = false; xPos = 0; yPos = 0; cachedSelStart = -1; cachedSelEnd = -1; m_maxResults = -1; if (form()) m_autocomplete = form()->autoComplete(); } HTMLInputElement::~HTMLInputElement() { if (inputType() == PASSWORD) document()->unregisterForDidRestoreFromCacheCallback(this); document()->checkedRadioButtons().removeButton(this); delete m_imageLoader; } const AtomicString& HTMLInputElement::name() const { return m_name.isNull() ? emptyAtom : m_name; } static inline HTMLFormElement::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) { // Unnamed radio buttons are never focusable (matches WinIE). if (name().isEmpty()) return false; // 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(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::focus(bool restorePreviousSelection) { if (isTextField()) { Document* doc = document(); if (doc->focusedNode() == this) return; doc->updateLayout(); if (!supportsFocus()) return; if (Page* page = doc->page()) page->focusController()->setFocusedNode(this, doc->frame()); // FIXME: Should isFocusable do the updateLayout? if (!isFocusable()) { setNeedsFocusAppearanceUpdate(true); return; } updateFocusAppearance(restorePreviousSelection); return; } HTMLFormControlElementWithState::focus(restorePreviousSelection); } void HTMLInputElement::updateFocusAppearance(bool restorePreviousSelection) { if (isTextField()) { if (!restorePreviousSelection || cachedSelStart == -1) select(); else // Restore the cached selection. setSelectionRange(cachedSelStart, cachedSelEnd); if (document() && document()->frame()) document()->frame()->revealSelection(); } else HTMLFormControlElementWithState::updateFocusAppearance(); } void HTMLInputElement::aboutToUnload() { if (isTextField() && focused() && document()->frame()) document()->frame()->textFieldDidEndEditing(this); } void HTMLInputElement::dispatchFocusEvent() { if (isTextField()) { setAutofilled(false); if (inputType() == PASSWORD && document()->frame()) document()->setUseSecureKeyboardEntryWhenActive(true); } HTMLFormControlElementWithState::dispatchFocusEvent(); } void HTMLInputElement::dispatchBlurEvent() { if (isTextField() && document()->frame()) { if (inputType() == PASSWORD) document()->setUseSecureKeyboardEntryWhenActive(false); document()->frame()->textFieldDidEndEditing(this); } HTMLFormControlElementWithState::dispatchBlurEvent(); } void HTMLInputElement::setType(const String& t) { if (t.isEmpty()) { int exccode; removeAttribute(typeAttr, exccode); } else setAttribute(typeAttr, t); } void HTMLInputElement::setInputType(const String& t) { InputType newType; if (equalIgnoringCase(t, "password")) newType = PASSWORD; else if (equalIgnoringCase(t, "checkbox")) newType = CHECKBOX; else if (equalIgnoringCase(t, "radio")) newType = RADIO; else if (equalIgnoringCase(t, "submit")) newType = SUBMIT; else if (equalIgnoringCase(t, "reset")) newType = RESET; else if (equalIgnoringCase(t, "file")) newType = FILE; else if (equalIgnoringCase(t, "hidden")) newType = HIDDEN; else if (equalIgnoringCase(t, "image")) newType = IMAGE; else if (equalIgnoringCase(t, "button")) newType = BUTTON; else if (equalIgnoringCase(t, "khtml_isindex")) newType = ISINDEX; else if (equalIgnoringCase(t, "search")) newType = SEARCH; else if (equalIgnoringCase(t, "range")) newType = RANGE; else newType = TEXT; // 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) { 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); bool wasAttached = m_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_value.isNull()) { setAttribute(valueAttr, m_value); m_value = String(); } if (!didStoreValue && willStoreValue) m_value = constrainValue(getAttribute(valueAttr)); else recheckValue(); if (wasPasswordField && !isPasswordField) document()->unregisterForDidRestoreFromCacheCallback(this); else if (!wasPasswordField && isPasswordField) document()->registerForDidRestoreFromCacheCallback(this); if (didRespectHeightAndWidth != willRespectHeightAndWidth) { NamedMappedAttrMap* map = mappedAttributes(); if (MappedAttribute* height = map->getAttributeItem(heightAttr)) attributeChanged(height, false); if (MappedAttribute* width = map->getAttributeItem(widthAttr)) attributeChanged(width, false); if (MappedAttribute* align = map->getAttributeItem(alignAttr)) attributeChanged(align, false); } if (wasAttached) attach(); checkedRadioButtons(this).addButton(this); } } m_haveType = true; if (inputType() != IMAGE && m_imageLoader) { delete m_imageLoader; m_imageLoader = 0; } } const AtomicString& HTMLInputElement::type() const { // needs to be lowercase according to DOM spec switch (inputType()) { case BUTTON: { static const AtomicString button("button"); return button; } case CHECKBOX: { static const AtomicString checkbox("checkbox"); return checkbox; } case FILE: { static const AtomicString file("file"); return file; } case HIDDEN: { static const AtomicString hidden("hidden"); return hidden; } case IMAGE: { static const AtomicString image("image"); return image; } case ISINDEX: return emptyAtom; case PASSWORD: { static const AtomicString password("password"); return password; } case RADIO: { static const AtomicString radio("radio"); return radio; } case RANGE: { static const AtomicString range("range"); return range; } case RESET: { static const AtomicString reset("reset"); return reset; } case SEARCH: { static const AtomicString search("search"); return search; } case SUBMIT: { static const AtomicString submit("submit"); return submit; } case TEXT: { static const AtomicString text("text"); return text; } } return emptyAtom; } bool HTMLInputElement::saveState(String& result) const { switch (inputType()) { case BUTTON: case FILE: case HIDDEN: case IMAGE: case ISINDEX: case RANGE: case RESET: case SEARCH: case SUBMIT: case TEXT: 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::restoreState(const String& state) { ASSERT(inputType() != PASSWORD); // should never save/restore password fields switch (inputType()) { case BUTTON: case FILE: case HIDDEN: case IMAGE: case ISINDEX: case RANGE: case RESET: case SEARCH: case SUBMIT: case TEXT: setValue(state); break; case CHECKBOX: case RADIO: setChecked(state == "on"); break; case PASSWORD: break; } } bool HTMLInputElement::canHaveSelection() const { return isTextField(); } int HTMLInputElement::selectionStart() const { if (!isTextField()) return 0; if (document()->focusedNode() != this && cachedSelStart != -1) return cachedSelStart; if (!renderer()) return 0; return static_cast(renderer())->selectionStart(); } int HTMLInputElement::selectionEnd() const { if (!isTextField()) return 0; if (document()->focusedNode() != this && cachedSelEnd != -1) return cachedSelEnd; if (!renderer()) return 0; return static_cast(renderer())->selectionEnd(); } void HTMLInputElement::setSelectionStart(int start) { if (!isTextField()) return; if (!renderer()) return; static_cast(renderer())->setSelectionStart(start); } void HTMLInputElement::setSelectionEnd(int end) { if (!isTextField()) return; if (!renderer()) return; static_cast(renderer())->setSelectionEnd(end); } void HTMLInputElement::select() { if (!isTextField()) return; if (!renderer()) return; static_cast(renderer())->select(); } void HTMLInputElement::setSelectionRange(int start, int end) { if (!isTextField()) return; if (!renderer()) return; static_cast(renderer())->setSelectionRange(start, end); } 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 ISINDEX: case PASSWORD: case SEARCH: case TEXT: // 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 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_name = attr->value(); checkedRadioButtons(this).addButton(this); } else if (attr->name() == autocompleteAttr) { m_autocomplete = !equalIgnoringCase(attr->value(), "off"); } 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_value.isNull()) setChanged(); setValueMatchesRenderer(false); } else if (attr->name() == checkedAttr) { m_defaultChecked = !attr->isNull(); if (m_useDefaultChecked) { setChecked(m_defaultChecked); m_useDefaultChecked = true; } } else if (attr->name() == maxlengthAttr) { int oldMaxLen = m_maxLen; m_maxLen = !attr->isNull() ? attr->value().toInt() : cMaxLen; if (m_maxLen <= 0 || m_maxLen > cMaxLen) m_maxLen = cMaxLen; if (oldMaxLen != m_maxLen) recheckValue(); setChanged(); } else if (attr->name() == sizeAttr) { m_size = !attr->isNull() ? attr->value().toInt() : 20; } else if (attr->name() == altAttr) { if (renderer() && inputType() == IMAGE) static_cast(renderer())->updateAltText(); } else if (attr->name() == srcAttr) { if (renderer() && inputType() == IMAGE) { if (!m_imageLoader) m_imageLoader = new HTMLImageLoader(this); m_imageLoader->updateFromElement(); } } else if (attr->name() == usemapAttr || attr->name() == accesskeyAttr) { // FIXME: ignore for the moment } else if (attr->name() == vspaceAttr) { addCSSLength(attr, CSS_PROP_MARGIN_TOP, attr->value()); addCSSLength(attr, CSS_PROP_MARGIN_BOTTOM, attr->value()); } else if (attr->name() == hspaceAttr) { addCSSLength(attr, CSS_PROP_MARGIN_LEFT, attr->value()); addCSSLength(attr, CSS_PROP_MARGIN_RIGHT, attr->value()); } else if (attr->name() == alignAttr) { if (inputType() == IMAGE) addHTMLAlignment(attr); } else if (attr->name() == widthAttr) { if (respectHeightAndWidthAttrs()) addCSSLength(attr, CSS_PROP_WIDTH, attr->value()); } else if (attr->name() == heightAttr) { if (respectHeightAndWidthAttrs()) addCSSLength(attr, CSS_PROP_HEIGHT, attr->value()); } else if (attr->name() == onfocusAttr) { setHTMLEventListener(focusEvent, attr); } else if (attr->name() == onblurAttr) { setHTMLEventListener(blurEvent, attr); } else if (attr->name() == onselectAttr) { setHTMLEventListener(selectEvent, attr); } else if (attr->name() == onchangeAttr) { setHTMLEventListener(changeEvent, attr); } else if (attr->name() == oninputAttr) { setHTMLEventListener(inputEvent, attr); } // Search field and slider attributes all just cause updateFromElement to be called through style // recalcing. else if (attr->name() == onsearchAttr) { setHTMLEventListener(searchEvent, attr); } else if (attr->name() == resultsAttr) { int oldResults = m_maxResults; m_maxResults = !attr->isNull() ? 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(); } setChanged(); } else if (attr->name() == autosaveAttr || attr->name() == incrementalAttr || attr->name() == placeholderAttr || attr->name() == minAttr || attr->name() == maxAttr || attr->name() == precisionAttr) { setChanged(); } else HTMLFormControlElementWithState::parseMappedAttribute(attr); } bool HTMLInputElement::rendererIsNeeded(RenderStyle *style) { switch (inputType()) { case BUTTON: case CHECKBOX: case FILE: case IMAGE: case ISINDEX: case PASSWORD: case RADIO: case RANGE: case RESET: case SEARCH: case SUBMIT: case TEXT: return HTMLFormControlElementWithState::rendererIsNeeded(style); case HIDDEN: return false; } ASSERT(false); return false; } 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 ISINDEX: case PASSWORD: case SEARCH: case TEXT: return new (arena) RenderTextControl(this, false); } 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 = new HTMLImageLoader(this); m_imageLoader->updateFromElement(); if (renderer()) { RenderImage* imageObj = static_cast(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(); } } } void HTMLInputElement::detach() { HTMLFormControlElementWithState::detach(); setValueMatchesRenderer(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 HIDDEN: case ISINDEX: case PASSWORD: case RANGE: case SEARCH: case TEXT: // 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"), xPos); encoding.appendData(name().isEmpty() ? "y" : (name() + ".y"), 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: // Can't submit file on GET. if (!multipart) return false; // If no filename at all is entered, return successful but empty. // Null would be more logical, but Netscape posts an empty file. Argh. if (value().isEmpty()) { encoding.appendData(name(), String("")); return true; } encoding.appendFile(name(), value()); return true; } return false; } void HTMLInputElement::reset() { if (storesValueSeparateFromAttribute()) setValue(String()); setChecked(m_defaultChecked); m_useDefaultChecked = true; } void HTMLInputElement::setChecked(bool nowChecked, bool sendChangeEvent) { if (checked() == nowChecked) return; m_useDefaultChecked = false; m_checked = nowChecked; setChanged(); checkedRadioButtons(this).addButton(this); if (renderer() && renderer()->style()->hasAppearance()) theme()->stateChanged(renderer(), CheckedState); // 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)) onChange(); } void HTMLInputElement::setIndeterminate(bool _indeterminate) { // Only checkboxes honor indeterminate. if (inputType() != CHECKBOX || indeterminate() == _indeterminate) return; m_indeterminate = _indeterminate; setChanged(); if (renderer() && renderer()->style()->hasAppearance()) theme()->stateChanged(renderer(), CheckedState); } void HTMLInputElement::copyNonAttributeProperties(const Element *source) { const HTMLInputElement *sourceElem = static_cast(source); m_value = sourceElem->m_value; m_checked = sourceElem->m_checked; m_indeterminate = sourceElem->m_indeterminate; HTMLFormControlElementWithState::copyNonAttributeProperties(source); } String HTMLInputElement::value() const { String value = m_value; // It's important *not* to fall back to the value attribute for file inputs, // because that would allow a malicious web page to upload files by setting the // value attribute in markup. if (value.isNull() && inputType() != FILE) value = constrainValue(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 FILE: case HIDDEN: case IMAGE: case ISINDEX: case PASSWORD: case RADIO: case RANGE: case SEARCH: case TEXT: break; case RESET: v = resetButtonDefaultLabel(); break; case SUBMIT: v = submitButtonDefaultLabel(); break; } } return v; } void HTMLInputElement::setValue(const String& value) { // For security reasons, we don't allow setting the filename, but we do allow clearing it. if (inputType() == FILE && !value.isEmpty()) return; setValueMatchesRenderer(false); if (storesValueSeparateFromAttribute()) { m_value = constrainValue(value); if (isTextField() && inDocument()) document()->updateRendering(); if (renderer()) renderer()->updateFromElement(); setChanged(); } else setAttribute(valueAttr, constrainValue(value)); if (isTextField()) { unsigned max = m_value.length(); if (document()->focusedNode() == this) setSelectionRange(max, max); else { cachedSelStart = max; cachedSelEnd = max; } } } void HTMLInputElement::setValueFromRenderer(const String& value) { // Renderer and our event handler are responsible for constraining values. ASSERT(value == constrainValue(value) || constrainValue(value).isEmpty()); // Workaround for bug where trailing \n is included in the result of textContent. // The assert macro above may also be simplified to: value == constrainValue(value) // http://bugs.webkit.org/show_bug.cgi?id=9661 if (value == "\n") m_value = ""; else m_value = value; setValueMatchesRenderer(); // Fire the "input" DOM event. dispatchHTMLEvent(inputEvent, true, false); } bool HTMLInputElement::storesValueSeparateFromAttribute() const { switch (inputType()) { case BUTTON: case CHECKBOX: case HIDDEN: case IMAGE: case RADIO: case RANGE: case RESET: case SUBMIT: return false; case FILE: case ISINDEX: case PASSWORD: case SEARCH: case TEXT: 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() == clickEvent && static_cast(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. if (name().isEmpty() || checked()) return 0; // Match WinIE and don't allow unnamed radio buttons to be checked. // Checked buttons just stay checked. // FIXME: Need to learn to work without a form. // 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() == clickEvent && static_cast(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(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. // Match WinIE and don't allow unnamed radio buttons to be checked. if (input->form() == form() && input->inputType() == RADIO && !name().isEmpty() && 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) { bool clickDefaultFormButton = false; if (isTextField() && evt->type() == textInputEvent && evt->isTextEvent() && static_cast(evt)->data() == "\n") clickDefaultFormButton = true; if (inputType() == IMAGE && evt->isMouseEvent() && evt->type() == clickEvent) { // record the mouse position for when we get the DOMActivate event MouseEvent* me = static_cast(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()) { xPos = 0; yPos = 0; } else { int offsetX, offsetY; renderer()->absolutePosition(offsetX, offsetY); xPos = me->pageX() - offsetX; // FIXME: Why is yPos a short? yPos = me->pageY() - offsetY; } } // Before calling the base class defaultEventHandler, which will call handleKeypress, call doTextFieldCommandFromEvent. if (isTextField() && evt->type() == keypressEvent && evt->isKeyboardEvent() && focused() && document()->frame() && document()->frame()->doTextFieldCommandFromEvent(this, static_cast(evt))) { evt->setDefaultHandled(); return; } if (inputType() == RADIO && evt->isMouseEvent() && evt->type() == clickEvent && static_cast(evt)->button() == LeftButton) { evt->setDefaultHandled(); return; } // Let the key handling done in EventTargetNode take precedence over the event handling here for editable text fields if (!clickDefaultFormButton) { 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() == 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 xPos and 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 xPos and yPos altogether! if (!form()->prepareSubmit(evt)) { xPos = 0; yPos = 0; } m_activeSubmit = false; } } else if (inputType() == FILE && renderer()) static_cast(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() == keypressEvent && evt->isKeyboardEvent()) { bool clickElement = false; String key = static_cast(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 HIDDEN: case ISINDEX: case PASSWORD: case RANGE: case SEARCH: case TEXT: break; } } if (key == "Enter") { switch (inputType()) { case BUTTON: case CHECKBOX: case HIDDEN: case ISINDEX: case PASSWORD: case RANGE: case SEARCH: case TEXT: // Simulate mouse click on the default form button for enter for these types of elements. clickDefaultFormButton = true; break; 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. } } 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(n); if (elt->form() != form()) break; if (n->hasTagName(inputTag)) { // Match WinIE and don't allow unnamed radio buttons to be checked. HTMLInputElement* inputElt = static_cast(n); if (inputElt->inputType() == RADIO && !name().isEmpty() && inputElt->name() == name() && inputElt->isFocusable()) { inputElt->setChecked(true); document()->setFocusedNode(inputElt); inputElt->dispatchSimulatedClick(evt, false, false); evt->setDefaultHandled(); break; } } } } } if (clickElement) { dispatchSimulatedClick(evt); evt->setDefaultHandled(); return; } } if (clickDefaultFormButton) { if (isSearchField()) { addSearchResult(); onSearch(); } // Fire onChange for text fields. RenderObject* r = renderer(); if (r && r->isTextField() && r->isEdited()) { onChange(); r->setEdited(false); } // Form may never have been present, or may have been destroyed by the change event. if (form()) form()->submitClick(evt); evt->setDefaultHandled(); return; } if (evt->isBeforeTextInsertedEvent()) { // Make sure that the text to be inserted will not violate the maxLength. int oldLen = numGraphemeClusters(value().impl()); ASSERT(oldLen <= maxLength()); int selectionLen = numGraphemeClusters(document()->frame()->selectionController()->toString().impl()); ASSERT(oldLen >= selectionLen); int maxNewLen = maxLength() - (oldLen - selectionLen); // Truncate the inserted text to avoid violating the maxLength and other constraints. BeforeTextInsertedEvent* textEvent = static_cast(evt); textEvent->setText(constrainValue(textEvent->text(), maxNewLen)); } if (isTextField() && renderer() && (evt->isMouseEvent() || evt->isDragEvent() || evt->isWheelEvent() || evt->type() == blurEvent || evt->type() == focusEvent)) static_cast(renderer())->forwardEvent(evt); if (inputType() == RANGE && renderer()) { RenderSlider* slider = static_cast(renderer()); if (evt->isMouseEvent() && evt->type() == mousedownEvent && static_cast(evt)->button() == LeftButton) { MouseEvent* mEvt = static_cast(evt); if (!slider->mouseEventIsInThumb(mEvt)) { slider->setValueForPosition(slider->positionForOffset(IntPoint(mEvt->offsetX(), mEvt->offsetY()))); } } if (evt->isMouseEvent() || evt->isDragEvent() || evt->isWheelEvent()) slider->forwardEvent(evt); } } 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); } 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); } void HTMLInputElement::setMaxLength(int _maxLength) { setAttribute(maxlengthAttr, String::number(_maxLength)); } void HTMLInputElement::setSize(unsigned _size) { setAttribute(sizeAttr, String::number(_size)); } String 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); } String HTMLInputElement::constrainValue(const String& proposedValue) const { return constrainValue(proposedValue, m_maxLen); } void HTMLInputElement::recheckValue() { String oldValue = value(); String newValue = constrainValue(oldValue); if (newValue != oldValue) setValue(newValue); } String HTMLInputElement::constrainValue(const String& proposedValue, int maxLen) const { if (isTextField()) { StringImpl* s = proposedValue.impl(); int newLen = numCharactersInGraphemeClusters(s, maxLen); for (int i = 0; i < newLen; ++i) { const UChar current = (*s)[i]; if (current < ' ' && current != '\t') { newLen = i; break; } } if (newLen < static_cast(proposedValue.length())) return proposedValue.substring(0, newLen); } return proposedValue; } void HTMLInputElement::addSearchResult() { ASSERT(isSearchField()); if (renderer()) static_cast(renderer())->addSearchResult(); } void HTMLInputElement::onSearch() { ASSERT(isSearchField()); if (renderer()) static_cast(renderer())->stopSearchEventTimer(); dispatchHTMLEvent(searchEvent, true, false); } Selection HTMLInputElement::selection() const { if (!renderer() || !isTextField() || cachedSelStart == -1 || cachedSelEnd == -1) return Selection(); return static_cast(renderer())->selection(cachedSelStart, cachedSelEnd); } void HTMLInputElement::didRestoreFromCache() { ASSERT(inputType() == PASSWORD); reset(); } void HTMLInputElement::willMoveToNewOwnerDocument() { if (inputType() == PASSWORD) document()->unregisterForDidRestoreFromCacheCallback(this); document()->checkedRadioButtons().removeButton(this); HTMLFormControlElementWithState::willMoveToNewOwnerDocument(); } void HTMLInputElement::didMoveToNewOwnerDocument() { if (inputType() == PASSWORD) document()->registerForDidRestoreFromCacheCallback(this); HTMLFormControlElementWithState::didMoveToNewOwnerDocument(); } } // namespace