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 *           (C) 2006 Alexey Proskuryakov (ap@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
46namespace blink {
47
48using namespace HTMLNames;
49
50HTMLOptionElement::HTMLOptionElement(Document& document)
51    : HTMLElement(optionTag, document)
52    , m_disabled(false)
53    , m_isSelected(false)
54{
55    setHasCustomStyleCallbacks();
56}
57
58HTMLOptionElement::~HTMLOptionElement()
59{
60}
61
62PassRefPtrWillBeRawPtr<HTMLOptionElement> HTMLOptionElement::create(Document& document)
63{
64    RefPtrWillBeRawPtr<HTMLOptionElement> option = adoptRefWillBeNoop(new HTMLOptionElement(document));
65    option->ensureUserAgentShadowRoot();
66    return option.release();
67}
68
69PassRefPtrWillBeRawPtr<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
87void 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
100void HTMLOptionElement::detach(const AttachContext& context)
101{
102    m_style.clear();
103    HTMLElement::detach(context);
104}
105
106String 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
124void 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
148void HTMLOptionElement::accessKeyAction(bool)
149{
150    if (HTMLSelectElement* select = ownerSelectElement())
151        select->accessKeySetSelectedIndex(index());
152}
153
154int 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
177void 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
200String 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
208void HTMLOptionElement::setValue(const AtomicString& value)
209{
210    setAttribute(valueAttr, value);
211}
212
213bool 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
230void 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
241void 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
253void 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
263HTMLDataListElement* HTMLOptionElement::ownerDataListElement() const
264{
265    return Traversal<HTMLDataListElement>::firstAncestor(*this);
266}
267
268HTMLSelectElement* HTMLOptionElement::ownerSelectElement() const
269{
270    return Traversal<HTMLSelectElement>::firstAncestor(*this);
271}
272
273String 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
281void HTMLOptionElement::setLabel(const AtomicString& label)
282{
283    setAttribute(labelAttr, label);
284}
285
286void HTMLOptionElement::updateNonRenderStyle()
287{
288    m_style = originalStyleForRenderer();
289    if (HTMLSelectElement* select = ownerSelectElement())
290        select->updateListOnRenderer();
291}
292
293RenderStyle* HTMLOptionElement::nonRendererStyle() const
294{
295    return m_style.get();
296}
297
298PassRefPtr<RenderStyle> HTMLOptionElement::customStyleForRenderer()
299{
300    updateNonRenderStyle();
301    return m_style;
302}
303
304void 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
316String HTMLOptionElement::textIndentedToRespectGroupLabel() const
317{
318    ContainerNode* parent = parentNode();
319    if (parent && isHTMLOptGroupElement(*parent))
320        return "    " + text();
321    return text();
322}
323
324bool 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
333Node::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
351void 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
360String 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
375HTMLFormElement* HTMLOptionElement::form() const
376{
377    if (HTMLSelectElement* selectElement = ownerSelectElement())
378        return selectElement->formOwner();
379
380    return 0;
381}
382
383void HTMLOptionElement::didAddUserAgentShadowRoot(ShadowRoot& root)
384{
385    updateLabel();
386}
387
388void HTMLOptionElement::updateLabel()
389{
390    if (ShadowRoot* root = userAgentShadowRoot())
391        root->setTextContent(textIndentedToRespectGroupLabel());
392}
393
394bool HTMLOptionElement::spatialNavigationFocused() const
395{
396    HTMLSelectElement* select = ownerSelectElement();
397    if (!select || !select->focused())
398        return false;
399    return select->spatialNavigationFocusedOption() == this;
400}
401
402bool 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