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 * (C) 2006 Alexey Proskuryakov (ap (at) nypop.com) 6 * Copyright (C) 2004, 2005, 2006, 2010 Apple Inc. All rights reserved. 7 * Copyright (C) 2010 Google Inc. All rights reserved. 8 * Copyright (C) 2011 Motorola Mobility, Inc. All rights reserved. 9 * 10 * This library is free software; you can redistribute it and/or 11 * modify it under the terms of the GNU Library General Public 12 * License as published by the Free Software Foundation; either 13 * version 2 of the License, or (at your option) any later version. 14 * 15 * This library is distributed in the hope that it will be useful, 16 * but WITHOUT ANY WARRANTY; without even the implied warranty of 17 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18 * Library General Public License for more details. 19 * 20 * You should have received a copy of the GNU Library General Public License 21 * along with this library; see the file COPYING.LIB. If not, write to 22 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 23 * Boston, MA 02110-1301, USA. 24 * 25 */ 26 27 #include "config.h" 28 #include "core/html/HTMLOptionElement.h" 29 30 #include "bindings/core/v8/ExceptionState.h" 31 #include "core/HTMLNames.h" 32 #include "core/dom/Document.h" 33 #include "core/dom/NodeRenderStyle.h" 34 #include "core/dom/NodeTraversal.h" 35 #include "core/dom/ScriptLoader.h" 36 #include "core/dom/Text.h" 37 #include "core/dom/shadow/ShadowRoot.h" 38 #include "core/html/HTMLDataListElement.h" 39 #include "core/html/HTMLOptGroupElement.h" 40 #include "core/html/HTMLSelectElement.h" 41 #include "core/html/parser/HTMLParserIdioms.h" 42 #include "core/rendering/RenderTheme.h" 43 #include "wtf/Vector.h" 44 #include "wtf/text/StringBuilder.h" 45 46 namespace blink { 47 48 using namespace HTMLNames; 49 50 HTMLOptionElement::HTMLOptionElement(Document& document) 51 : HTMLElement(optionTag, document) 52 , m_disabled(false) 53 , m_isSelected(false) 54 { 55 setHasCustomStyleCallbacks(); 56 } 57 58 HTMLOptionElement::~HTMLOptionElement() 59 { 60 } 61 62 PassRefPtrWillBeRawPtr<HTMLOptionElement> HTMLOptionElement::create(Document& document) 63 { 64 RefPtrWillBeRawPtr<HTMLOptionElement> option = adoptRefWillBeNoop(new HTMLOptionElement(document)); 65 option->ensureUserAgentShadowRoot(); 66 return option.release(); 67 } 68 69 PassRefPtrWillBeRawPtr<HTMLOptionElement> HTMLOptionElement::createForJSConstructor(Document& document, const String& data, const AtomicString& value, 70 bool defaultSelected, bool selected, ExceptionState& exceptionState) 71 { 72 RefPtrWillBeRawPtr<HTMLOptionElement> element = adoptRefWillBeNoop(new HTMLOptionElement(document)); 73 element->ensureUserAgentShadowRoot(); 74 element->appendChild(Text::create(document, data.isNull() ? "" : data), exceptionState); 75 if (exceptionState.hadException()) 76 return nullptr; 77 78 if (!value.isNull()) 79 element->setValue(value); 80 if (defaultSelected) 81 element->setAttribute(selectedAttr, emptyAtom); 82 element->setSelected(selected); 83 84 return element.release(); 85 } 86 87 void HTMLOptionElement::attach(const AttachContext& context) 88 { 89 AttachContext optionContext(context); 90 if (context.resolvedStyle) { 91 ASSERT(!m_style || m_style == context.resolvedStyle); 92 m_style = context.resolvedStyle; 93 } else { 94 updateNonRenderStyle(); 95 optionContext.resolvedStyle = m_style.get(); 96 } 97 HTMLElement::attach(optionContext); 98 } 99 100 void HTMLOptionElement::detach(const AttachContext& context) 101 { 102 m_style.clear(); 103 HTMLElement::detach(context); 104 } 105 106 String HTMLOptionElement::text() const 107 { 108 Document& document = this->document(); 109 String text; 110 111 // WinIE does not use the label attribute, so as a quirk, we ignore it. 112 if (!document.inQuirksMode()) 113 text = fastGetAttribute(labelAttr); 114 115 // FIXME: The following treats an element with the label attribute set to 116 // the empty string the same as an element with no label attribute at all. 117 // Is that correct? If it is, then should the label function work the same way? 118 if (text.isEmpty()) 119 text = collectOptionInnerText(); 120 121 return text.stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); 122 } 123 124 void HTMLOptionElement::setText(const String &text, ExceptionState& exceptionState) 125 { 126 RefPtrWillBeRawPtr<Node> protectFromMutationEvents(this); 127 128 // Changing the text causes a recalc of a select's items, which will reset the selected 129 // index to the first item if the select is single selection with a menu list. We attempt to 130 // preserve the selected item. 131 RefPtrWillBeRawPtr<HTMLSelectElement> select = ownerSelectElement(); 132 bool selectIsMenuList = select && select->usesMenuList(); 133 int oldSelectedIndex = selectIsMenuList ? select->selectedIndex() : -1; 134 135 // Handle the common special case where there's exactly 1 child node, and it's a text node. 136 Node* child = firstChild(); 137 if (child && child->isTextNode() && !child->nextSibling()) 138 toText(child)->setData(text); 139 else { 140 removeChildren(); 141 appendChild(Text::create(document(), text), exceptionState); 142 } 143 144 if (selectIsMenuList && select->selectedIndex() != oldSelectedIndex) 145 select->setSelectedIndex(oldSelectedIndex); 146 } 147 148 void HTMLOptionElement::accessKeyAction(bool) 149 { 150 if (HTMLSelectElement* select = ownerSelectElement()) 151 select->accessKeySetSelectedIndex(index()); 152 } 153 154 int HTMLOptionElement::index() const 155 { 156 // It would be faster to cache the index, but harder to get it right in all cases. 157 158 HTMLSelectElement* selectElement = ownerSelectElement(); 159 if (!selectElement) 160 return 0; 161 162 int optionIndex = 0; 163 164 const WillBeHeapVector<RawPtrWillBeMember<HTMLElement> >& items = selectElement->listItems(); 165 size_t length = items.size(); 166 for (size_t i = 0; i < length; ++i) { 167 if (!isHTMLOptionElement(*items[i])) 168 continue; 169 if (items[i].get() == this) 170 return optionIndex; 171 ++optionIndex; 172 } 173 174 return 0; 175 } 176 177 void HTMLOptionElement::parseAttribute(const QualifiedName& name, const AtomicString& value) 178 { 179 if (name == valueAttr) { 180 if (HTMLDataListElement* dataList = ownerDataListElement()) 181 dataList->optionElementChildrenChanged(); 182 } else if (name == disabledAttr) { 183 bool oldDisabled = m_disabled; 184 m_disabled = !value.isNull(); 185 if (oldDisabled != m_disabled) { 186 pseudoStateChanged(CSSSelector::PseudoDisabled); 187 pseudoStateChanged(CSSSelector::PseudoEnabled); 188 if (renderer() && renderer()->style()->hasAppearance()) 189 RenderTheme::theme().stateChanged(renderer(), EnabledControlState); 190 } 191 } else if (name == selectedAttr) { 192 if (bool willBeSelected = !value.isNull()) 193 setSelected(willBeSelected); 194 } else if (name == labelAttr) { 195 updateLabel(); 196 } else 197 HTMLElement::parseAttribute(name, value); 198 } 199 200 String HTMLOptionElement::value() const 201 { 202 const AtomicString& value = fastGetAttribute(valueAttr); 203 if (!value.isNull()) 204 return value; 205 return collectOptionInnerText().stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); 206 } 207 208 void HTMLOptionElement::setValue(const AtomicString& value) 209 { 210 setAttribute(valueAttr, value); 211 } 212 213 bool HTMLOptionElement::selected() const 214 { 215 if (HTMLSelectElement* select = ownerSelectElement()) { 216 // If a stylesheet contains option:checked selectors, this function is 217 // called during parsing. updateListItemSelectedStates() is O(N) where N 218 // is the number of option elements, so the <select> parsing would be 219 // O(N^2) without the isFinishedParsingChildren check. Also, 220 // updateListItemSelectedStates() determines default selection, and we'd 221 // like to avoid to determine default selection with incomplete option 222 // list. 223 if (!select->isFinishedParsingChildren()) 224 return m_isSelected; 225 select->updateListItemSelectedStates(); 226 } 227 return m_isSelected; 228 } 229 230 void HTMLOptionElement::setSelected(bool selected) 231 { 232 if (m_isSelected == selected) 233 return; 234 235 setSelectedState(selected); 236 237 if (HTMLSelectElement* select = ownerSelectElement()) 238 select->optionSelectionStateChanged(this, selected); 239 } 240 241 void HTMLOptionElement::setSelectedState(bool selected) 242 { 243 if (m_isSelected == selected) 244 return; 245 246 m_isSelected = selected; 247 pseudoStateChanged(CSSSelector::PseudoChecked); 248 249 if (HTMLSelectElement* select = ownerSelectElement()) 250 select->invalidateSelectedItems(); 251 } 252 253 void HTMLOptionElement::childrenChanged(const ChildrenChange& change) 254 { 255 if (HTMLDataListElement* dataList = ownerDataListElement()) 256 dataList->optionElementChildrenChanged(); 257 else if (HTMLSelectElement* select = ownerSelectElement()) 258 select->optionElementChildrenChanged(); 259 updateLabel(); 260 HTMLElement::childrenChanged(change); 261 } 262 263 HTMLDataListElement* HTMLOptionElement::ownerDataListElement() const 264 { 265 return Traversal<HTMLDataListElement>::firstAncestor(*this); 266 } 267 268 HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const 269 { 270 return Traversal<HTMLSelectElement>::firstAncestor(*this); 271 } 272 273 String HTMLOptionElement::label() const 274 { 275 const AtomicString& label = fastGetAttribute(labelAttr); 276 if (!label.isNull()) 277 return label; 278 return collectOptionInnerText().stripWhiteSpace(isHTMLSpace<UChar>).simplifyWhiteSpace(isHTMLSpace<UChar>); 279 } 280 281 void HTMLOptionElement::setLabel(const AtomicString& label) 282 { 283 setAttribute(labelAttr, label); 284 } 285 286 void HTMLOptionElement::updateNonRenderStyle() 287 { 288 m_style = originalStyleForRenderer(); 289 if (HTMLSelectElement* select = ownerSelectElement()) 290 select->updateListOnRenderer(); 291 } 292 293 RenderStyle* HTMLOptionElement::nonRendererStyle() const 294 { 295 return m_style.get(); 296 } 297 298 PassRefPtr<RenderStyle> HTMLOptionElement::customStyleForRenderer() 299 { 300 updateNonRenderStyle(); 301 return m_style; 302 } 303 304 void HTMLOptionElement::didRecalcStyle(StyleRecalcChange change) 305 { 306 if (change == NoChange) 307 return; 308 309 // FIXME: We ask our owner select to repaint regardless of which property changed. 310 if (HTMLSelectElement* select = ownerSelectElement()) { 311 if (RenderObject* renderer = select->renderer()) 312 renderer->setShouldDoFullPaintInvalidation(true); 313 } 314 } 315 316 String HTMLOptionElement::textIndentedToRespectGroupLabel() const 317 { 318 ContainerNode* parent = parentNode(); 319 if (parent && isHTMLOptGroupElement(*parent)) 320 return " " + text(); 321 return text(); 322 } 323 324 bool HTMLOptionElement::isDisabledFormControl() const 325 { 326 if (ownElementDisabled()) 327 return true; 328 if (Element* parent = parentElement()) 329 return isHTMLOptGroupElement(*parent) && parent->isDisabledFormControl(); 330 return false; 331 } 332 333 Node::InsertionNotificationRequest HTMLOptionElement::insertedInto(ContainerNode* insertionPoint) 334 { 335 if (HTMLSelectElement* select = ownerSelectElement()) { 336 select->setRecalcListItems(); 337 // Do not call selected() since calling updateListItemSelectedStates() 338 // at this time won't do the right thing. (Why, exactly?) 339 if (m_isSelected) { 340 // FIXME: Might be better to call this unconditionally, always 341 // passing m_isSelected, rather than only calling it if we are 342 // selected. 343 select->optionSelectionStateChanged(this, true); 344 select->scrollToSelection(); 345 } 346 } 347 348 return HTMLElement::insertedInto(insertionPoint); 349 } 350 351 void HTMLOptionElement::removedFrom(ContainerNode* insertionPoint) 352 { 353 if (HTMLSelectElement* select = Traversal<HTMLSelectElement>::firstAncestorOrSelf(*insertionPoint)) { 354 select->setRecalcListItems(); 355 select->optionRemoved(*this); 356 } 357 HTMLElement::removedFrom(insertionPoint); 358 } 359 360 String HTMLOptionElement::collectOptionInnerText() const 361 { 362 StringBuilder text; 363 for (Node* node = firstChild(); node; ) { 364 if (node->isTextNode()) 365 text.append(node->nodeValue()); 366 // Text nodes inside script elements are not part of the option text. 367 if (node->isElementNode() && toScriptLoaderIfPossible(toElement(node))) 368 node = NodeTraversal::nextSkippingChildren(*node, this); 369 else 370 node = NodeTraversal::next(*node, this); 371 } 372 return text.toString(); 373 } 374 375 HTMLFormElement* HTMLOptionElement::form() const 376 { 377 if (HTMLSelectElement* selectElement = ownerSelectElement()) 378 return selectElement->formOwner(); 379 380 return 0; 381 } 382 383 void HTMLOptionElement::didAddUserAgentShadowRoot(ShadowRoot& root) 384 { 385 updateLabel(); 386 } 387 388 void HTMLOptionElement::updateLabel() 389 { 390 if (ShadowRoot* root = userAgentShadowRoot()) 391 root->setTextContent(textIndentedToRespectGroupLabel()); 392 } 393 394 bool HTMLOptionElement::spatialNavigationFocused() const 395 { 396 HTMLSelectElement* select = ownerSelectElement(); 397 if (!select || !select->focused()) 398 return false; 399 return select->spatialNavigationFocusedOption() == this; 400 } 401 402 bool HTMLOptionElement::isDisplayNone() const 403 { 404 // If m_style is not set, then the node is still unattached. 405 // We have to wait till it gets attached to read the display property. 406 if (!m_style) 407 return false; 408 409 if (m_style->display() != NONE) { 410 Element* parent = parentElement(); 411 ASSERT(parent); 412 if (isHTMLOptGroupElement(*parent)) { 413 RenderStyle* parentStyle = parent->renderStyle() ? parent->renderStyle() : parent->computedStyle(); 414 return !parentStyle || parentStyle->display() == NONE; 415 } 416 } 417 return m_style->display() == NONE; 418 } 419 420 } // namespace blink 421