/* * Copyright (C) 2008 Torch Mobile Inc. All rights reserved. (http://www.torchmobile.com/) * * 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 "InputElement.h" #include "BeforeTextInsertedEvent.h" #if ENABLE(WCSS) #include "CSSPropertyNames.h" #include "CSSRule.h" #include "CSSRuleList.h" #include "CSSStyleRule.h" #include "CSSStyleSelector.h" #endif #include "Attribute.h" #include "Chrome.h" #include "ChromeClient.h" #include "Document.h" #include "Event.h" #include "EventNames.h" #include "Frame.h" #include "Page.h" #include "RenderTextControlSingleLine.h" #include "SelectionController.h" #include "TextIterator.h" namespace WebCore { // FIXME: According to HTML4, the length attribute's value can be arbitrarily // large. However, due to https://bugs.webkit.org/show_bug.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. const int InputElement::s_maximumLength = 524288; const int InputElement::s_defaultSize = 20; void InputElement::dispatchFocusEvent(InputElement* inputElement, Element* element) { if (!inputElement->isTextField()) return; Document* document = element->document(); if (inputElement->isPasswordField() && document->frame()) document->setUseSecureKeyboardEntryWhenActive(true); } void InputElement::dispatchBlurEvent(InputElement* inputElement, Element* element) { if (!inputElement->isTextField()) return; Document* document = element->document(); Frame* frame = document->frame(); if (!frame) return; if (inputElement->isPasswordField()) document->setUseSecureKeyboardEntryWhenActive(false); frame->editor()->textFieldDidEndEditing(element); } void InputElement::updateFocusAppearance(InputElementData& data, InputElement* inputElement, Element* element, bool restorePreviousSelection) { ASSERT(inputElement->isTextField()); if (!restorePreviousSelection || data.cachedSelectionStart() == -1) inputElement->select(); else // Restore the cached selection. updateSelectionRange(inputElement, element, data.cachedSelectionStart(), data.cachedSelectionEnd()); Document* document = element->document(); if (document && document->frame()) document->frame()->selection()->revealSelection(); } void InputElement::updateSelectionRange(InputElement* inputElement, Element* element, int start, int end) { if (!inputElement->isTextField()) return; setSelectionRange(element, start, end); } void InputElement::aboutToUnload(InputElement* inputElement, Element* element) { if (!inputElement->isTextField() || !element->focused()) return; Document* document = element->document(); Frame* frame = document->frame(); if (!frame) return; frame->editor()->textFieldDidEndEditing(element); } void InputElement::setValueFromRenderer(InputElementData& data, InputElement* inputElement, Element* element, const String& value) { // Renderer and our event handler are responsible for sanitizing values. ASSERT_UNUSED(inputElement, value == inputElement->sanitizeValue(value) || inputElement->sanitizeValue(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") data.setValue(""); else data.setValue(value); element->setFormControlValueMatchesRenderer(true); // Input event is fired by the Node::defaultEventHandler for editable controls. if (!inputElement->isTextField()) element->dispatchInputEvent(); notifyFormStateChanged(element); } static String replaceEOLAndLimitLength(const InputElement* inputElement, const String& proposedValue, int maxLength) { if (!inputElement->isTextField()) return proposedValue; String string = proposedValue; string.replace("\r\n", " "); string.replace('\r', ' '); string.replace('\n', ' '); unsigned newLength = numCharactersInGraphemeClusters(string, maxLength); for (unsigned i = 0; i < newLength; ++i) { const UChar current = string[i]; if (current < ' ' && current != '\t') { newLength = i; break; } } return string.left(newLength); } String InputElement::sanitizeValueForTextField(const InputElement* inputElement, const String& proposedValue) { #if ENABLE(WCSS) InputElementData data = const_cast(inputElement)->data(); if (!isConformToInputMask(data, proposedValue)) { if (isConformToInputMask(data, data.value())) return data.value(); return String(); } #endif return replaceEOLAndLimitLength(inputElement, proposedValue, s_maximumLength); } String InputElement::sanitizeUserInputValue(const InputElement* inputElement, const String& proposedValue, int maxLength) { return replaceEOLAndLimitLength(inputElement, proposedValue, maxLength); } void InputElement::handleBeforeTextInsertedEvent(InputElementData& data, InputElement* inputElement, Element* element, Event* event) { ASSERT(event->isBeforeTextInsertedEvent()); // Make sure that the text to be inserted will not violate the maxLength. // We use RenderTextControlSingleLine::text() instead of InputElement::value() // because they can be mismatched by sanitizeValue() in // RenderTextControlSingleLine::subtreeHasChanged() in some cases. unsigned oldLength = numGraphemeClusters(toRenderTextControlSingleLine(element->renderer())->text()); // selectionLength represents the selection length of this text field to be // removed by this insertion. // If the text field has no focus, we don't need to take account of the // selection length. The selection is the source of text drag-and-drop in // that case, and nothing in the text field will be removed. unsigned selectionLength = element->focused() ? numGraphemeClusters(plainText(element->document()->frame()->selection()->selection().toNormalizedRange().get())) : 0; ASSERT(oldLength >= selectionLength); // Selected characters will be removed by the next text event. unsigned baseLength = oldLength - selectionLength; unsigned maxLength = static_cast(inputElement->supportsMaxLength() ? data.maxLength() : s_maximumLength); // maxLength() can never be negative. unsigned appendableLength = maxLength > baseLength ? maxLength - baseLength : 0; // Truncate the inserted text to avoid violating the maxLength and other constraints. BeforeTextInsertedEvent* textEvent = static_cast(event); #if ENABLE(WCSS) RefPtr range = element->document()->frame()->selection()->selection().toNormalizedRange(); String candidateString = toRenderTextControlSingleLine(element->renderer())->text(); if (selectionLength) candidateString.replace(range->startOffset(), range->endOffset(), textEvent->text()); else candidateString.insert(textEvent->text(), range->startOffset()); if (!isConformToInputMask(inputElement->data(), candidateString)) { textEvent->setText(""); return; } #endif textEvent->setText(sanitizeUserInputValue(inputElement, textEvent->text(), appendableLength)); } void InputElement::parseSizeAttribute(InputElementData& data, Element* element, Attribute* attribute) { data.setSize(attribute->isNull() ? InputElement::s_defaultSize : attribute->value().toInt()); if (RenderObject* renderer = element->renderer()) renderer->setNeedsLayoutAndPrefWidthsRecalc(); } void InputElement::parseMaxLengthAttribute(InputElementData& data, InputElement* inputElement, Element* element, Attribute* attribute) { int maxLength = attribute->isNull() ? InputElement::s_maximumLength : attribute->value().toInt(); if (maxLength <= 0 || maxLength > InputElement::s_maximumLength) maxLength = InputElement::s_maximumLength; int oldMaxLength = data.maxLength(); data.setMaxLength(maxLength); if (oldMaxLength != maxLength) updateValueIfNeeded(data, inputElement); element->setNeedsStyleRecalc(); } void InputElement::updateValueIfNeeded(InputElementData& data, InputElement* inputElement) { String oldValue = data.value(); String newValue = inputElement->sanitizeValue(oldValue); if (newValue != oldValue) inputElement->setValue(newValue); } void InputElement::notifyFormStateChanged(Element* element) { Document* document = element->document(); Frame* frame = document->frame(); if (!frame) return; if (Page* page = frame->page()) page->chrome()->client()->formStateDidChange(element); } // InputElementData InputElementData::InputElementData() : m_size(InputElement::s_defaultSize) , m_maxLength(InputElement::s_maximumLength) , m_cachedSelectionStart(-1) , m_cachedSelectionEnd(-1) #if ENABLE(WCSS) , m_inputFormatMask("*m") , m_maxInputCharsAllowed(InputElement::s_maximumLength) #endif { } InputElementData::~InputElementData() { } const AtomicString& InputElementData::name() const { return m_name.isNull() ? emptyAtom : m_name; } #if ENABLE(WCSS) static inline const AtomicString& formatCodes() { DEFINE_STATIC_LOCAL(AtomicString, codes, ("AaNnXxMm")); return codes; } static unsigned cursorPositionToMaskIndex(const String& inputFormatMask, unsigned cursorPosition) { UChar mask; int index = -1; do { mask = inputFormatMask[++index]; if (mask == '\\') ++index; else if (mask == '*' || (isASCIIDigit(mask) && mask != '0')) { index = inputFormatMask.length() - 1; break; } } while (cursorPosition--); return index; } bool InputElement::isConformToInputMask(const InputElementData& data, const String& inputChars) { for (unsigned i = 0; i < inputChars.length(); ++i) if (!isConformToInputMask(data, inputChars[i], i)) return false; return true; } bool InputElement::isConformToInputMask(const InputElementData& data, UChar inChar, unsigned cursorPosition) { String inputFormatMask = data.inputFormatMask(); if (inputFormatMask.isEmpty() || inputFormatMask == "*M" || inputFormatMask == "*m") return true; if (cursorPosition >= data.maxInputCharsAllowed()) return false; unsigned maskIndex = cursorPositionToMaskIndex(inputFormatMask, cursorPosition); bool ok = true; UChar mask = inputFormatMask[maskIndex]; // Match the inputed character with input mask switch (mask) { case 'A': ok = !isASCIIDigit(inChar) && !isASCIILower(inChar) && isASCIIPrintable(inChar); break; case 'a': ok = !isASCIIDigit(inChar) && !isASCIIUpper(inChar) && isASCIIPrintable(inChar); break; case 'N': ok = isASCIIDigit(inChar); break; case 'n': ok = !isASCIIAlpha(inChar) && isASCIIPrintable(inChar); break; case 'X': ok = !isASCIILower(inChar) && isASCIIPrintable(inChar); break; case 'x': ok = !isASCIIUpper(inChar) && isASCIIPrintable(inChar); break; case 'M': case 'm': ok = isASCIIPrintable(inChar); break; default: ok = (mask == inChar); break; } return ok; } String InputElement::validateInputMask(InputElementData& data, String& inputMask) { inputMask.replace("\\\\", "\\"); bool isValid = true; bool hasWildcard = false; unsigned escapeCharCount = 0; unsigned maskLength = inputMask.length(); UChar formatCode; for (unsigned i = 0; i < maskLength; ++i) { formatCode = inputMask[i]; if (formatCodes().find(formatCode) == -1) { if (formatCode == '*' || (isASCIIDigit(formatCode) && formatCode != '0')) { // Validate codes which ends with '*f' or 'nf' formatCode = inputMask[++i]; if ((i + 1 != maskLength) || formatCodes().find(formatCode) == -1) { isValid = false; break; } hasWildcard = true; } else if (formatCode == '\\') { // skip over the next mask character ++i; ++escapeCharCount; } else { isValid = false; break; } } } if (!isValid) return String(); // calculate the number of characters allowed to be entered by input mask unsigned allowedLength = maskLength; if (escapeCharCount) allowedLength -= escapeCharCount; if (hasWildcard) { formatCode = inputMask[maskLength - 2]; if (formatCode == '*') allowedLength = data.maxInputCharsAllowed(); else { unsigned leftLen = String(&formatCode).toInt(); allowedLength = leftLen + allowedLength - 2; } } if (allowedLength < data.maxInputCharsAllowed()) data.setMaxInputCharsAllowed(allowedLength); return inputMask; } #endif }