1 /* 2 * Copyright (C) 1999 Lars Knoll (knoll (at) kde.org) 3 * (C) 1999 Antti Koivisto (koivisto (at) kde.org) 4 * (C) 2001 Dirk Mueller (mueller (at) kde.org) 5 * Copyright (C) 2004, 2005, 2006, 2007, 2008, 2010 Apple Inc. All rights reserved. 6 * (C) 2006 Alexey Proskuryakov (ap (at) nypop.com) 7 * Copyright (C) 2007 Samuel Weinig (sam (at) 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/v8/ExceptionState.h" 30 #include "bindings/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 55 namespace WebCore { 56 57 using namespace HTMLNames; 58 59 static const int defaultRows = 2; 60 static const int defaultCols = 20; 61 62 // On submission, LF characters are converted into CRLF. 63 // This function returns number of characters considering this. 64 static 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 75 static inline unsigned computeLengthForSubmission(const String& text) 76 { 77 return text.length() + numberOfLineBreaks(text); 78 } 79 80 HTMLTextAreaElement::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 ScriptWrappable::init(this); 89 } 90 91 PassRefPtrWillBeRawPtr<HTMLTextAreaElement> HTMLTextAreaElement::create(Document& document, HTMLFormElement* form) 92 { 93 RefPtrWillBeRawPtr<HTMLTextAreaElement> textArea = adoptRefWillBeNoop(new HTMLTextAreaElement(document, form)); 94 textArea->ensureUserAgentShadowRoot(); 95 return textArea.release(); 96 } 97 98 void HTMLTextAreaElement::didAddUserAgentShadowRoot(ShadowRoot& root) 99 { 100 root.appendChild(TextControlInnerEditorElement::create(document())); 101 } 102 103 const AtomicString& HTMLTextAreaElement::formControlType() const 104 { 105 DEFINE_STATIC_LOCAL(const AtomicString, textarea, ("textarea", AtomicString::ConstructFromLiteral)); 106 return textarea; 107 } 108 109 FormControlState HTMLTextAreaElement::saveFormControlState() const 110 { 111 return m_isDirty ? FormControlState(value()) : FormControlState(); 112 } 113 114 void HTMLTextAreaElement::restoreFormControlState(const FormControlState& state) 115 { 116 setValue(state[0]); 117 } 118 119 void HTMLTextAreaElement::childrenChanged(bool changedByParser, Node* beforeChange, Node* afterChange, int childCountDelta) 120 { 121 HTMLElement::childrenChanged(changedByParser, beforeChange, afterChange, childCountDelta); 122 setLastChangeWasNotUserEdit(); 123 if (m_isDirty) 124 setInnerEditorValue(value()); 125 else 126 setNonDirtyValue(defaultValue()); 127 } 128 129 bool HTMLTextAreaElement::isPresentationAttribute(const QualifiedName& name) const 130 { 131 if (name == alignAttr) { 132 // Don't map 'align' attribute. This matches what Firefox, Opera and IE do. 133 // See http://bugs.webkit.org/show_bug.cgi?id=7075 134 return false; 135 } 136 137 if (name == wrapAttr) 138 return true; 139 return HTMLTextFormControlElement::isPresentationAttribute(name); 140 } 141 142 void HTMLTextAreaElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStylePropertySet* style) 143 { 144 if (name == wrapAttr) { 145 if (shouldWrapText()) { 146 addPropertyToPresentationAttributeStyle(style, CSSPropertyWhiteSpace, CSSValuePreWrap); 147 addPropertyToPresentationAttributeStyle(style, CSSPropertyWordWrap, CSSValueBreakWord); 148 } else { 149 addPropertyToPresentationAttributeStyle(style, CSSPropertyWhiteSpace, CSSValuePre); 150 addPropertyToPresentationAttributeStyle(style, CSSPropertyWordWrap, CSSValueNormal); 151 } 152 } else 153 HTMLTextFormControlElement::collectStyleForPresentationAttribute(name, value, style); 154 } 155 156 void HTMLTextAreaElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 157 { 158 if (name == rowsAttr) { 159 int rows = value.toInt(); 160 if (rows <= 0) 161 rows = defaultRows; 162 if (m_rows != rows) { 163 m_rows = rows; 164 if (renderer()) 165 renderer()->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(); 166 } 167 } else if (name == colsAttr) { 168 int cols = value.toInt(); 169 if (cols <= 0) 170 cols = defaultCols; 171 if (m_cols != cols) { 172 m_cols = cols; 173 if (renderer()) 174 renderer()->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(); 175 } 176 } else if (name == wrapAttr) { 177 // The virtual/physical values were a Netscape extension of HTML 3.0, now deprecated. 178 // The soft/hard /off values are a recommendation for HTML 4 extension by IE and NS 4. 179 WrapMethod wrap; 180 if (equalIgnoringCase(value, "physical") || equalIgnoringCase(value, "hard") || equalIgnoringCase(value, "on")) 181 wrap = HardWrap; 182 else if (equalIgnoringCase(value, "off")) 183 wrap = NoWrap; 184 else 185 wrap = SoftWrap; 186 if (wrap != m_wrap) { 187 m_wrap = wrap; 188 if (renderer()) 189 renderer()->setNeedsLayoutAndPrefWidthsRecalcAndFullPaintInvalidation(); 190 } 191 } else if (name == accesskeyAttr) { 192 // ignore for the moment 193 } else if (name == maxlengthAttr) 194 setNeedsValidityCheck(); 195 else 196 HTMLTextFormControlElement::parseAttribute(name, value); 197 } 198 199 RenderObject* HTMLTextAreaElement::createRenderer(RenderStyle*) 200 { 201 return new RenderTextControlMultiLine(this); 202 } 203 204 bool HTMLTextAreaElement::appendFormData(FormDataList& encoding, bool) 205 { 206 if (name().isEmpty()) 207 return false; 208 209 document().updateLayout(); 210 211 const String& text = (m_wrap == HardWrap) ? valueWithHardLineBreaks() : value(); 212 encoding.appendData(name(), text); 213 214 const AtomicString& dirnameAttrValue = fastGetAttribute(dirnameAttr); 215 if (!dirnameAttrValue.isNull()) 216 encoding.appendData(dirnameAttrValue, directionForFormData()); 217 return true; 218 } 219 220 void HTMLTextAreaElement::resetImpl() 221 { 222 setNonDirtyValue(defaultValue()); 223 } 224 225 bool HTMLTextAreaElement::hasCustomFocusLogic() const 226 { 227 return true; 228 } 229 230 bool HTMLTextAreaElement::isKeyboardFocusable() const 231 { 232 // If a given text area can be focused at all, then it will always be keyboard focusable. 233 return isFocusable(); 234 } 235 236 bool HTMLTextAreaElement::shouldShowFocusRingOnMouseFocus() const 237 { 238 return true; 239 } 240 241 void HTMLTextAreaElement::updateFocusAppearance(bool restorePreviousSelection) 242 { 243 if (!restorePreviousSelection) 244 setSelectionRange(0, 0); 245 else 246 restoreCachedSelection(); 247 248 if (document().frame()) 249 document().frame()->selection().revealSelection(); 250 } 251 252 void HTMLTextAreaElement::defaultEventHandler(Event* event) 253 { 254 if (renderer() && (event->isMouseEvent() || event->isDragEvent() || event->hasInterface(EventNames::WheelEvent) || event->type() == EventTypeNames::blur)) 255 forwardEvent(event); 256 else if (renderer() && event->isBeforeTextInsertedEvent()) 257 handleBeforeTextInsertedEvent(static_cast<BeforeTextInsertedEvent*>(event)); 258 259 HTMLTextFormControlElement::defaultEventHandler(event); 260 } 261 262 void HTMLTextAreaElement::handleFocusEvent(Element*, FocusType) 263 { 264 if (LocalFrame* frame = document().frame()) 265 frame->spellChecker().didBeginEditing(this); 266 } 267 268 void HTMLTextAreaElement::subtreeHasChanged() 269 { 270 setChangedSinceLastFormControlChangeEvent(true); 271 m_valueIsUpToDate = false; 272 setNeedsValidityCheck(); 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 284 void 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 = focused() ? computeLengthForSubmission(plainText(document().frame()->selection().selection().toNormalizedRange().get())) : 0; 304 ASSERT(currentLength >= selectionLength); 305 unsigned baseLength = currentLength - selectionLength; 306 unsigned appendableLength = unsignedMaxLength > baseLength ? unsignedMaxLength - baseLength : 0; 307 event->setText(sanitizeUserInputValue(event->text(), appendableLength)); 308 } 309 310 String HTMLTextAreaElement::sanitizeUserInputValue(const String& proposedValue, unsigned maxLength) 311 { 312 if (maxLength > 0 && U16_IS_LEAD(proposedValue[maxLength - 1])) 313 --maxLength; 314 return proposedValue.left(maxLength); 315 } 316 317 void HTMLTextAreaElement::updateValue() const 318 { 319 if (m_valueIsUpToDate) 320 return; 321 322 ASSERT(renderer()); 323 m_value = innerEditorValue(); 324 const_cast<HTMLTextAreaElement*>(this)->m_valueIsUpToDate = true; 325 const_cast<HTMLTextAreaElement*>(this)->notifyFormStateChanged(); 326 m_isDirty = true; 327 const_cast<HTMLTextAreaElement*>(this)->updatePlaceholderVisibility(false); 328 } 329 330 String HTMLTextAreaElement::value() const 331 { 332 updateValue(); 333 return m_value; 334 } 335 336 void HTMLTextAreaElement::setValue(const String& value, TextFieldEventBehavior eventBehavior) 337 { 338 RefPtrWillBeRawPtr<HTMLTextAreaElement> protector(this); 339 setValueCommon(value, eventBehavior); 340 m_isDirty = true; 341 setNeedsValidityCheck(); 342 if (document().focusedElement() == this) 343 document().frameHost()->chrome().client().didUpdateTextOfFocusedElementByNonUserInput(); 344 } 345 346 void HTMLTextAreaElement::setNonDirtyValue(const String& value) 347 { 348 setValueCommon(value, DispatchNoEvent); 349 m_isDirty = false; 350 setNeedsValidityCheck(); 351 } 352 353 void HTMLTextAreaElement::setValueCommon(const String& newValue, TextFieldEventBehavior eventBehavior) 354 { 355 // Code elsewhere normalizes line endings added by the user via the keyboard or pasting. 356 // We normalize line endings coming from JavaScript here. 357 String normalizedValue = newValue.isNull() ? "" : newValue; 358 normalizedValue.replace("\r\n", "\n"); 359 normalizedValue.replace('\r', '\n'); 360 361 // Return early because we don't want to move the caret or trigger other side effects 362 // when the value isn't changing. This matches Firefox behavior, at least. 363 if (normalizedValue == value()) 364 return; 365 366 m_value = normalizedValue; 367 setInnerEditorValue(m_value); 368 if (eventBehavior == DispatchNoEvent) 369 setLastChangeWasNotUserEdit(); 370 updatePlaceholderVisibility(false); 371 setNeedsStyleRecalc(SubtreeStyleChange); 372 m_suggestedValue = String(); 373 374 // Set the caret to the end of the text value. 375 if (document().focusedElement() == this) { 376 unsigned endOfString = m_value.length(); 377 setSelectionRange(endOfString, endOfString); 378 } 379 380 notifyFormStateChanged(); 381 if (eventBehavior == DispatchNoEvent) { 382 setTextAsOfLastFormControlChangeEvent(normalizedValue); 383 } else { 384 if (eventBehavior == DispatchInputAndChangeEvent) 385 dispatchFormControlInputEvent(); 386 dispatchFormControlChangeEvent(); 387 } 388 } 389 390 void HTMLTextAreaElement::setInnerEditorValue(const String& value) 391 { 392 HTMLTextFormControlElement::setInnerEditorValue(value); 393 m_valueIsUpToDate = true; 394 } 395 396 String HTMLTextAreaElement::defaultValue() const 397 { 398 StringBuilder value; 399 400 // Since there may be comments, ignore nodes other than text nodes. 401 for (Node* n = firstChild(); n; n = n->nextSibling()) { 402 if (n->isTextNode()) 403 value.append(toText(n)->data()); 404 } 405 406 return value.toString(); 407 } 408 409 void HTMLTextAreaElement::setDefaultValue(const String& defaultValue) 410 { 411 RefPtrWillBeRawPtr<Node> protectFromMutationEvents(this); 412 413 // To preserve comments, remove only the text nodes, then add a single text node. 414 WillBeHeapVector<RefPtrWillBeMember<Node> > textNodes; 415 for (Node* n = firstChild(); n; n = n->nextSibling()) { 416 if (n->isTextNode()) 417 textNodes.append(n); 418 } 419 size_t size = textNodes.size(); 420 for (size_t i = 0; i < size; ++i) 421 removeChild(textNodes[i].get(), IGNORE_EXCEPTION); 422 423 // Normalize line endings. 424 String value = defaultValue; 425 value.replace("\r\n", "\n"); 426 value.replace('\r', '\n'); 427 428 insertBefore(document().createTextNode(value), firstChild(), IGNORE_EXCEPTION); 429 430 if (!m_isDirty) 431 setNonDirtyValue(value); 432 } 433 434 int HTMLTextAreaElement::maxLength() const 435 { 436 bool ok; 437 int value = getAttribute(maxlengthAttr).string().toInt(&ok); 438 return ok && value >= 0 ? value : -1; 439 } 440 441 void HTMLTextAreaElement::setMaxLength(int newValue, ExceptionState& exceptionState) 442 { 443 if (newValue < 0) 444 exceptionState.throwDOMException(IndexSizeError, "The value provided (" + String::number(newValue) + ") is not positive or 0."); 445 else 446 setIntegralAttribute(maxlengthAttr, newValue); 447 } 448 449 String HTMLTextAreaElement::suggestedValue() const 450 { 451 return m_suggestedValue; 452 } 453 454 void HTMLTextAreaElement::setSuggestedValue(const String& value) 455 { 456 m_suggestedValue = value; 457 458 if (!value.isNull()) 459 setInnerEditorValue(m_suggestedValue); 460 else 461 setInnerEditorValue(m_value); 462 updatePlaceholderVisibility(false); 463 setNeedsStyleRecalc(SubtreeStyleChange); 464 } 465 466 String HTMLTextAreaElement::validationMessage() const 467 { 468 if (!willValidate()) 469 return String(); 470 471 if (customError()) 472 return customValidationMessage(); 473 474 if (valueMissing()) 475 return locale().queryString(blink::WebLocalizedString::ValidationValueMissing); 476 477 if (tooLong()) 478 return locale().validationMessageTooLongText(computeLengthForSubmission(value()), maxLength()); 479 480 return String(); 481 } 482 483 bool HTMLTextAreaElement::valueMissing() const 484 { 485 return willValidate() && valueMissing(value()); 486 } 487 488 bool HTMLTextAreaElement::tooLong() const 489 { 490 return willValidate() && tooLong(value(), CheckDirtyFlag); 491 } 492 493 bool HTMLTextAreaElement::tooLong(const String& value, NeedsToCheckDirtyFlag check) const 494 { 495 // Return false for the default value or value set by script even if it is 496 // longer than maxLength. 497 if (check == CheckDirtyFlag && !lastChangeWasUserEdit()) 498 return false; 499 500 int max = maxLength(); 501 if (max < 0) 502 return false; 503 return computeLengthForSubmission(value) > static_cast<unsigned>(max); 504 } 505 506 bool HTMLTextAreaElement::isValidValue(const String& candidate) const 507 { 508 return !valueMissing(candidate) && !tooLong(candidate, IgnoreDirtyFlag); 509 } 510 511 void HTMLTextAreaElement::accessKeyAction(bool) 512 { 513 focus(); 514 } 515 516 void HTMLTextAreaElement::setCols(int cols) 517 { 518 setIntegralAttribute(colsAttr, cols); 519 } 520 521 void HTMLTextAreaElement::setRows(int rows) 522 { 523 setIntegralAttribute(rowsAttr, rows); 524 } 525 526 bool HTMLTextAreaElement::shouldUseInputMethod() 527 { 528 return true; 529 } 530 531 bool HTMLTextAreaElement::matchesReadOnlyPseudoClass() const 532 { 533 return isReadOnly(); 534 } 535 536 bool HTMLTextAreaElement::matchesReadWritePseudoClass() const 537 { 538 return !isReadOnly(); 539 } 540 541 void HTMLTextAreaElement::updatePlaceholderText() 542 { 543 HTMLElement* placeholder = placeholderElement(); 544 const AtomicString& placeholderText = fastGetAttribute(placeholderAttr); 545 if (placeholderText.isEmpty()) { 546 if (placeholder) 547 userAgentShadowRoot()->removeChild(placeholder); 548 return; 549 } 550 if (!placeholder) { 551 RefPtrWillBeRawPtr<HTMLDivElement> newElement = HTMLDivElement::create(document()); 552 placeholder = newElement.get(); 553 placeholder->setShadowPseudoId(AtomicString("-webkit-input-placeholder", AtomicString::ConstructFromLiteral)); 554 placeholder->setAttribute(idAttr, ShadowElementNames::placeholder()); 555 userAgentShadowRoot()->insertBefore(placeholder, innerEditorElement()->nextSibling()); 556 } 557 placeholder->setTextContent(placeholderText); 558 } 559 560 bool HTMLTextAreaElement::isInteractiveContent() const 561 { 562 return true; 563 } 564 565 bool HTMLTextAreaElement::supportsAutofocus() const 566 { 567 return true; 568 } 569 570 } 571