| /* |
| * 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*>(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<unsigned>(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<BeforeTextInsertedEvent*>(event); |
| #if ENABLE(WCSS) |
| RefPtr<Range> 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 |
| |
| } |