1/* 2 * Copyright (C) 1999 Lars Knoll (knoll@kde.org) 3 * (C) 1999 Antti Koivisto (koivisto@kde.org) 4 * (C) 2001 Dirk Mueller (mueller@kde.org) 5 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2010 Apple Inc. All rights reserved. 6 * (C) 2006 Alexey Proskuryakov (ap@nypop.com) 7 * Copyright (C) 2007 Samuel Weinig (sam@webkit.org) 8 * 9 * This library is free software; you can redistribute it and/or 10 * modify it under the terms of the GNU Library General Public 11 * License as published by the Free Software Foundation; either 12 * version 2 of the License, or (at your option) any later version. 13 * 14 * This library is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 17 * Library General Public License for more details. 18 * 19 * You should have received a copy of the GNU Library General Public License 20 * along with this library; see the file COPYING.LIB. If not, write to 21 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 22 * Boston, MA 02110-1301, USA. 23 * 24 */ 25 26#include "config.h" 27#include "core/html/HTMLTextAreaElement.h" 28 29#include "bindings/core/v8/ExceptionState.h" 30#include "bindings/core/v8/ExceptionStatePlaceholder.h" 31#include "core/CSSValueKeywords.h" 32#include "core/HTMLNames.h" 33#include "core/dom/Document.h" 34#include "core/dom/ExceptionCode.h" 35#include "core/dom/Text.h" 36#include "core/dom/shadow/ShadowRoot.h" 37#include "core/editing/FrameSelection.h" 38#include "core/editing/SpellChecker.h" 39#include "core/editing/TextIterator.h" 40#include "core/events/BeforeTextInsertedEvent.h" 41#include "core/events/Event.h" 42#include "core/frame/FrameHost.h" 43#include "core/frame/LocalFrame.h" 44#include "core/html/FormDataList.h" 45#include "core/html/forms/FormController.h" 46#include "core/html/shadow/ShadowElementNames.h" 47#include "core/html/shadow/TextControlInnerElements.h" 48#include "core/page/Chrome.h" 49#include "core/page/ChromeClient.h" 50#include "core/rendering/RenderTextControlMultiLine.h" 51#include "platform/text/PlatformLocale.h" 52#include "wtf/StdLibExtras.h" 53#include "wtf/text/StringBuilder.h" 54 55namespace blink { 56 57using namespace HTMLNames; 58 59static const int defaultRows = 2; 60static const int defaultCols = 20; 61 62// On submission, LF characters are converted into CRLF. 63// This function returns number of characters considering this. 64static unsigned numberOfLineBreaks(const String& text) 65{ 66 unsigned length = text.length(); 67 unsigned count = 0; 68 for (unsigned i = 0; i < length; i++) { 69 if (text[i] == '\n') 70 count++; 71 } 72 return count; 73} 74 75static inline unsigned computeLengthForSubmission(const String& text) 76{ 77 return text.length() + numberOfLineBreaks(text); 78} 79 80HTMLTextAreaElement::HTMLTextAreaElement(Document& document, HTMLFormElement* form) 81 : HTMLTextFormControlElement(textareaTag, document, form) 82 , m_rows(defaultRows) 83 , m_cols(defaultCols) 84 , m_wrap(SoftWrap) 85 , m_isDirty(false) 86 , m_valueIsUpToDate(true) 87{ 88} 89 90PassRefPtrWillBeRawPtr<HTMLTextAreaElement> HTMLTextAreaElement::create(Document& document, HTMLFormElement* form) 91{ 92 RefPtrWillBeRawPtr<HTMLTextAreaElement> textArea = adoptRefWillBeNoop(new HTMLTextAreaElement(document, form)); 93 textArea->ensureUserAgentShadowRoot(); 94 return textArea.release(); 95} 96 97void HTMLTextAreaElement::didAddUserAgentShadowRoot(ShadowRoot& root) 98{ 99 root.appendChild(TextControlInnerEditorElement::create(document())); 100} 101 102const AtomicString& HTMLTextAreaElement::formControlType() const 103{ 104 DEFINE_STATIC_LOCAL(const AtomicString, textarea, ("textarea", AtomicString::ConstructFromLiteral)); 105 return textarea; 106} 107 108FormControlState HTMLTextAreaElement::saveFormControlState() const 109{ 110 return m_isDirty ? FormControlState(value()) : FormControlState(); 111} 112 113void HTMLTextAreaElement::restoreFormControlState(const FormControlState& state) 114{ 115 setValue(state[0]); 116} 117 118void HTMLTextAreaElement::childrenChanged(const ChildrenChange& change) 119{ 120 HTMLElement::childrenChanged(change); 121 setLastChangeWasNotUserEdit(); 122 if (m_isDirty) 123 setInnerEditorValue(value()); 124 else 125 setNonDirtyValue(defaultValue()); 126} 127 128bool HTMLTextAreaElement::isPresentationAttribute(const QualifiedName& name) const 129{ 130 if (name == alignAttr) { 131 // Don't map 'align' attribute. This matches what Firefox, Opera and IE do. 132 // See http://bugs.webkit.org/show_bug.cgi?id=7075 133 return false; 134 } 135 136 if (name == wrapAttr) 137 return true; 138 return HTMLTextFormControlElement::isPresentationAttribute(name); 139} 140 141void HTMLTextAreaElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStylePropertySet* style) 142{ 143 if (name == wrapAttr) { 144 if (shouldWrapText()) { 145 addPropertyToPresentationAttributeStyle(style, CSSPropertyWhiteSpace, CSSValuePreWrap); 146 addPropertyToPresentationAttributeStyle(style, CSSPropertyWordWrap, CSSValueBreakWord); 147 } else { 148 addPropertyToPresentationAttributeStyle(style, CSSPropertyWhiteSpace, CSSValuePre); 149 addPropertyToPresentationAttributeStyle(style, CSSPropertyWordWrap, CSSValueNormal); 150 } 151 } else 152 HTMLTextFormControlElement::collectStyleForPresentationAttribute(name, value, style); 153} 154 155void HTMLTextAreaElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 156{ 157 if (name == rowsAttr) { 158 int rows = value.toInt(); 159 if (rows <= 0) 160 rows = defaultRows; 161 if (m_rows != rows) { 162 m_rows = rows; 163 if (renderer()) 164 renderer()->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(); 165 } 166 } else if (name == colsAttr) { 167 int cols = value.toInt(); 168 if (cols <= 0) 169 cols = defaultCols; 170 if (m_cols != cols) { 171 m_cols = cols; 172 if (renderer()) 173 renderer()->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(); 174 } 175 } else if (name == wrapAttr) { 176 // The virtual/physical values were a Netscape extension of HTML 3.0, now deprecated. 177 // The soft/hard /off values are a recommendation for HTML 4 extension by IE and NS 4. 178 WrapMethod wrap; 179 if (equalIgnoringCase(value, "physical") || equalIgnoringCase(value, "hard") || equalIgnoringCase(value, "on")) 180 wrap = HardWrap; 181 else if (equalIgnoringCase(value, "off")) 182 wrap = NoWrap; 183 else 184 wrap = SoftWrap; 185 if (wrap != m_wrap) { 186 m_wrap = wrap; 187 if (renderer()) 188 renderer()->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(); 189 } 190 } else if (name == accesskeyAttr) { 191 // ignore for the moment 192 } else if (name == maxlengthAttr) 193 setNeedsValidityCheck(); 194 else 195 HTMLTextFormControlElement::parseAttribute(name, value); 196} 197 198RenderObject* HTMLTextAreaElement::createRenderer(RenderStyle*) 199{ 200 return new RenderTextControlMultiLine(this); 201} 202 203bool HTMLTextAreaElement::appendFormData(FormDataList& encoding, bool) 204{ 205 if (name().isEmpty()) 206 return false; 207 208 document().updateLayout(); 209 210 const String& text = (m_wrap == HardWrap) ? valueWithHardLineBreaks() : value(); 211 encoding.appendData(name(), text); 212 213 const AtomicString& dirnameAttrValue = fastGetAttribute(dirnameAttr); 214 if (!dirnameAttrValue.isNull()) 215 encoding.appendData(dirnameAttrValue, directionForFormData()); 216 return true; 217} 218 219void HTMLTextAreaElement::resetImpl() 220{ 221 setNonDirtyValue(defaultValue()); 222} 223 224bool HTMLTextAreaElement::hasCustomFocusLogic() const 225{ 226 return true; 227} 228 229bool HTMLTextAreaElement::isKeyboardFocusable() const 230{ 231 // If a given text area can be focused at all, then it will always be keyboard focusable. 232 return isFocusable(); 233} 234 235bool HTMLTextAreaElement::shouldShowFocusRingOnMouseFocus() const 236{ 237 return true; 238} 239 240void HTMLTextAreaElement::updateFocusAppearance(bool restorePreviousSelection) 241{ 242 if (!restorePreviousSelection) 243 setSelectionRange(0, 0); 244 else 245 restoreCachedSelection(); 246 247 if (document().frame()) 248 document().frame()->selection().revealSelection(); 249} 250 251void HTMLTextAreaElement::defaultEventHandler(Event* event) 252{ 253 if (renderer() && (event->isMouseEvent() || event->isDragEvent() || event->hasInterface(EventNames::WheelEvent) || event->type() == EventTypeNames::blur)) 254 forwardEvent(event); 255 else if (renderer() && event->isBeforeTextInsertedEvent()) 256 handleBeforeTextInsertedEvent(static_cast<BeforeTextInsertedEvent*>(event)); 257 258 HTMLTextFormControlElement::defaultEventHandler(event); 259} 260 261void HTMLTextAreaElement::handleFocusEvent(Element*, FocusType) 262{ 263 if (LocalFrame* frame = document().frame()) 264 frame->spellChecker().didBeginEditing(this); 265} 266 267void HTMLTextAreaElement::subtreeHasChanged() 268{ 269 setChangedSinceLastFormControlChangeEvent(true); 270 m_valueIsUpToDate = false; 271 setNeedsValidityCheck(); 272 setAutofilled(false); 273 274 if (!focused()) 275 return; 276 277 // When typing in a textarea, childrenChanged is not called, so we need to force the directionality check. 278 calculateAndAdjustDirectionality(); 279 280 ASSERT(document().isActive()); 281 document().frameHost()->chrome().client().didChangeValueInTextField(*this); 282} 283 284void HTMLTextAreaElement::handleBeforeTextInsertedEvent(BeforeTextInsertedEvent* event) const 285{ 286 ASSERT(event); 287 ASSERT(renderer()); 288 int signedMaxLength = maxLength(); 289 if (signedMaxLength < 0) 290 return; 291 unsigned unsignedMaxLength = static_cast<unsigned>(signedMaxLength); 292 293 const String& currentValue = innerEditorValue(); 294 unsigned currentLength = computeLengthForSubmission(currentValue); 295 if (currentLength + computeLengthForSubmission(event->text()) < unsignedMaxLength) 296 return; 297 298 // selectionLength represents the selection length of this text field to be 299 // removed by this insertion. 300 // If the text field has no focus, we don't need to take account of the 301 // selection length. The selection is the source of text drag-and-drop in 302 // that case, and nothing in the text field will be removed. 303 unsigned selectionLength = 0; 304 if (focused()) { 305 Position start, end; 306 document().frame()->selection().selection().toNormalizedPositions(start, end); 307 selectionLength = computeLengthForSubmission(plainText(start, end)); 308 } 309 ASSERT(currentLength >= selectionLength); 310 unsigned baseLength = currentLength - selectionLength; 311 unsigned appendableLength = unsignedMaxLength > baseLength ? unsignedMaxLength - baseLength : 0; 312 event->setText(sanitizeUserInputValue(event->text(), appendableLength)); 313} 314 315String HTMLTextAreaElement::sanitizeUserInputValue(const String& proposedValue, unsigned maxLength) 316{ 317 if (maxLength > 0 && U16_IS_LEAD(proposedValue[maxLength - 1])) 318 --maxLength; 319 return proposedValue.left(maxLength); 320} 321 322void HTMLTextAreaElement::updateValue() const 323{ 324 if (m_valueIsUpToDate) 325 return; 326 327 m_value = innerEditorValue(); 328 const_cast<HTMLTextAreaElement*>(this)->m_valueIsUpToDate = true; 329 const_cast<HTMLTextAreaElement*>(this)->notifyFormStateChanged(); 330 m_isDirty = true; 331 const_cast<HTMLTextAreaElement*>(this)->updatePlaceholderVisibility(false); 332} 333 334String HTMLTextAreaElement::value() const 335{ 336 updateValue(); 337 return m_value; 338} 339 340void HTMLTextAreaElement::setValue(const String& value, TextFieldEventBehavior eventBehavior) 341{ 342 RefPtrWillBeRawPtr<HTMLTextAreaElement> protector(this); 343 setValueCommon(value, eventBehavior); 344 m_isDirty = true; 345 if (document().focusedElement() == this) 346 document().frameHost()->chrome().client().didUpdateTextOfFocusedElementByNonUserInput(); 347} 348 349void HTMLTextAreaElement::setNonDirtyValue(const String& value) 350{ 351 setValueCommon(value, DispatchNoEvent, SetSeletion); 352 m_isDirty = false; 353} 354 355void HTMLTextAreaElement::setValueCommon(const String& newValue, TextFieldEventBehavior eventBehavior, SetValueCommonOption setValueOption) 356{ 357 // Code elsewhere normalizes line endings added by the user via the keyboard or pasting. 358 // We normalize line endings coming from JavaScript here. 359 String normalizedValue = newValue.isNull() ? "" : newValue; 360 normalizedValue.replace("\r\n", "\n"); 361 normalizedValue.replace('\r', '\n'); 362 363 // Return early because we don't want to trigger other side effects 364 // when the value isn't changing. 365 // FIXME: Simple early return doesn't match the Firefox ever. 366 // Remove these lines. 367 if (normalizedValue == value()) { 368 if (setValueOption == SetSeletion) { 369 setNeedsValidityCheck(); 370 if (isFinishedParsingChildren()) { 371 // Set the caret to the end of the text value except for initialize. 372 unsigned endOfString = m_value.length(); 373 setSelectionRange(endOfString, endOfString, SelectionHasNoDirection, ChangeSelectionIfFocused); 374 } 375 } 376 return; 377 } 378 379 m_value = normalizedValue; 380 setInnerEditorValue(m_value); 381 if (eventBehavior == DispatchNoEvent) 382 setLastChangeWasNotUserEdit(); 383 updatePlaceholderVisibility(false); 384 setNeedsStyleRecalc(SubtreeStyleChange); 385 m_suggestedValue = String(); 386 setNeedsValidityCheck(); 387 if (isFinishedParsingChildren()) { 388 // Set the caret to the end of the text value except for initialize. 389 unsigned endOfString = m_value.length(); 390 setSelectionRange(endOfString, endOfString, SelectionHasNoDirection, ChangeSelectionIfFocused); 391 } 392 393 notifyFormStateChanged(); 394 if (eventBehavior == DispatchNoEvent) { 395 setTextAsOfLastFormControlChangeEvent(normalizedValue); 396 } else { 397 if (eventBehavior == DispatchInputAndChangeEvent) 398 dispatchFormControlInputEvent(); 399 dispatchFormControlChangeEvent(); 400 } 401} 402 403void HTMLTextAreaElement::setInnerEditorValue(const String& value) 404{ 405 HTMLTextFormControlElement::setInnerEditorValue(value); 406 m_valueIsUpToDate = true; 407} 408 409String HTMLTextAreaElement::defaultValue() const 410{ 411 StringBuilder value; 412 413 // Since there may be comments, ignore nodes other than text nodes. 414 for (Node* n = firstChild(); n; n = n->nextSibling()) { 415 if (n->isTextNode()) 416 value.append(toText(n)->data()); 417 } 418 419 return value.toString(); 420} 421 422void HTMLTextAreaElement::setDefaultValue(const String& defaultValue) 423{ 424 RefPtrWillBeRawPtr<Node> protectFromMutationEvents(this); 425 426 // To preserve comments, remove only the text nodes, then add a single text node. 427 WillBeHeapVector<RefPtrWillBeMember<Node> > textNodes; 428 for (Node* n = firstChild(); n; n = n->nextSibling()) { 429 if (n->isTextNode()) 430 textNodes.append(n); 431 } 432 size_t size = textNodes.size(); 433 for (size_t i = 0; i < size; ++i) 434 removeChild(textNodes[i].get(), IGNORE_EXCEPTION); 435 436 // Normalize line endings. 437 String value = defaultValue; 438 value.replace("\r\n", "\n"); 439 value.replace('\r', '\n'); 440 441 insertBefore(document().createTextNode(value), firstChild(), IGNORE_EXCEPTION); 442 443 if (!m_isDirty) 444 setNonDirtyValue(value); 445} 446 447int HTMLTextAreaElement::maxLength() const 448{ 449 bool ok; 450 int value = getAttribute(maxlengthAttr).toInt(&ok); 451 return ok && value >= 0 ? value : -1; 452} 453 454void HTMLTextAreaElement::setMaxLength(int newValue, ExceptionState& exceptionState) 455{ 456 if (newValue < 0) 457 exceptionState.throwDOMException(IndexSizeError, "The value provided (" + String::number(newValue) + ") is not positive or 0."); 458 else 459 setIntegralAttribute(maxlengthAttr, newValue); 460} 461 462String HTMLTextAreaElement::suggestedValue() const 463{ 464 return m_suggestedValue; 465} 466 467void HTMLTextAreaElement::setSuggestedValue(const String& value) 468{ 469 m_suggestedValue = value; 470 471 if (!value.isNull()) 472 setInnerEditorValue(m_suggestedValue); 473 else 474 setInnerEditorValue(m_value); 475 updatePlaceholderVisibility(false); 476 setNeedsStyleRecalc(SubtreeStyleChange); 477} 478 479String HTMLTextAreaElement::validationMessage() const 480{ 481 if (!willValidate()) 482 return String(); 483 484 if (customError()) 485 return customValidationMessage(); 486 487 if (valueMissing()) 488 return locale().queryString(blink::WebLocalizedString::ValidationValueMissing); 489 490 if (tooLong()) 491 return locale().validationMessageTooLongText(computeLengthForSubmission(value()), maxLength()); 492 493 return String(); 494} 495 496bool HTMLTextAreaElement::valueMissing() const 497{ 498 // We should not call value() for performance. 499 return willValidate() && valueMissing(0); 500} 501 502bool HTMLTextAreaElement::valueMissing(const String* value) const 503{ 504 return isRequiredFormControl() && !isDisabledOrReadOnly() && (value ? *value : this->value()).isEmpty(); 505} 506 507bool HTMLTextAreaElement::tooLong() const 508{ 509 // We should not call value() for performance. 510 return willValidate() && tooLong(0, CheckDirtyFlag); 511} 512 513bool HTMLTextAreaElement::tooLong(const String* value, NeedsToCheckDirtyFlag check) const 514{ 515 // Return false for the default value or value set by script even if it is 516 // longer than maxLength. 517 if (check == CheckDirtyFlag && !lastChangeWasUserEdit()) 518 return false; 519 520 int max = maxLength(); 521 if (max < 0) 522 return false; 523 return computeLengthForSubmission(value ? *value : this->value()) > static_cast<unsigned>(max); 524} 525 526bool HTMLTextAreaElement::isValidValue(const String& candidate) const 527{ 528 return !valueMissing(&candidate) && !tooLong(&candidate, IgnoreDirtyFlag); 529} 530 531void HTMLTextAreaElement::accessKeyAction(bool) 532{ 533 focus(); 534} 535 536void HTMLTextAreaElement::setCols(int cols) 537{ 538 setIntegralAttribute(colsAttr, cols); 539} 540 541void HTMLTextAreaElement::setRows(int rows) 542{ 543 setIntegralAttribute(rowsAttr, rows); 544} 545 546bool HTMLTextAreaElement::matchesReadOnlyPseudoClass() const 547{ 548 return isReadOnly(); 549} 550 551bool HTMLTextAreaElement::matchesReadWritePseudoClass() const 552{ 553 return !isReadOnly(); 554} 555 556void HTMLTextAreaElement::updatePlaceholderText() 557{ 558 HTMLElement* placeholder = placeholderElement(); 559 const AtomicString& placeholderText = fastGetAttribute(placeholderAttr); 560 if (placeholderText.isEmpty()) { 561 if (placeholder) 562 userAgentShadowRoot()->removeChild(placeholder); 563 return; 564 } 565 if (!placeholder) { 566 RefPtrWillBeRawPtr<HTMLDivElement> newElement = HTMLDivElement::create(document()); 567 placeholder = newElement.get(); 568 placeholder->setShadowPseudoId(AtomicString("-webkit-input-placeholder", AtomicString::ConstructFromLiteral)); 569 placeholder->setAttribute(idAttr, ShadowElementNames::placeholder()); 570 userAgentShadowRoot()->insertBefore(placeholder, innerEditorElement()->nextSibling()); 571 } 572 placeholder->setTextContent(placeholderText); 573} 574 575bool HTMLTextAreaElement::isInteractiveContent() const 576{ 577 return true; 578} 579 580bool HTMLTextAreaElement::supportsAutofocus() const 581{ 582 return true; 583} 584 585} 586