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 "HTMLTextAreaElement.h" 28 29#include "Attribute.h" 30#include "BeforeTextInsertedEvent.h" 31#include "CSSValueKeywords.h" 32#include "Document.h" 33#include "Editor.h" 34#include "Event.h" 35#include "EventHandler.h" 36#include "EventNames.h" 37#include "ExceptionCode.h" 38#include "ExceptionCodePlaceholder.h" 39#include "FormController.h" 40#include "FormDataList.h" 41#include "Frame.h" 42#include "FrameSelection.h" 43#include "HTMLNames.h" 44#include "LocalizedStrings.h" 45#include "RenderTextControlMultiLine.h" 46#include "ShadowRoot.h" 47#include "Text.h" 48#include "TextBreakIterator.h" 49#include "TextControlInnerElements.h" 50#include "TextIterator.h" 51#include "TextNodeTraversal.h" 52#include <wtf/Ref.h> 53#include <wtf/StdLibExtras.h> 54#include <wtf/text/StringBuilder.h> 55 56namespace WebCore { 57 58using namespace HTMLNames; 59 60static const int defaultRows = 2; 61static const int defaultCols = 20; 62 63// On submission, LF characters are converted into CRLF. 64// This function returns number of characters considering this. 65static inline unsigned computeLengthForSubmission(const String& text, unsigned numberOfLineBreaks) 66{ 67 return numGraphemeClusters(text) + numberOfLineBreaks; 68} 69 70static unsigned numberOfLineBreaks(const String& text) 71{ 72 unsigned length = text.length(); 73 unsigned count = 0; 74 for (unsigned i = 0; i < length; i++) { 75 if (text[i] == '\n') 76 count++; 77 } 78 return count; 79} 80 81static inline unsigned computeLengthForSubmission(const String& text) 82{ 83 return numGraphemeClusters(text) + numberOfLineBreaks(text); 84} 85 86static inline unsigned upperBoundForLengthForSubmission(const String& text, unsigned numberOfLineBreaks) 87{ 88 return text.length() + numberOfLineBreaks; 89} 90 91HTMLTextAreaElement::HTMLTextAreaElement(const QualifiedName& tagName, Document& document, HTMLFormElement* form) 92 : HTMLTextFormControlElement(tagName, document, form) 93 , m_rows(defaultRows) 94 , m_cols(defaultCols) 95 , m_wrap(SoftWrap) 96 , m_placeholder(0) 97 , m_isDirty(false) 98 , m_wasModifiedByUser(false) 99{ 100 ASSERT(hasTagName(textareaTag)); 101 setFormControlValueMatchesRenderer(true); 102} 103 104PassRefPtr<HTMLTextAreaElement> HTMLTextAreaElement::create(const QualifiedName& tagName, Document& document, HTMLFormElement* form) 105{ 106 RefPtr<HTMLTextAreaElement> textArea = adoptRef(new HTMLTextAreaElement(tagName, document, form)); 107 textArea->ensureUserAgentShadowRoot(); 108 return textArea.release(); 109} 110 111void HTMLTextAreaElement::didAddUserAgentShadowRoot(ShadowRoot* root) 112{ 113 root->appendChild(TextControlInnerTextElement::create(document()), ASSERT_NO_EXCEPTION); 114 updateInnerTextElementEditability(); 115} 116 117const AtomicString& HTMLTextAreaElement::formControlType() const 118{ 119 DEPRECATED_DEFINE_STATIC_LOCAL(const AtomicString, textarea, ("textarea", AtomicString::ConstructFromLiteral)); 120 return textarea; 121} 122 123FormControlState HTMLTextAreaElement::saveFormControlState() const 124{ 125 return m_isDirty ? FormControlState(value()) : FormControlState(); 126} 127 128void HTMLTextAreaElement::restoreFormControlState(const FormControlState& state) 129{ 130 setValue(state[0]); 131} 132 133void HTMLTextAreaElement::childrenChanged(const ChildChange& change) 134{ 135 HTMLElement::childrenChanged(change); 136 setLastChangeWasNotUserEdit(); 137 if (m_isDirty) 138 setInnerTextValue(value()); 139 else 140 setNonDirtyValue(defaultValue()); 141} 142 143bool HTMLTextAreaElement::isPresentationAttribute(const QualifiedName& name) const 144{ 145 if (name == alignAttr) { 146 // Don't map 'align' attribute. This matches what Firefox, Opera and IE do. 147 // See http://bugs.webkit.org/show_bug.cgi?id=7075 148 return false; 149 } 150 151 if (name == wrapAttr) 152 return true; 153 return HTMLTextFormControlElement::isPresentationAttribute(name); 154} 155 156void HTMLTextAreaElement::collectStyleForPresentationAttribute(const QualifiedName& name, const AtomicString& value, MutableStyleProperties& style) 157{ 158 if (name == wrapAttr) { 159 if (shouldWrapText()) { 160 addPropertyToPresentationAttributeStyle(style, CSSPropertyWhiteSpace, CSSValuePreWrap); 161 addPropertyToPresentationAttributeStyle(style, CSSPropertyWordWrap, CSSValueBreakWord); 162 } else { 163 addPropertyToPresentationAttributeStyle(style, CSSPropertyWhiteSpace, CSSValuePre); 164 addPropertyToPresentationAttributeStyle(style, CSSPropertyWordWrap, CSSValueNormal); 165 } 166 } else 167 HTMLTextFormControlElement::collectStyleForPresentationAttribute(name, value, style); 168} 169 170void HTMLTextAreaElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 171{ 172 if (name == rowsAttr) { 173 int rows = value.toInt(); 174 if (rows <= 0) 175 rows = defaultRows; 176 if (m_rows != rows) { 177 m_rows = rows; 178 if (renderer()) 179 renderer()->setNeedsLayoutAndPrefWidthsRecalc(); 180 } 181 } else if (name == colsAttr) { 182 int cols = value.toInt(); 183 if (cols <= 0) 184 cols = defaultCols; 185 if (m_cols != cols) { 186 m_cols = cols; 187 if (renderer()) 188 renderer()->setNeedsLayoutAndPrefWidthsRecalc(); 189 } 190 } else if (name == wrapAttr) { 191 // The virtual/physical values were a Netscape extension of HTML 3.0, now deprecated. 192 // The soft/hard /off values are a recommendation for HTML 4 extension by IE and NS 4. 193 WrapMethod wrap; 194 if (equalIgnoringCase(value, "physical") || equalIgnoringCase(value, "hard") || equalIgnoringCase(value, "on")) 195 wrap = HardWrap; 196 else if (equalIgnoringCase(value, "off")) 197 wrap = NoWrap; 198 else 199 wrap = SoftWrap; 200 if (wrap != m_wrap) { 201 m_wrap = wrap; 202 if (renderer()) 203 renderer()->setNeedsLayoutAndPrefWidthsRecalc(); 204 } 205 } else if (name == accesskeyAttr) { 206 // ignore for the moment 207 } else if (name == maxlengthAttr) 208 setNeedsValidityCheck(); 209 else 210 HTMLTextFormControlElement::parseAttribute(name, value); 211} 212 213RenderPtr<RenderElement> HTMLTextAreaElement::createElementRenderer(PassRef<RenderStyle> style) 214{ 215 return createRenderer<RenderTextControlMultiLine>(*this, WTF::move(style)); 216} 217 218bool HTMLTextAreaElement::appendFormData(FormDataList& encoding, bool) 219{ 220 if (name().isEmpty()) 221 return false; 222 223 document().updateLayout(); 224 225 const String& text = (m_wrap == HardWrap) ? valueWithHardLineBreaks() : value(); 226 encoding.appendData(name(), text); 227 228 const AtomicString& dirnameAttrValue = fastGetAttribute(dirnameAttr); 229 if (!dirnameAttrValue.isNull()) 230 encoding.appendData(dirnameAttrValue, directionForFormData()); 231 return true; 232} 233 234void HTMLTextAreaElement::reset() 235{ 236 setNonDirtyValue(defaultValue()); 237} 238 239bool HTMLTextAreaElement::hasCustomFocusLogic() const 240{ 241 return true; 242} 243 244bool HTMLTextAreaElement::isKeyboardFocusable(KeyboardEvent*) const 245{ 246 // If a given text area can be focused at all, then it will always be keyboard focusable. 247 return isFocusable(); 248} 249 250bool HTMLTextAreaElement::isMouseFocusable() const 251{ 252 return isFocusable(); 253} 254 255void HTMLTextAreaElement::updateFocusAppearance(bool restorePreviousSelection) 256{ 257 if (!restorePreviousSelection || !hasCachedSelection()) { 258 // If this is the first focus, set a caret at the beginning of the text. 259 // This matches some browsers' behavior; see bug 11746 Comment #15. 260 // http://bugs.webkit.org/show_bug.cgi?id=11746#c15 261 setSelectionRange(0, 0); 262 } else 263 restoreCachedSelection(); 264 265 if (document().frame()) 266 document().frame()->selection().revealSelection(); 267} 268 269void HTMLTextAreaElement::defaultEventHandler(Event* event) 270{ 271 if (renderer() && (event->isMouseEvent() || event->isDragEvent() || event->eventInterface() == WheelEventInterfaceType || event->type() == eventNames().blurEvent)) 272 forwardEvent(event); 273 else if (renderer() && event->isBeforeTextInsertedEvent()) 274 handleBeforeTextInsertedEvent(toBeforeTextInsertedEvent(event)); 275 276 HTMLTextFormControlElement::defaultEventHandler(event); 277} 278 279void HTMLTextAreaElement::subtreeHasChanged() 280{ 281 setChangedSinceLastFormControlChangeEvent(true); 282 setFormControlValueMatchesRenderer(false); 283 setNeedsValidityCheck(); 284 285 if (!focused()) 286 return; 287 288 if (Frame* frame = document().frame()) 289 frame->editor().textDidChangeInTextArea(this); 290 // When typing in a textarea, childrenChanged is not called, so we need to force the directionality check. 291 calculateAndAdjustDirectionality(); 292} 293 294void HTMLTextAreaElement::handleBeforeTextInsertedEvent(BeforeTextInsertedEvent* event) const 295{ 296 ASSERT(event); 297 ASSERT(renderer()); 298 int signedMaxLength = maxLength(); 299 if (signedMaxLength < 0) 300 return; 301 unsigned unsignedMaxLength = static_cast<unsigned>(signedMaxLength); 302 303 const String& currentValue = innerTextValue(); 304 unsigned numberOfLineBreaksInCurrentValue = numberOfLineBreaks(currentValue); 305 if (upperBoundForLengthForSubmission(currentValue, numberOfLineBreaksInCurrentValue) 306 + upperBoundForLengthForSubmission(event->text(), numberOfLineBreaks(event->text())) < unsignedMaxLength) 307 return; 308 309 unsigned currentLength = computeLengthForSubmission(currentValue, numberOfLineBreaksInCurrentValue); 310 // selectionLength represents the selection length of this text field to be 311 // removed by this insertion. 312 // If the text field has no focus, we don't need to take account of the 313 // selection length. The selection is the source of text drag-and-drop in 314 // that case, and nothing in the text field will be removed. 315 unsigned selectionLength = focused() ? computeLengthForSubmission(plainText(document().frame()->selection().selection().toNormalizedRange().get())) : 0; 316 ASSERT(currentLength >= selectionLength); 317 unsigned baseLength = currentLength - selectionLength; 318 unsigned appendableLength = unsignedMaxLength > baseLength ? unsignedMaxLength - baseLength : 0; 319 event->setText(sanitizeUserInputValue(event->text(), appendableLength)); 320} 321 322String HTMLTextAreaElement::sanitizeUserInputValue(const String& proposedValue, unsigned maxLength) 323{ 324 return proposedValue.left(numCharactersInGraphemeClusters(proposedValue, maxLength)); 325} 326 327TextControlInnerTextElement* HTMLTextAreaElement::innerTextElement() const 328{ 329 Node* node = userAgentShadowRoot()->firstChild(); 330 return toTextControlInnerTextElement(node); 331} 332 333void HTMLTextAreaElement::rendererWillBeDestroyed() 334{ 335 updateValue(); 336} 337 338void HTMLTextAreaElement::updateValue() const 339{ 340 if (formControlValueMatchesRenderer()) 341 return; 342 343 ASSERT(renderer()); 344 m_value = innerTextValue(); 345 const_cast<HTMLTextAreaElement*>(this)->setFormControlValueMatchesRenderer(true); 346 m_isDirty = true; 347 m_wasModifiedByUser = true; 348 const_cast<HTMLTextAreaElement*>(this)->updatePlaceholderVisibility(false); 349} 350 351String HTMLTextAreaElement::value() const 352{ 353 updateValue(); 354 return m_value; 355} 356 357void HTMLTextAreaElement::setValue(const String& value) 358{ 359 setValueCommon(value); 360 m_isDirty = true; 361 setNeedsValidityCheck(); 362} 363 364void HTMLTextAreaElement::setNonDirtyValue(const String& value) 365{ 366 setValueCommon(value); 367 m_isDirty = false; 368 setNeedsValidityCheck(); 369} 370 371void HTMLTextAreaElement::setValueCommon(const String& newValue) 372{ 373 m_wasModifiedByUser = false; 374 // Code elsewhere normalizes line endings added by the user via the keyboard or pasting. 375 // We normalize line endings coming from JavaScript here. 376 String normalizedValue = newValue.isNull() ? "" : newValue; 377 normalizedValue.replace("\r\n", "\n"); 378 normalizedValue.replace('\r', '\n'); 379 380 // Return early because we don't want to move the caret or trigger other side effects 381 // when the value isn't changing. This matches Firefox behavior, at least. 382 if (normalizedValue == value()) 383 return; 384 385 m_value = normalizedValue; 386 setInnerTextValue(m_value); 387 setLastChangeWasNotUserEdit(); 388 updatePlaceholderVisibility(false); 389 setNeedsStyleRecalc(); 390 setFormControlValueMatchesRenderer(true); 391 392 // Set the caret to the end of the text value. 393 if (document().focusedElement() == this) { 394 unsigned endOfString = m_value.length(); 395 setSelectionRange(endOfString, endOfString); 396 } 397 398 setTextAsOfLastFormControlChangeEvent(normalizedValue); 399} 400 401String HTMLTextAreaElement::defaultValue() const 402{ 403 return TextNodeTraversal::contentsAsString(this); 404} 405 406void HTMLTextAreaElement::setDefaultValue(const String& defaultValue) 407{ 408 Ref<HTMLTextAreaElement> protectFromMutationEvents(*this); 409 410 // To preserve comments, remove only the text nodes, then add a single text node. 411 Vector<RefPtr<Text>> textNodes; 412 for (Text* textNode = TextNodeTraversal::firstChild(this); textNode; textNode = TextNodeTraversal::nextSibling(textNode)) 413 textNodes.append(textNode); 414 415 size_t size = textNodes.size(); 416 for (size_t i = 0; i < size; ++i) 417 removeChild(textNodes[i].get(), IGNORE_EXCEPTION); 418 419 // Normalize line endings. 420 String value = defaultValue; 421 value.replace("\r\n", "\n"); 422 value.replace('\r', '\n'); 423 424 insertBefore(document().createTextNode(value), firstChild(), IGNORE_EXCEPTION); 425 426 if (!m_isDirty) 427 setNonDirtyValue(value); 428} 429 430int HTMLTextAreaElement::maxLength() const 431{ 432 bool ok; 433 int value = getAttribute(maxlengthAttr).string().toInt(&ok); 434 return ok && value >= 0 ? value : -1; 435} 436 437void HTMLTextAreaElement::setMaxLength(int newValue, ExceptionCode& ec) 438{ 439 if (newValue < 0) 440 ec = INDEX_SIZE_ERR; 441 else 442 setIntegralAttribute(maxlengthAttr, newValue); 443} 444 445String HTMLTextAreaElement::validationMessage() const 446{ 447 if (!willValidate()) 448 return String(); 449 450 if (customError()) 451 return customValidationMessage(); 452 453 if (valueMissing()) 454 return validationMessageValueMissingText(); 455 456 if (tooLong()) 457 return validationMessageTooLongText(computeLengthForSubmission(value()), maxLength()); 458 459 return String(); 460} 461 462bool HTMLTextAreaElement::valueMissing() const 463{ 464 return willValidate() && valueMissing(value()); 465} 466 467bool HTMLTextAreaElement::tooLong() const 468{ 469 return willValidate() && tooLong(value(), CheckDirtyFlag); 470} 471 472bool HTMLTextAreaElement::tooLong(const String& value, NeedsToCheckDirtyFlag check) const 473{ 474 // Return false for the default value or value set by script even if it is 475 // longer than maxLength. 476 if (check == CheckDirtyFlag && !m_wasModifiedByUser) 477 return false; 478 479 int max = maxLength(); 480 if (max < 0) 481 return false; 482 unsigned unsignedMax = static_cast<unsigned>(max); 483 unsigned numberOfLineBreaksInValue = numberOfLineBreaks(value); 484 return upperBoundForLengthForSubmission(value, numberOfLineBreaksInValue) > unsignedMax 485 && computeLengthForSubmission(value, numberOfLineBreaksInValue) > unsignedMax; 486} 487 488bool HTMLTextAreaElement::isValidValue(const String& candidate) const 489{ 490 return !valueMissing(candidate) && !tooLong(candidate, IgnoreDirtyFlag); 491} 492 493void HTMLTextAreaElement::accessKeyAction(bool) 494{ 495 focus(); 496} 497 498void HTMLTextAreaElement::setCols(int cols) 499{ 500 setIntegralAttribute(colsAttr, cols); 501} 502 503void HTMLTextAreaElement::setRows(int rows) 504{ 505 setIntegralAttribute(rowsAttr, rows); 506} 507 508bool HTMLTextAreaElement::shouldUseInputMethod() 509{ 510 return true; 511} 512 513HTMLElement* HTMLTextAreaElement::placeholderElement() const 514{ 515 return m_placeholder; 516} 517 518bool HTMLTextAreaElement::matchesReadOnlyPseudoClass() const 519{ 520 return isReadOnly(); 521} 522 523bool HTMLTextAreaElement::matchesReadWritePseudoClass() const 524{ 525 return !isDisabledOrReadOnly(); 526} 527 528void HTMLTextAreaElement::updatePlaceholderText() 529{ 530 String placeholderText = strippedPlaceholder(); 531 if (placeholderText.isEmpty()) { 532 if (m_placeholder) { 533 userAgentShadowRoot()->removeChild(m_placeholder, ASSERT_NO_EXCEPTION); 534 m_placeholder = 0; 535 } 536 return; 537 } 538 if (!m_placeholder) { 539 RefPtr<HTMLDivElement> placeholder = HTMLDivElement::create(document()); 540 m_placeholder = placeholder.get(); 541 m_placeholder->setPseudo(AtomicString("-webkit-input-placeholder", AtomicString::ConstructFromLiteral)); 542 userAgentShadowRoot()->insertBefore(m_placeholder, innerTextElement()->nextSibling()); 543 } 544 m_placeholder->setInnerText(placeholderText, ASSERT_NO_EXCEPTION); 545} 546 547bool HTMLTextAreaElement::willRespondToMouseClickEvents() 548{ 549 return !isDisabledFormControl(); 550} 551 552} // namespace WebCore 553